@sun-asterisk/sungen 3.2.24-beta.6 → 3.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +129 -15
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +3 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +4 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +3 -1
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +6 -1
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +1 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +6 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +5 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js +5 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +10 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +23 -0
- package/dist/orchestrator/templates/env.appium.example +11 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +79 -6
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +70 -7
- package/dist/orchestrator/templates/wdio.conf.ts +100 -3
- package/dist/utils/selector-types.d.ts +1 -1
- package/dist/utils/selector-types.d.ts.map +1 -1
- package/dist/utils/selector-types.js +1 -0
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +3 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs +8 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/location-services-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +6 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +129 -15
- package/src/generators/test-generator/template-engine.ts +4 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +4 -0
- package/src/harness/audit.ts +3 -1
- package/src/harness/capability-plan.ts +3 -1
- package/src/harness/parse.ts +2 -0
- package/src/harness/script-check.ts +6 -2
- package/src/harness/sensors.ts +5 -1
- package/src/orchestrator/mobile-runtime-scaffolder.ts +5 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +10 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +23 -0
- package/src/orchestrator/templates/env.appium.example +11 -0
- package/src/orchestrator/templates/specs-db.ts +70 -7
- package/src/orchestrator/templates/wdio.conf.ts +100 -3
- package/src/utils/selector-types.ts +1 -0
|
@@ -27,7 +27,7 @@ const ident = (s: string): string => {
|
|
|
27
27
|
// SQL engines: verified via the direct guarded-SELECT path and needing the `$n`→`?` placeholder
|
|
28
28
|
// rewrite. This literal is the single source of truth for that rewrite condition and is quoted, by
|
|
29
29
|
// name, in the sungen-gherkin-syntax skill's "direct SQL verification" note — keep them in sync.
|
|
30
|
-
const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'mysql'] as const;
|
|
30
|
+
const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'device-sqlite', 'mysql'] as const;
|
|
31
31
|
// NoSQL engines: implement the Engine contract natively (no SQL rewrite). Tracked separately so the
|
|
32
32
|
// SQL-rewrite guard and its skill-parity test stay pinned to the three SQL engines above.
|
|
33
33
|
const NOSQL_ENGINES = ['cosmos', 'mongodb', 'dynamodb'] as const;
|
|
@@ -43,9 +43,15 @@ interface SshConfig {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface DataSourceConfig {
|
|
46
|
-
engine: 'postgres' | 'mysql' | 'sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
|
|
46
|
+
engine: 'postgres' | 'mysql' | 'sqlite' | 'device-sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
|
|
47
47
|
url: string;
|
|
48
48
|
readonly?: boolean;
|
|
49
|
+
// device-sqlite (mobile): an ON-DEVICE app database, pulled FRESH per query through the active
|
|
50
|
+
// Appium session (pullFile's `@<package>/<path>` app-container syntax — run-as under the hood,
|
|
51
|
+
// so it needs a debuggable build or an emulator). `package` = the app id, `db_path` = the
|
|
52
|
+
// path inside the app's data dir (e.g. databases/app.db). No `url`. Same SQLite dialect.
|
|
53
|
+
package?: string;
|
|
54
|
+
db_path?: string;
|
|
49
55
|
statement_timeout_ms?: number;
|
|
50
56
|
max_rows?: number;
|
|
51
57
|
// Fallback path: tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
|
|
@@ -160,7 +166,7 @@ function loadConfig(): Record<string, DataSourceConfig> {
|
|
|
160
166
|
* reorder is essential there too (an author-written PartiQL `WHERE sk = $2 AND pk = $1`).
|
|
161
167
|
*/
|
|
162
168
|
export function rewritePlaceholders(engine: string, sql: string, params: any[]): { sql: string; params: any[] } {
|
|
163
|
-
if (engine !== 'sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
|
|
169
|
+
if (engine !== 'sqlite' && engine !== 'device-sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
|
|
164
170
|
let out = '';
|
|
165
171
|
let inStr = false;
|
|
166
172
|
const newParams: any[] = [];
|
|
@@ -517,10 +523,11 @@ class DataSource {
|
|
|
517
523
|
const { key, conf } = this.cfg(name);
|
|
518
524
|
if (this.engines.has(key)) return { engine: this.engines.get(key)!, conf };
|
|
519
525
|
let engine: Engine;
|
|
520
|
-
if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite') {
|
|
521
|
-
// url is required for the SQL engines (
|
|
522
|
-
//
|
|
523
|
-
|
|
526
|
+
if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite' || conf.engine === 'device-sqlite') {
|
|
527
|
+
// url is required for the HOST-based SQL engines (or a sqlite file path). device-sqlite has
|
|
528
|
+
// no url — the file comes off the device (package + db_path, validated in its exec builder);
|
|
529
|
+
// NoSQL engines without a URL (e.g. DynamoDB region+creds) also supply their own config.
|
|
530
|
+
if (!conf.url && conf.engine !== 'device-sqlite') throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
|
|
524
531
|
engine = new SqlEngine(conf.engine, await this.sqlExec(key, conf));
|
|
525
532
|
} else if (conf.engine === 'cosmos') {
|
|
526
533
|
engine = this.cosmosEngine(key, conf);
|
|
@@ -586,6 +593,62 @@ class DataSource {
|
|
|
586
593
|
}
|
|
587
594
|
};
|
|
588
595
|
}
|
|
596
|
+
// device-sqlite — an ON-DEVICE app database. Every query pulls a FRESH copy through the
|
|
597
|
+
// active Appium session so the oracle always reads CURRENT device state (persistence checks
|
|
598
|
+
// assert AFTER actions). pullFile's `@<package>/<path>` app-container syntax uses run-as
|
|
599
|
+
// under the hood → needs a debuggable build or an emulator. SQLite WAL sidecars (-wal/-shm)
|
|
600
|
+
// are pulled best-effort alongside: Room defaults to WAL, where recent commits live in the
|
|
601
|
+
// -wal file — without it the main file silently misses the newest writes.
|
|
602
|
+
if (conf.engine === 'device-sqlite') {
|
|
603
|
+
if (!conf.package || !conf.db_path) {
|
|
604
|
+
throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs package: and db_path: (path inside the app data dir, e.g. databases/app.db).`);
|
|
605
|
+
}
|
|
606
|
+
const pkg = conf.package;
|
|
607
|
+
const rel = conf.db_path.replace(/^\/+/, '');
|
|
608
|
+
return async (sql, params) => {
|
|
609
|
+
const wd = (globalThis as any).driver;
|
|
610
|
+
if (!wd || typeof wd.pullFile !== 'function') {
|
|
611
|
+
throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs an active Appium session — device @query steps only run inside mobile specs.`);
|
|
612
|
+
}
|
|
613
|
+
const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path');
|
|
614
|
+
const base = path.join(os.tmpdir(), `sungen-devdb-${process.pid}-${Date.now()}`);
|
|
615
|
+
const files: string[] = [];
|
|
616
|
+
const pull = async (suffix: string, required: boolean): Promise<void> => {
|
|
617
|
+
let b64: string | null = null;
|
|
618
|
+
let pullErr: unknown;
|
|
619
|
+
try {
|
|
620
|
+
b64 = await wd.pullFile(`@${pkg}/${rel}${suffix}`);
|
|
621
|
+
} catch (e) {
|
|
622
|
+
pullErr = e;
|
|
623
|
+
// pullFile's container route does `run-as cp → /data/local/tmp`, which newer Android
|
|
624
|
+
// blocks for the app UID. Fallback: stream base64 over run-as via `mobile: shell`
|
|
625
|
+
// (opt-in — APPIUM_ALLOW_ADB_SHELL=1, see the wdio.conf template).
|
|
626
|
+
try {
|
|
627
|
+
const out = await wd.execute('mobile: shell', { command: 'run-as', args: [pkg, 'base64', `${rel}${suffix}`] });
|
|
628
|
+
b64 = String(out).replace(/\s+/g, '');
|
|
629
|
+
} catch (e2) {
|
|
630
|
+
if (required) {
|
|
631
|
+
throw new Error(`Data Driver: could not pull @${pkg}/${rel} from the device. pullFile: ${pullErr} · shell fallback: ${e2} — needs a DEBUGGABLE build; if pullFile's container route fails on this Android version, set APPIUM_ALLOW_ADB_SHELL=1 (.env.appium).`);
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (b64 == null) return;
|
|
637
|
+
fs.writeFileSync(base + suffix, Buffer.from(b64, 'base64'));
|
|
638
|
+
files.push(base + suffix);
|
|
639
|
+
};
|
|
640
|
+
await pull('', true);
|
|
641
|
+
await pull('-wal', false);
|
|
642
|
+
await pull('-shm', false);
|
|
643
|
+
try {
|
|
644
|
+
const Database = require('better-sqlite3');
|
|
645
|
+
const db = new Database(base, { readonly: false }); // WAL recovery needs write access to the TEMP copy
|
|
646
|
+
try { return db.prepare(sql).all(...params); } finally { db.close(); }
|
|
647
|
+
} finally {
|
|
648
|
+
for (const f of files) { try { fs.unlinkSync(f); } catch { /* best-effort */ } }
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
589
652
|
// sqlite
|
|
590
653
|
if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
|
|
591
654
|
const Database = require('better-sqlite3');
|
|
@@ -178,6 +178,61 @@ function resolveLocaleCaps(): Record<string, unknown> {
|
|
|
178
178
|
}
|
|
179
179
|
const localeCaps = resolveLocaleCaps();
|
|
180
180
|
|
|
181
|
+
// ── Connectivity self-heal (#615) ───────────────────────────────────────────
|
|
182
|
+
// The connectivity/location steps mutate DEVICE state (airplane, wifi, data, GPS). A run that
|
|
183
|
+
// aborts mid-toggle — e.g. an Appium session timeout during an airplane round-trip — leaves the
|
|
184
|
+
// device offline as PERSISTENT state, so every later launch fails with the app's network error
|
|
185
|
+
// and the next run dies at cold start. Restore the baseline around the run via host-side adb:
|
|
186
|
+
// it works even when no Appium session survives (a spec-level after() cannot — the session is
|
|
187
|
+
// exactly what died). Android-only (the mutating steps are Android-only), idempotent (on/on/on
|
|
188
|
+
// is the baseline every suite assumes; offline scenarios set their own state mid-test), and
|
|
189
|
+
// best-effort per command (a device lacking one command just skips that line).
|
|
190
|
+
function restoreConnectivity(hook: string): void {
|
|
191
|
+
if (!RUN_ANDROID) return;
|
|
192
|
+
// Appium finds adb via ANDROID_HOME — PATH may not carry it (fresh CI runner), which would
|
|
193
|
+
// make every heal command fail silently. Resolve the same way Appium does, fall back to PATH.
|
|
194
|
+
const adbBin = process.env.ANDROID_HOME ? path.join(process.env.ANDROID_HOME, 'platform-tools', 'adb') : 'adb';
|
|
195
|
+
const adb = `"${adbBin}" -s ${process.env.ANDROID_UDID || '__ANDROID_UDID__'} shell`;
|
|
196
|
+
// stderr ignored: with a detached device the reads would otherwise spray raw adb errors on every run.
|
|
197
|
+
const sh = (cmd: string) => execSync(`${adb} ${cmd}`, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
198
|
+
// Detect-first: read each state and only touch what is actually wedged, so a healthy run
|
|
199
|
+
// costs four quick reads, mutates nothing, and logs nothing. Each fix lists every command
|
|
200
|
+
// variant (emulator `svc`, real-device `cmd`, API<30 fallbacks) — all best-effort, same
|
|
201
|
+
// direction, idempotent, so running the inapplicable ones is harmless.
|
|
202
|
+
let touched = false;
|
|
203
|
+
const heal = (read: string, wedged: string, fixes: string[]) => {
|
|
204
|
+
try {
|
|
205
|
+
if (sh(read) !== wedged) return;
|
|
206
|
+
for (const fix of fixes) { try { sh(fix); } catch { /* variant not on this API — fine */ } }
|
|
207
|
+
touched = true;
|
|
208
|
+
} catch { /* unreadable state (device gone / key absent) — leave untouched */ }
|
|
209
|
+
};
|
|
210
|
+
heal('settings get global airplane_mode_on', '1', [
|
|
211
|
+
'cmd connectivity airplane-mode disable', // API 30+
|
|
212
|
+
'settings put global airplane_mode_on 0', // API<30 wedge (settings+broadcast)
|
|
213
|
+
'am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false',
|
|
214
|
+
]);
|
|
215
|
+
heal('settings get global wifi_on', '0', [
|
|
216
|
+
'svc wifi enable', // emulator flavor
|
|
217
|
+
'cmd -w wifi set-wifi-enabled enabled', // real-device flavor (svc can no-op silently)
|
|
218
|
+
]);
|
|
219
|
+
heal('settings get global mobile_data', '0', [
|
|
220
|
+
'svc data enable',
|
|
221
|
+
'cmd phone data enable',
|
|
222
|
+
]);
|
|
223
|
+
heal('cmd location is-location-enabled', 'false', ['cmd location set-location-enabled true']);
|
|
224
|
+
if (!touched) return;
|
|
225
|
+
console.log(`[sungen] connectivity self-heal (${hook}): device was offline — a previous run likely aborted mid-toggle. Baseline restored (airplane off, wifi/data/location on).`);
|
|
226
|
+
// Radios re-associate asynchronously (emulator wifi can take 10-30s) — wait for REAL IP
|
|
227
|
+
// connectivity before the session starts, or the app's first cold start still lands on its
|
|
228
|
+
// network-error dialog and the auth/first assert fails despite the heal.
|
|
229
|
+
for (let i = 0; i < 10; i++) {
|
|
230
|
+
try { execSync(`${adb} ping -c 1 -W 2 8.8.8.8`, { stdio: 'ignore', timeout: 8000 }); return; } catch { /* not yet */ }
|
|
231
|
+
try { execSync(`${adb} sleep 2`, { stdio: 'ignore', timeout: 8000 }); } catch { /* device gone — stop waiting */ return; }
|
|
232
|
+
}
|
|
233
|
+
console.log('[sungen] connectivity self-heal: no ICMP reply within ~30s — continuing (fine if this network blocks ping; otherwise the device may need a cold boot).');
|
|
234
|
+
}
|
|
235
|
+
|
|
181
236
|
export const config: WebdriverIO.Config = {
|
|
182
237
|
runner: 'local',
|
|
183
238
|
tsConfigPath: './tsconfig.json',
|
|
@@ -192,7 +247,10 @@ export const config: WebdriverIO.Config = {
|
|
|
192
247
|
// Global auto-wait for $/$$ matchers (toBeDisplayed, click, …). Default is 3s, too short for a
|
|
193
248
|
// heavy app's cold (re)launch — each scenario terminates+activates its app, so the first
|
|
194
249
|
// find/click must wait out the splash/first-frame. 20s keeps slow native/Flutter apps green.
|
|
195
|
-
|
|
250
|
+
// 30s (not the classic 20s): on a Compose app with looping in-app animations the accessibility
|
|
251
|
+
// manager can starve for 10s+ per query ("Timed out waiting for the root AccessibilityNodeInfo",
|
|
252
|
+
// observed live) — a 20s budget turns that latency into flaky not-found failures.
|
|
253
|
+
waitforTimeout: 30000,
|
|
196
254
|
|
|
197
255
|
// Capability set(s) selected by MOBILE_PLATFORM (android | ios | both). Each cap carries its OWN
|
|
198
256
|
// `specs` (per-OS routing): the Android cap runs android + mobile specs, the iOS cap runs ios +
|
|
@@ -217,6 +275,14 @@ export const config: WebdriverIO.Config = {
|
|
|
217
275
|
'appium:noReset': true,
|
|
218
276
|
'appium:autoGrantPermissions': true,
|
|
219
277
|
'appium:newCommandTimeout': 300,
|
|
278
|
+
// Compose/animated UIs (a looping promo animation is enough) never report accessibility-idle,
|
|
279
|
+
// so every find first waits out UiAutomator2's full idle timeout — 10000ms by default. 2s keeps
|
|
280
|
+
// interactions responsive while still absorbing screen transitions. NOTE: milliseconds — the
|
|
281
|
+
// same-named XCUITest cap below is in seconds; the two drivers define the knob differently.
|
|
282
|
+
'appium:settings[waitForIdleTimeout]': 2000,
|
|
283
|
+
// Disable SYSTEM animation scales for the session — trims accessibility-tree churn so
|
|
284
|
+
// queries stall less on animation-heavy apps (in-app Compose animations still run).
|
|
285
|
+
'appium:disableWindowAnimation': true,
|
|
220
286
|
// i18n: when SUNGEN_ENV/SUNGEN_LOCALE is a locale code, this overrides noReset→false and
|
|
221
287
|
// adds appium:language/appium:locale so the app boots in that locale. Empty otherwise.
|
|
222
288
|
...localeCaps,
|
|
@@ -275,7 +341,23 @@ export const config: WebdriverIO.Config = {
|
|
|
275
341
|
reporters: ['spec', [PwShapeReporter, {}]],
|
|
276
342
|
|
|
277
343
|
// Auto-start a local Appium server on :4723 (base path '/')
|
|
278
|
-
|
|
344
|
+
// Appium server options — security-gated features, each opt-in via .env.appium:
|
|
345
|
+
// APPIUM_ALLOW_ADB_SHELL=1 → --allow-insecure uiautomator2:adb_shell
|
|
346
|
+
// (raw shell access — device-level oracles like device-sqlite's run-as fallback)
|
|
347
|
+
// APPIUM_CHROMEDRIVER_AUTODOWNLOAD=1 → --allow-insecure uiautomator2:chromedriver_autodownload
|
|
348
|
+
// (hybrid WebView: downloads the chromedriver matching the device's WebView at session start —
|
|
349
|
+
// without it, `switch to [X] frame` fails with "No Chromedriver found that can automate Chrome N")
|
|
350
|
+
// Both stay OFF by default (see docs/spec/security-and-telemetry-roadmap.md).
|
|
351
|
+
services: [
|
|
352
|
+
['appium', (() => {
|
|
353
|
+
const feats: string[] = [];
|
|
354
|
+
if (process.env.APPIUM_ALLOW_ADB_SHELL) feats.push('uiautomator2:adb_shell');
|
|
355
|
+
if (process.env.APPIUM_CHROMEDRIVER_AUTODOWNLOAD) feats.push('uiautomator2:chromedriver_autodownload');
|
|
356
|
+
// NOTE: pass a comma-joined STRING — @wdio/appium-service JSON.stringifies arrays, which the
|
|
357
|
+
// appium server then reads as a JSON literal and the features silently never register.
|
|
358
|
+
return feats.length ? { args: { allowInsecure: feats.join(',') } } : {};
|
|
359
|
+
})()],
|
|
360
|
+
],
|
|
279
361
|
port: 4723,
|
|
280
362
|
path: '/',
|
|
281
363
|
|
|
@@ -291,12 +373,27 @@ export const config: WebdriverIO.Config = {
|
|
|
291
373
|
retries: 1,
|
|
292
374
|
},
|
|
293
375
|
|
|
376
|
+
// expect-webdriverio's default assertion budget (~2s wait / 100ms interval) is tuned for
|
|
377
|
+
// sub-second finds. On an accessibility tree churned by looping in-app animations (a rotating
|
|
378
|
+
// promo carousel is enough) a single server-side find can take 5-15s, so every toBeDisplayed()
|
|
379
|
+
// would expire before its first query even returns — align the budget with waitforTimeout.
|
|
380
|
+
before: async () => {
|
|
381
|
+
const { setOptions } = await import('expect-webdriverio');
|
|
382
|
+
setOptions({ wait: 20000, interval: 500 });
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
// Connectivity self-heal (#615): onPrepare heals a device wedged by a previous KILLED run
|
|
386
|
+
// (its onComplete never fired) before the session starts; onComplete restores after this run,
|
|
387
|
+
// pass or fail. Both run in the launcher process, so they fire even when the worker/session died.
|
|
388
|
+
onPrepare: () => restoreConnectivity('onPrepare'),
|
|
389
|
+
onComplete: () => restoreConnectivity('onComplete'),
|
|
390
|
+
|
|
294
391
|
// iOS + locale: XCUITest's `noReset:false` does NOT actually clear the app's data container
|
|
295
392
|
// (verified live 2026-06-11) — a Flutter app that persisted its language on a previous launch keeps
|
|
296
393
|
// it and ignores the new device locale, so the i18n run asserts the wrong language. Wipe the app
|
|
297
394
|
// data via simctl BEFORE the session so first-launch locale detection re-runs. Android needs no
|
|
298
395
|
// hook (its noReset:false reset works natively).
|
|
299
|
-
beforeSession: async (_config, capabilities) => {
|
|
396
|
+
beforeSession: async (_config, capabilities, specs) => {
|
|
300
397
|
if (Object.keys(localeCaps).length === 0) return; // not a locale run
|
|
301
398
|
const caps = capabilities as Record<string, unknown>;
|
|
302
399
|
if (String(caps['platformName'] || '').toLowerCase() !== 'ios') return;
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Valid selector types used across scaffold generator, selector resolver, and validator.
|
|
3
3
|
* Single source of truth — import this instead of hardcoding.
|
|
4
4
|
*/
|
|
5
|
-
export declare const VALID_SELECTOR_TYPES: readonly ["placeholder", "role", "testid", "label", "text", "page", "column", "locator", "id", "upload", "frame", "table", "accessibility-id", "xpath", "android-uiautomator", "ios-predicate"];
|
|
5
|
+
export declare const VALID_SELECTOR_TYPES: readonly ["placeholder", "role", "testid", "label", "text", "page", "column", "locator", "id", "upload", "frame", "table", "accessibility-id", "xpath", "android-uiautomator", "ios-predicate", "css"];
|
|
6
6
|
export type SelectorType = (typeof VALID_SELECTOR_TYPES)[number];
|
|
7
7
|
//# sourceMappingURL=selector-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector-types.d.ts","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"selector-types.d.ts","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,wMAmBvB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -23,5 +23,6 @@ exports.VALID_SELECTOR_TYPES = [
|
|
|
23
23
|
'xpath', // cross-platform, last resort
|
|
24
24
|
'android-uiautomator', // Android only — UiSelector expressions
|
|
25
25
|
'ios-predicate', // iOS only — NSPredicate strings
|
|
26
|
+
'css', // hybrid WebView only — CSS inside a WEBVIEW context (after `switch to [X] frame`)
|
|
26
27
|
];
|
|
27
28
|
//# sourceMappingURL=selector-types.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector-types.js","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACU,QAAA,oBAAoB,GAAG;IAClC,aAAa;IACb,MAAM;IACN,QAAQ;IACR,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,OAAO;IACP,8EAA8E;IAC9E,kBAAkB,EAAK,sEAAsE;IAC7F,OAAO,EAAgB,8BAA8B;IACrD,qBAAqB,EAAE,wCAAwC;IAC/D,eAAe,EAAQ,iCAAiC;
|
|
1
|
+
{"version":3,"file":"selector-types.js","sourceRoot":"","sources":["../../src/utils/selector-types.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACU,QAAA,oBAAoB,GAAG;IAClC,aAAa;IACb,MAAM;IACN,QAAQ;IACR,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;IACR,SAAS;IACT,IAAI;IACJ,QAAQ;IACR,OAAO;IACP,OAAO;IACP,8EAA8E;IAC9E,kBAAkB,EAAK,sEAAsE;IAC7F,OAAO,EAAgB,8BAA8B;IACrD,qBAAqB,EAAE,wCAAwC;IAC/D,eAAe,EAAQ,iCAAiC;IACxD,KAAK,EAAkB,mFAAmF;CAClG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.24
|
|
3
|
+
"version": "3.2.24",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-data-factory": "3.2.24
|
|
43
|
-
"@sungen/driver-ui": "3.2.24
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.24",
|
|
43
|
+
"@sungen/driver-ui": "3.2.24",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/biometric-action.hbs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Simulated biometric input — EMULATOR/SIMULATOR only (a real device needs a real finger; the
|
|
2
|
+
// command is rejected there and the step fails loud). Android: `mobile: fingerprint` — finger 1
|
|
3
|
+
// must be ENROLLED on the emulator (Settings → Security); an unenrolled id gives the no-match
|
|
4
|
+
// path. iOS Simulator: `mobile: sendBiometricMatch` (Features → Touch/Face ID enrolled).
|
|
5
|
+
if (driver.isIOS) {
|
|
6
|
+
await driver.execute('mobile: sendBiometricMatch', { type: '{{#if faceId}}faceId{{else}}touchId{{/if}}', match: {{#if match}}true{{else}}false{{/if}} });
|
|
7
|
+
} else {
|
|
8
|
+
await driver.execute('mobile: fingerprint', { fingerprintId: {{#if match}}1{{else}}9{{/if}} });
|
|
9
|
+
}
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{{!-- WebdriverIO/Appium has no .check(). Ensure the toggle ends up ON: read the current state and
|
|
2
2
|
tap only if it's not already on, so the step is idempotent (re-running a passing test, or
|
|
3
3
|
running under `noReset` with unknown persisted state, won't flip it back off). State is read
|
|
4
|
-
the same way the checked/unchecked assertions read it —
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
the same way the checked/unchecked assertions read it — through the __assertCheckedState
|
|
5
|
+
oracle's __probeCheckedSignals (test-file preamble), which weighs `checked`/`value`, `selected`
|
|
6
|
+
and isSelected() on the node and its descendants, because no single one of them is reliable on
|
|
7
|
+
a custom-rendered control. The tap itself uses
|
|
7
8
|
the trailing-edge coordinate method (not a centre .click()): on a full-width composite row the
|
|
8
9
|
a11y node merges the label with the switch, so a centre tap lands on the label and doesn't
|
|
9
10
|
flip the control. --}}
|
|
@@ -12,9 +13,13 @@ await (async () => {
|
|
|
12
13
|
// Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
|
|
13
14
|
// otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
|
|
14
15
|
if (!__cb.elementId) throw new Error((__cb as any).error?.message || `Element not found: ${__cb.selector}`);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Read the CURRENT state through the same oracle the checked assertions use. The old read here
|
|
17
|
+
// was `getAttribute('checked') === 'true'` with an isSelected() fallback in a `catch` — but on
|
|
18
|
+
// Android that read never throws (UiAutomator2 defaults `checked` to "false" on every node), so
|
|
19
|
+
// the fallback never fired and a custom-rendered toggle already ON read as OFF. This step then
|
|
20
|
+
// tapped it and turned it OFF — the idempotence guard silently inverted.
|
|
21
|
+
const __signals = await __probeCheckedSignals(__cb);
|
|
22
|
+
const __on = __signals.some(s => s.value);
|
|
18
23
|
if (__on) return;
|
|
19
24
|
|
|
20
25
|
const r = await driver.getElementRect(__cb.elementId);
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/deep-link-action.hbs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Open a deep link into the app — VIEW intent on Android, XCUITest deepLink on iOS (Simulator).
|
|
2
|
+
// THE entry point for push-param / universal-link routing scenarios.
|
|
3
|
+
await (async () => {
|
|
4
|
+
{{#if dataRef}}const __url = String(testData.get('{{dataRef}}'));{{else}}const __url = '{{escapeQuotes url}}';{{/if}}
|
|
5
|
+
const __pkg = __resolveAppId();
|
|
6
|
+
await driver.execute('mobile: deepLink', driver.isIOS ? { url: __url, bundleId: __pkg } : { url: __url, package: __pkg });
|
|
7
|
+
await driver.pause(1500); // let the routed screen mount before the next step asserts on it
|
|
8
|
+
})();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Android-only: XCUITest cannot toggle Location Services — fail loud, never silently skip.
|
|
2
|
+
if (driver.isIOS) {
|
|
3
|
+
throw new Error('turn device location {{#if on}}on{{else}}off{{/if}}: iOS/XCUITest cannot toggle Location Services — run this scenario on Android or tag it @platform:android');
|
|
4
|
+
}
|
|
5
|
+
// `mobile: toggleGps` flips blindly, so read `mobile: isGpsEnabled` first and only toggle when the
|
|
6
|
+
// state differs (idempotent). NOTE: the execute-method aliases, not the legacy JSONWP wrappers —
|
|
7
|
+
// WDIO v9 removed driver.isLocationServicesEnabled()/toggleLocationServices().
|
|
8
|
+
await (async () => {
|
|
9
|
+
const __locOn = await driver.execute('mobile: isGpsEnabled');
|
|
10
|
+
if (__locOn !== {{#if on}}true{{else}}false{{/if}}) { await driver.execute('mobile: toggleGps'); }
|
|
11
|
+
})();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Reinstall the app: remove → install from the session's build file → launch → settle.
|
|
2
|
+
// Persistence oracles (dismissals, pins) hinge on what survives a reinstall.
|
|
3
|
+
await (async () => {
|
|
4
|
+
const __pkg = __resolveAppId();
|
|
5
|
+
const __caps = (driver.requestedCapabilities || {}) as Record<string, string>;
|
|
6
|
+
const __appPath = __caps['appium:app'] || __caps['app'];
|
|
7
|
+
if (!__appPath) {
|
|
8
|
+
throw new Error('reinstall the app: the session must install from a build file — set APP_APK (Android) / IOS_APP (iOS) so the appium:app capability is present.');
|
|
9
|
+
}
|
|
10
|
+
try { await driver.removeApp(__pkg); } catch { /* not installed */ }
|
|
11
|
+
await driver.installApp(__appPath);
|
|
12
|
+
await driver.activateApp(__pkg);
|
|
13
|
+
let __prevHad = false;
|
|
14
|
+
await driver
|
|
15
|
+
.waitUntil(async () => {
|
|
16
|
+
const src = await driver.getPageSource();
|
|
17
|
+
const has = /content-desc="[^"]+"|\btext="[^"]+"/.test(src);
|
|
18
|
+
const ok = has && __prevHad; __prevHad = has; return ok;
|
|
19
|
+
}, { timeout: 45000, interval: 400 })
|
|
20
|
+
.catch(() => { /* best-effort — first launch after install can be the slowest */ });
|
|
21
|
+
})();
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Restart the app: kill → relaunch → wait for rendered content (same settle oracle as the
|
|
2
|
+
// per-scenario relaunch hook — a cold start first paints a contentless skeleton).
|
|
3
|
+
await (async () => {
|
|
4
|
+
const __pkg = __resolveAppId();
|
|
5
|
+
try { await driver.terminateApp(__pkg); } catch { /* not running */ }
|
|
6
|
+
await driver.pause(500);
|
|
7
|
+
await driver.activateApp(__pkg);
|
|
8
|
+
let __prevHad = false;
|
|
9
|
+
await driver
|
|
10
|
+
.waitUntil(async () => {
|
|
11
|
+
const src = await driver.getPageSource();
|
|
12
|
+
const has = /content-desc="[^"]+"|\btext="[^"]+"/.test(src);
|
|
13
|
+
const ok = has && __prevHad; __prevHad = has; return ok;
|
|
14
|
+
}, { timeout: 30000, interval: 400 })
|
|
15
|
+
.catch(() => { /* best-effort — the next step has its own wait */ });
|
|
16
|
+
})();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Android-only: XCUITest exposes no connectivity control — fail loud, never silently skip.
|
|
2
|
+
if (driver.isIOS) {
|
|
3
|
+
throw new Error('turn {{kind}} {{#if on}}on{{else}}off{{/if}}: iOS/XCUITest cannot toggle device connectivity — run this scenario on Android or tag it @platform:android');
|
|
4
|
+
}
|
|
5
|
+
{{#if isAirplane}}
|
|
6
|
+
// Airplane {{#if on}}ON kills wifi+data{{else}}OFF restores wifi+data{{/if}} — set all three explicitly.
|
|
7
|
+
await driver.execute('mobile: setConnectivity', { wifi: {{#if on}}false{{else}}true{{/if}}, data: {{#if on}}false{{else}}true{{/if}}, airplaneMode: {{#if on}}true{{else}}false{{/if}} });
|
|
8
|
+
{{else}}
|
|
9
|
+
await driver.execute('mobile: setConnectivity', { {{kindKey}}: {{#if on}}true{{else}}false{{/if}} });
|
|
10
|
+
{{/if}}
|
|
11
|
+
await driver.pause(1500); // let the radio state settle before the next step acts on the UI
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{{!-- WebdriverIO/Appium has no .uncheck(). Ensure the toggle ends up OFF: tap only if currently on,
|
|
2
2
|
so the step is idempotent under repeat runs / persisted `noReset` state. State read + tap
|
|
3
|
-
method mirror check-action.hbs (see there for why):
|
|
4
|
-
|
|
3
|
+
method mirror check-action.hbs (see there for why): state via __probeCheckedSignals (the
|
|
4
|
+
multi-signal oracle), then a trailing-edge coordinate tap rather
|
|
5
5
|
than a centre .click() (a centre tap on a full-width composite row hits the label, not the
|
|
6
6
|
switch). --}}
|
|
7
7
|
await (async () => {
|
|
@@ -9,9 +9,13 @@ await (async () => {
|
|
|
9
9
|
// Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
|
|
10
10
|
// otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
|
|
11
11
|
if (!__cb.elementId) throw new Error((__cb as any).error?.message || `Element not found: ${__cb.selector}`);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Read the CURRENT state through the same oracle the checked assertions use. The old read here
|
|
13
|
+
// was `getAttribute('checked') === 'true'` with an isSelected() fallback in a `catch` — but on
|
|
14
|
+
// Android that read never throws (UiAutomator2 defaults `checked` to "false" on every node), so
|
|
15
|
+
// the fallback never fired and a custom-rendered toggle already ON read as OFF. This step then
|
|
16
|
+
// tapped it and turned it ON — the idempotence guard silently inverted.
|
|
17
|
+
const __signals = await __probeCheckedSignals(__cb);
|
|
18
|
+
const __on = __signals.some(s => s.value);
|
|
15
19
|
if (!__on) return;
|
|
16
20
|
|
|
17
21
|
const r = await driver.getElementRect(__cb.elementId);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
{{!-- Checkbox/toggle state via the __assertCheckedState oracle (test-file preamble), NOT a bare
|
|
2
|
+
toHaveAttribute. A single `checked`(Android)/`value`(iOS) read cannot be trusted on its own:
|
|
3
|
+
UiAutomator2 defaults `checked` to "false" on EVERY node, so a custom-rendered control that
|
|
4
|
+
publishes no checked semantics (Flutter without Semantics(checked:), RN without
|
|
5
|
+
accessibilityState) reports a confident false while sitting visibly ON. The oracle probes
|
|
6
|
+
`checked`/`value`, `selected` and isSelected() on the node AND on its descendants (on a
|
|
7
|
+
composite row the state routinely sits on an inner node), and passes on ANY positive signal.
|
|
8
|
+
`selected` alone is no substitute either — toBeSelected() is always false for a Flutter
|
|
9
|
+
Switch, which is why this template stopped using it. --}}
|
|
10
|
+
await __assertCheckedState(await ({{> locator}}), true, '{{escapeQuotes selectorRef}}');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{!-- Device-log oracle (Android: logcat · iOS: syslog) — makes SILENT failure modes and analytics
|
|
2
|
+
events observable. getLogs returns entries SINCE THE LAST CALL (incremental buffer), so
|
|
3
|
+
poll-accumulate up to 10s before failing loud with the log type in the message. --}}
|
|
4
|
+
await (async () => {
|
|
5
|
+
{{#if dataRef}}const __needle = String(testData.get('{{dataRef}}'));{{else}}const __needle = '{{escapeQuotes value}}';{{/if}}
|
|
6
|
+
const __type = driver.isIOS ? 'syslog' : 'logcat';
|
|
7
|
+
let __buf = '';
|
|
8
|
+
const __t0 = Date.now();
|
|
9
|
+
while (Date.now() - __t0 < 10000) {
|
|
10
|
+
const __entries = (await driver.getLogs(__type)) as Array<{ message?: string } | string>;
|
|
11
|
+
__buf += __entries.map((e) => (typeof e === 'string' ? e : (e && e.message) || '')).join('\n') + '\n';
|
|
12
|
+
if (__buf.includes(__needle)) return;
|
|
13
|
+
await driver.pause(500);
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`device log (${__type}) did not contain ${JSON.stringify(__needle)} within 10s`);
|
|
16
|
+
})();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{{!-- The element must vanish within the window (auto-dismiss / expiry oracle). Plain $() on
|
|
2
|
+
purpose — never __ensureVisible: scroll-hunting for something that should disappear would
|
|
3
|
+
drag the screen away. A 2s grace absorbs accessibility-tree churn latency without hiding a
|
|
4
|
+
genuinely stuck element. --}}
|
|
5
|
+
await $({{> appium-selector-expr}}).waitForDisplayed({ reverse: true, timeout: {{seconds}} * 1000 + 2000, timeoutMsg: '{{selectorRef}} still visible after the {{seconds}}s disappear window' });
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
{{!-- The unchecked twin of checked-assertion — same __assertCheckedState oracle, expected=false.
|
|
2
|
+
Deliberately NOT symmetric with the checked direction: "unchecked" requires that NO probed
|
|
3
|
+
signal anywhere (node or descendant, `checked`/`value`/`selected`/isSelected()) reads
|
|
4
|
+
positive, so a visibly-ON custom-rendered control cannot slip through as a false PASS the way
|
|
5
|
+
a single Android `checked="false"` read would let it. --}}
|
|
6
|
+
await __assertCheckedState(await ({{> locator}}), false, '{{escapeQuotes selectorRef}}');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{{!-- State-with-filter (mobile): $$ over the base selector, per-OS text read (hand-rolled — no
|
|
2
2
|
CSS filter() combinator on Appium) matched against dataValue, then assert the state on the
|
|
3
|
-
match. checked/selected
|
|
3
|
+
match. checked/selected go through the __assertCheckedState oracle (mirrors checked-assertion) —
|
|
4
|
+
a single `checked`/`value` read is unreliable on a custom-rendered control;
|
|
4
5
|
empty is a text read; enabled/focused use direct WDIO matchers. A state with no
|
|
5
6
|
native-mobile analog (sorted*, aria-busy loading) fails loud, matching sorted-assertion's
|
|
6
7
|
house style. --}}
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
if (String(__s.find(s => s && String(s).trim() !== '') ?? '').trim() === __expected) { __el = __c; break; }
|
|
14
15
|
}
|
|
15
16
|
if (!__el) throw new Error(`[sungen] No match with text "${__expected}" to assert state on`);
|
|
16
|
-
{{#if (or selectedProbe (eq stateAssertion "toBeChecked()"))}}await
|
|
17
|
-
{{else if (eq stateAssertion "toBeChecked({ checked: false })")}}await
|
|
17
|
+
{{#if (or selectedProbe (eq stateAssertion "toBeChecked()"))}}await __assertCheckedState(__el, true, `item with "${__expected}"`);
|
|
18
|
+
{{else if (eq stateAssertion "toBeChecked({ checked: false })")}}await __assertCheckedState(__el, false, `item with "${__expected}"`);
|
|
18
19
|
{{else if (eq stateAssertion "toBeEnabled()")}}await expect(__el).toBeEnabled();
|
|
19
20
|
{{else if (eq stateAssertion "toBeFocused()")}}await expect(__el).toBeFocused();
|
|
20
21
|
{{else if (eq stateAssertion "toBeEmpty()")}}await expect(__el).toHaveText('');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{{!-- The element must SURVIVE the window (e.g. a barcode valid for 60s, a banner that must not
|
|
2
|
+
auto-dismiss). Wait out the window first, then run the platform-aware visibility oracle. --}}
|
|
3
|
+
await driver.pause({{seconds}} * 1000);
|
|
4
|
+
await __assertVisible({{> appium-selector-expr}});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
await (async () => { const e = await {{> locator}};
|
|
2
|
+
// Fail loud on a missing target — an undefined elementId otherwise surfaces as the opaque
|
|
3
|
+
// server error "The swipe area coordinates must be provided if element is not set".
|
|
4
|
+
if (!e.elementId) { throw new Error('pull-to-refresh target not found: ' + {{> appium-selector-expr}}); }
|
|
2
5
|
// Pull-to-refresh = drag the content down. Platform-native swipe per OS.
|
|
3
6
|
if (driver.isIOS) { await driver.execute('mobile: swipe', { elementId: e.elementId, direction: 'down' }); }
|
|
4
7
|
else { await driver.execute('mobile: swipeGesture', { elementId: e.elementId, direction: 'down', percent: 0.9, speed: 5000 }); }
|
package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
await (async () => { const e = await {{> locator}};
|
|
2
|
+
// Fail loud on a missing target — an undefined elementId otherwise surfaces as the opaque
|
|
3
|
+
// server error "The swipe area coordinates must be provided if element is not set".
|
|
4
|
+
if (!e.elementId) { throw new Error('swipe target not found: ' + {{> appium-selector-expr}}); }
|
|
2
5
|
// Platform-native swipe: XCUITest `mobile: swipe` (direction only) vs UiAutomator2 `mobile: swipeGesture`.
|
|
3
6
|
if (driver.isIOS) { await driver.execute('mobile: swipe', { elementId: e.elementId, direction: '{{direction}}' }); }
|
|
4
7
|
else { await driver.execute('mobile: swipeGesture', { elementId: e.elementId, direction: '{{direction}}', percent: 0.75 }); }
|
package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
{{~#case 'xpath'}}{{escapeQuotes value}}{{/case~}}
|
|
9
9
|
{{~#case 'android-uiautomator'}}android={{escapeQuotes value}}{{/case~}}
|
|
10
10
|
{{~#case 'ios-predicate'}}-ios predicate string:{{escapeQuotes value}}{{/case~}}
|
|
11
|
-
{{
|
|
11
|
+
{{~!--
|
|
12
|
+
'id' — Appium's id strategy prepends `<appPackage>:id/` to any value WITHOUT a colon, so a bare
|
|
13
|
+
Compose testTagsAsResourceId value ("bottom_bar") silently never matches. Bare values therefore
|
|
14
|
+
compile to an exact UiSelector.resourceId match (Android); package-qualified classic View ids
|
|
15
|
+
("com.foo:id/btn") keep the plain id strategy, which passes them through unchanged.
|
|
16
|
+
--~}}
|
|
17
|
+
{{~#case 'id'}}{{#if (includes value ':')}}id={{escapeQuotes value}}{{else}}android=new UiSelector().resourceId("{{escapeQuotes value}}"){{/if}}{{/case~}}
|
|
12
18
|
{{~#case 'locator'}}{{escapeQuotes value}}{{/case~}}
|
|
13
19
|
{{~#case 'css'}}{{escapeQuotes value}}{{/case~}}
|
|
14
20
|
{{~#default}}~{{escapeQuotes value}}{{/default~}}
|