@keepsnappy/appium-driver 0.0.0-stage → 0.1.0-alpha.11
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/README.md +37 -2
- package/dist/src/browserstack.d.ts +24 -0
- package/dist/src/browserstack.js +54 -0
- package/dist/src/browserstack.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +8 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/journey.d.ts +43 -0
- package/dist/src/journey.js +2 -0
- package/dist/src/journey.js.map +1 -0
- package/dist/src/links.d.ts +35 -0
- package/dist/src/links.js +87 -0
- package/dist/src/links.js.map +1 -0
- package/dist/src/load.d.ts +4 -0
- package/dist/src/load.js +23 -0
- package/dist/src/load.js.map +1 -0
- package/dist/src/platform.d.ts +63 -0
- package/dist/src/platform.js +145 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/run.d.ts +92 -0
- package/dist/src/run.js +157 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/selectors.d.ts +25 -0
- package/dist/src/selectors.js +57 -0
- package/dist/src/selectors.js.map +1 -0
- package/dist/src/session.d.ts +156 -0
- package/dist/src/session.js +449 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/snapshot.d.ts +64 -0
- package/dist/src/snapshot.js +197 -0
- package/dist/src/snapshot.js.map +1 -0
- package/dist/src/tokens.d.ts +14 -0
- package/dist/src/tokens.js +28 -0
- package/dist/src/tokens.js.map +1 -0
- package/dist/src/version.d.ts +14 -0
- package/dist/src/version.js +69 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/webdriver.d.ts +83 -0
- package/dist/src/webdriver.js +233 -0
- package/dist/src/webdriver.js.map +1 -0
- package/package.json +37 -4
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @keepsnappy/appium-driver
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
KeepSnappy's built-in Appium driver for `perflab farm run --driver appium --journey-module <file>` (BrowserStack App Automate, iOS and Android; `--platform ios|android`, default ios). A thin step vocabulary over W3C WebDriver: you write the journey, the driver owns the session, run tokens, step markers, the readiness wait (`expectNext`) after a touch and the result files. Design: `docs/plans/54b-appium-driver.md`.
|
|
4
|
+
|
|
5
|
+
A journey module default-exports `defineJourney({ app, home, options?, setup?, journeys })`; see the plan for the full shape. A journey needs no import from this package: selectors are plain objects (`{ id }`, `{ text }`, `{ ...sel, childOf }`, or a bare string) and everything else is on the `t` it is given (`t.acceptAlert`, `t.openLink`, ...). `defineJourney` is only an identity helper for editor types. The CLI runs its own bundled copy, so do not `instanceof` driver errors (`WdError`) from your own copy; test `e.wdError`.
|
|
6
|
+
|
|
7
|
+
Run directly (what `farm run` does): `node dist/src/cli.js --journey-module j.mjs --app bs://… --tokens-js tokens.js --journeys a,b --repeat 3 --out DIR [--local-identifier ID]`, with `BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY`. Exit 0, or 1 when the session or an iteration failed (the result files are written either way).
|
|
8
|
+
|
|
9
|
+
## Step checks and Android
|
|
10
|
+
|
|
11
|
+
`t.waitAll([{ shown: sel }, { gone: sel }, { keyboard: 'up' }, ...])` waits until all conditions hold in one poll (the poll asks them one lookup after another). On Android the ids are asked in one UiAutomator2 lookup (a walk of the whole tree costs the same for one id and for ten); elsewhere each condition is waited for in turn. The keyboard condition is answered by the platform (Android `mobile: isKeyboardShown`, iOS the Return key), so a journey needs no platform branch. `t.swipeFromUntilVisible(from, direction, target, times)` swipes until a target shows.
|
|
12
|
+
|
|
13
|
+
Android specifics (UiAutomator2, measured on a Galaxy S23): a lookup that finds nothing walks the whole tree (about 1.2 s), one that finds a shown element takes 0.1 to 0.2 s, so `findShown` asks for one element first. `ignoreUnimportantViews` is on (about 30 % faster lookups).
|
|
14
|
+
|
|
15
|
+
Inverted bounds: when the keyboard opens, TapIn translates the chat up while Android still cuts the chat nodes by the container's untranslated rect. The chat's scroll container and its nodes come back with top below bottom and `displayed=false`, and are left out of lookups. While the keyboard is up (and at most every 3 s) `waitAll` reads the page source with `allowInvisibleElements` and counts such a node as shown when it sits in an inverted scroll container, its real bottom edge is on screen and below the edge the clipped-away nodes pile up on (at least two clipped messages are needed to find the edge). See `src/snapshot.ts`.
|
|
16
|
+
|
|
17
|
+
How to write journeys that run fast on Android, with measured costs: `docs/writing-fast-journeys.md`.
|
|
18
|
+
|
|
19
|
+
## GC before the action (`--action-gc on|off`, default off)
|
|
20
|
+
|
|
21
|
+
With `--action-gc on` the driver asks the app for a full garbage collection before every declared step, after the step's readiness wait and before its start marker, and once more before the run's flush. Design: `docs/plans/gc-before-action.md`; device checks: `docs/plans/gc-before-action-measurement.md`.
|
|
22
|
+
|
|
23
|
+
- Each command has a number `n`, unique in the session. Android sends it as a broadcast (`mobile: broadcast`, action `<bundleId>.perflab.COMMAND`, extras `cmd=gc`, `n`) and reads the answer `gc:<n> ...` from the broadcast's result. iOS opens `<scheme>devtools/perf-lab?cmd=gc&n=<n>` and waits up to 10 s for the element `perflab.ack` whose label starts with `gc:<n> `.
|
|
24
|
+
- The step's start marker carries `gc_n`, so the backend (derivation 18) pairs the step with its GC record without mapping clocks.
|
|
25
|
+
- No answer turns the command off for the rest of the session (`actionGc: unsupported` in `driver-log.json`); the test still passes. Safari link mode sends none (`safari`).
|
|
26
|
+
- The GC command ends the app's open windows: runs with and without it are a different instrument (`actionGc` aspect), never compared as an app change.
|
|
27
|
+
|
|
28
|
+
## GC at the run end (`--end-gc on|off`, default on)
|
|
29
|
+
|
|
30
|
+
One GC command at the end of every run, after the journey's last step and its readiness wait, before the markers and the flush, also when `--action-gc` is off. Its record gives the end reading after a GC (derivation 19: `mem.end_after_gc_mb`, `mem.retained_growth_mb`, `mem.js_heap_end_mb`; setup fact `sdk.end_gc`). End the journey on its home screen (`docs/writing-fast-journeys.md`), so the reading holds what the journey kept, not an open heavy screen; `sdk.journey_ends_home` = 0 flags a run that did not.
|
|
31
|
+
|
|
32
|
+
- Same command and fallbacks as above; an app that does not answer turns it off for the rest of the session.
|
|
33
|
+
- The command ends the last open window (end reason `gc-command`) instead of the flush: that one window per run is shorter than on a driver without it. Driver 0.2.0 carries both commands, so runs before and after it are a different instrument already (`driverVersion` aspect).
|
|
34
|
+
- On by default (REQ-MEAS-35, REQ-FARM-28; driver 0.3.0); `--end-gc off` or `endGc: false` turns it off. It changes the memory readings only (it runs after the last window): a main without it still judges a PR's render, commit and component measures; only the memory verdict needs end-GC runs on both sides (`endGc` setup aspect, memory measures only). Older SDKs do not answer and send no line.
|
|
35
|
+
|
|
36
|
+
## The default run configuration (`links.ts`)
|
|
37
|
+
|
|
38
|
+
`RUN_DEFAULTS` and the link builders are the one source of the activation, flush and GC links (REQ-FARM-28): `profile=1`, `diag=1` (the React export), ttl 3600, no `cpuprof`, and the end GC on (`END_GC_DEFAULT`). A journey module's `sdkArgs` (`k=v&...`) are set over the defaults key by key, so `sdkArgs: 'profile=1'` still gets `diag=1`; `diag=0` turns the export off explicitly. `perflab activation-link | flush-link | gc-link` print the same links for runners that are not this driver, and `perflab farm open` writes the settings into `tokens.js` (`output.perflabLinkArgs`) for Maestro flows. Maestro runs send no GC for now. A guard test in `apps/agent` fails on a hand-written activation link anywhere else in the repo.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Platform } from './platform.js';
|
|
2
|
+
import type { FetchLike } from './webdriver.js';
|
|
3
|
+
export declare const HUB = "https://hub-cloud.browserstack.com/wd/hub";
|
|
4
|
+
export declare const BS_API = "https://api-cloud.browserstack.com/app-automate";
|
|
5
|
+
export declare const basicAuth: (user: string, key: string) => string;
|
|
6
|
+
/** `bs://` passes through; a file is uploaded, or the recent upload under `customId` is reused. */
|
|
7
|
+
export declare function resolveApp(app: string, customId: string, auth: string, fetchImpl: FetchLike, platform?: Platform): Promise<string>;
|
|
8
|
+
export interface CapsInput {
|
|
9
|
+
appUrl: string;
|
|
10
|
+
device: string;
|
|
11
|
+
buildName: string;
|
|
12
|
+
sessionName: string;
|
|
13
|
+
appiumVersion: string;
|
|
14
|
+
localIdentifier?: string | undefined;
|
|
15
|
+
/** Default iOS. */
|
|
16
|
+
platform?: Platform;
|
|
17
|
+
}
|
|
18
|
+
/** `local` is on only when a tunnel exists: without one BrowserStack waits for a Local binary that never comes. */
|
|
19
|
+
export declare function sessionCaps(i: CapsInput): Record<string, unknown>;
|
|
20
|
+
export declare function sessionDetail(sessionId: string, auth: string, fetchImpl: FetchLike): Promise<{
|
|
21
|
+
build_hashed_id?: string;
|
|
22
|
+
public_url?: string;
|
|
23
|
+
browser_url?: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { openAsBlob } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { IOS } from './platform.js';
|
|
4
|
+
// BrowserStack App Automate only: real devices, so no simulator-only capability belongs in the caps.
|
|
5
|
+
export const HUB = 'https://hub-cloud.browserstack.com/wd/hub';
|
|
6
|
+
export const BS_API = 'https://api-cloud.browserstack.com/app-automate';
|
|
7
|
+
export const basicAuth = (user, key) => `Basic ${Buffer.from(`${user}:${key}`).toString('base64')}`;
|
|
8
|
+
/** `bs://` passes through; a file is uploaded, or the recent upload under `customId` is reused. */
|
|
9
|
+
export async function resolveApp(app, customId, auth, fetchImpl, platform = IOS) {
|
|
10
|
+
if (app.startsWith('bs://'))
|
|
11
|
+
return app;
|
|
12
|
+
const headers = { Authorization: auth };
|
|
13
|
+
if (customId) {
|
|
14
|
+
const res = await fetchImpl(`${BS_API}/recent_apps/${encodeURIComponent(customId)}`, { headers });
|
|
15
|
+
const list = res.ok ? await res.json().catch(() => []) : [];
|
|
16
|
+
const url = Array.isArray(list) ? list[0]?.app_url : undefined;
|
|
17
|
+
if (url)
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
const form = new FormData();
|
|
21
|
+
form.append('file', await openAsBlob(app), basename(app));
|
|
22
|
+
if (customId)
|
|
23
|
+
form.append('custom_id', customId);
|
|
24
|
+
for (const [k, v] of Object.entries(platform.uploadFields))
|
|
25
|
+
form.append(k, v);
|
|
26
|
+
const res = await fetchImpl(`${BS_API}/upload`, { method: 'POST', headers, body: form });
|
|
27
|
+
const body = (await res.json().catch(() => ({})));
|
|
28
|
+
if (!body.app_url)
|
|
29
|
+
throw new Error(`app upload failed: HTTP ${res.status} ${JSON.stringify(body)}`);
|
|
30
|
+
return body.app_url;
|
|
31
|
+
}
|
|
32
|
+
/** `local` is on only when a tunnel exists: without one BrowserStack waits for a Local binary that never comes. */
|
|
33
|
+
export function sessionCaps(i) {
|
|
34
|
+
const tunnel = i.localIdentifier ? { local: true, localIdentifier: i.localIdentifier } : {};
|
|
35
|
+
return {
|
|
36
|
+
...(i.platform ?? IOS).caps(i.device),
|
|
37
|
+
'appium:app': i.appUrl,
|
|
38
|
+
'bstack:options': {
|
|
39
|
+
projectName: 'perflab',
|
|
40
|
+
buildName: i.buildName,
|
|
41
|
+
sessionName: i.sessionName,
|
|
42
|
+
...tunnel,
|
|
43
|
+
idleTimeout: 300,
|
|
44
|
+
appiumVersion: i.appiumVersion,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function sessionDetail(sessionId, auth, fetchImpl) {
|
|
49
|
+
const res = await fetchImpl(`${BS_API}/sessions/${sessionId}.json`, { headers: { Authorization: auth } }).catch(() => null);
|
|
50
|
+
if (!res?.ok)
|
|
51
|
+
return {};
|
|
52
|
+
return (await res.json()).automation_session ?? {};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=browserstack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserstack.js","sourceRoot":"","sources":["../../src/browserstack.ts"],"names":[],"mappings":"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"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from './load.js';
|
|
3
|
+
// No "is this the main module" guard: it compared two paths and silently exited 0 through a symlink (macOS /tmp).
|
|
4
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
5
|
+
console.error(`::error::${e instanceof Error ? e.message : String(e)}`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,kHAAkH;AAClH,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC9B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,CAAU,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Driver, WdError, realSleep, type CommandLogEntry, type FetchLike, type Rect } from './webdriver.js';
|
|
2
|
+
export { END_GC_DEFAULT, RUN_DEFAULTS, acceptAlert, activationLink, flushLink, gcLink, linkArgs, openLink, parseSdkArgs, type LinkState } from './links.js';
|
|
3
|
+
export { defineJourney, type JourneyContext, type JourneyFn, type JourneyModule } from './journey.js';
|
|
4
|
+
export { ANDROID, IOS, PLATFORMS, platformOf, type Platform, type PlatformName } from './platform.js';
|
|
5
|
+
export { androidLocators, childOf, describeSelector, id, idText, locator, text, toSelector, type Selector, type SelectorLike } from './selectors.js';
|
|
6
|
+
export { FLUSH_WAIT_MS, HOST_CLOCK_TRIPS, Session, sendMarkers, type ActionGc, type ClockTrip, type Condition, type Marker } from './session.js';
|
|
7
|
+
export { loadElements, parseTokensJs, plan, type PlannedRun } from './tokens.js';
|
|
8
|
+
export { runDriver, DRIVER_OPTIONS, type IterationRecord, type RunDeps } from './run.js';
|
|
9
|
+
export { loadJourneyModule, main } from './load.js';
|
|
10
|
+
export { driverVersion, journeyVersion } from './version.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Driver, WdError, realSleep } from './webdriver.js';
|
|
2
|
+
export { END_GC_DEFAULT, RUN_DEFAULTS, acceptAlert, activationLink, flushLink, gcLink, linkArgs, openLink, parseSdkArgs } from './links.js';
|
|
3
|
+
export { defineJourney } from './journey.js';
|
|
4
|
+
export { ANDROID, IOS, PLATFORMS, platformOf } from './platform.js';
|
|
5
|
+
export { androidLocators, childOf, describeSelector, id, idText, locator, text, toSelector } from './selectors.js';
|
|
6
|
+
export { FLUSH_WAIT_MS, HOST_CLOCK_TRIPS, Session, sendMarkers } from './session.js';
|
|
7
|
+
export { loadElements, parseTokensJs, plan } from './tokens.js';
|
|
8
|
+
export { runDriver, DRIVER_OPTIONS } from './run.js';
|
|
9
|
+
export { loadJourneyModule, main } from './load.js';
|
|
10
|
+
export { driverVersion, journeyVersion } from './version.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAmD,MAAM,gBAAgB,CAAC;AAC7G,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAkB,MAAM,YAAY,CAAC;AAC5J,OAAO,EAAE,aAAa,EAA2D,MAAM,cAAc,CAAC;AACtG,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAoC,MAAM,eAAe,CAAC;AACtG,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAoC,MAAM,gBAAgB,CAAC;AACrJ,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,EAA8D,MAAM,cAAc,CAAC;AACjJ,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,EAAmB,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAsC,MAAM,UAAU,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Session } from './session.js';
|
|
2
|
+
import type { SelectorLike } from './selectors.js';
|
|
3
|
+
import type { FetchLike } from './webdriver.js';
|
|
4
|
+
export interface JourneyContext {
|
|
5
|
+
fetch: FetchLike;
|
|
6
|
+
/** Run tokens from `farm open`: journey name to one token per iteration. */
|
|
7
|
+
tokens: Record<string, string[]>;
|
|
8
|
+
/** The journey names of this session, in run order. */
|
|
9
|
+
journeys: readonly string[];
|
|
10
|
+
repeat: number;
|
|
11
|
+
/** Parsed command line: the driver's own flags plus the module's `options`. */
|
|
12
|
+
args: Record<string, string | boolean | undefined>;
|
|
13
|
+
/** The journey and iteration being run (unset in `setup`). */
|
|
14
|
+
journey?: string;
|
|
15
|
+
iteration?: number;
|
|
16
|
+
}
|
|
17
|
+
export type JourneyFn = (t: Session, ctx: JourneyContext) => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* What a customer's journey module default-exports. Everything app-specific is a value in here; the driver owns the
|
|
20
|
+
* session, the run tokens, the begin/end of each run, the `run:complete` marker and the result files.
|
|
21
|
+
*/
|
|
22
|
+
export interface JourneyModule {
|
|
23
|
+
app: {
|
|
24
|
+
bundleId: string;
|
|
25
|
+
scheme: string;
|
|
26
|
+
};
|
|
27
|
+
/** Visible once the app has launched; begin-run waits for it before opening the activation link. */
|
|
28
|
+
home: SelectorLike;
|
|
29
|
+
sessionName?: string;
|
|
30
|
+
/** Extra command-line flags the module reads from `ctx.args` (node:util parseArgs option configs). */
|
|
31
|
+
options?: Record<string, {
|
|
32
|
+
type: 'string' | 'boolean';
|
|
33
|
+
default?: string | boolean;
|
|
34
|
+
multiple?: false;
|
|
35
|
+
}>;
|
|
36
|
+
/** Query args of the SDK activation link (`k=v&...`), set over the run defaults (`RUN_DEFAULTS`) key by key. */
|
|
37
|
+
sdkArgs?: string;
|
|
38
|
+
/** Once per session, after it started and before the first iteration (e.g. pointing the app at a mock server). */
|
|
39
|
+
setup?: JourneyFn;
|
|
40
|
+
/** One function per journey name that `--journeys` may list. */
|
|
41
|
+
journeys: Record<string, JourneyFn>;
|
|
42
|
+
}
|
|
43
|
+
export declare const defineJourney: (m: JourneyModule) => JourneyModule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journey.js","sourceRoot":"","sources":["../../src/journey.ts"],"names":[],"mappings":"AAuCA,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAgB,EAAiB,EAAE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Driver } from './webdriver.js';
|
|
2
|
+
export declare const ALERT_POLL_MS = 250;
|
|
3
|
+
/**
|
|
4
|
+
* The default run configuration (REQ-FARM-28), the one source for the farm, this driver, the agent CLI and the Maestro flows:
|
|
5
|
+
* React profiling and the React export on, the Hermes CPU profile off (no `cpuprof`, REQ-MEAS-31), and one GC at the run end
|
|
6
|
+
* (REQ-MEAS-35). A runner turns a default off only explicitly (`profile=0`, `diag=0`, `--end-gc off`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const RUN_DEFAULTS: {
|
|
9
|
+
readonly ttl: 3600;
|
|
10
|
+
readonly profile: 1;
|
|
11
|
+
readonly diag: 1;
|
|
12
|
+
};
|
|
13
|
+
export declare const END_GC_DEFAULT = true;
|
|
14
|
+
/** `k=v&k2=v2` into pairs; an empty string gives none. */
|
|
15
|
+
export declare function parseSdkArgs(args: string): Array<[string, string]>;
|
|
16
|
+
/**
|
|
17
|
+
* The activation link's settings: the defaults, then `args` (`k=v&...`, e.g. a journey module's `sdkArgs`) over them key by key, so
|
|
18
|
+
* a module that names only `profile=1` still gets `diag=1`. `activate` and `runToken` are not settings and are dropped.
|
|
19
|
+
*/
|
|
20
|
+
export declare function linkArgs(args?: string): string;
|
|
21
|
+
export declare const activationLink: (scheme: string, token: string, args?: string) => string;
|
|
22
|
+
export declare const flushLink: (scheme: string) => string;
|
|
23
|
+
/** Command n (a GC); the SDK takes it as a link on both platforms, and Android drivers send the same command as a broadcast. */
|
|
24
|
+
export declare const gcLink: (scheme: string, n: number) => string;
|
|
25
|
+
/** Taps the button of a system alert if one shows within `waitMs` ("Open in ...?" and any permission prompt). Returns the label tapped, or null. */
|
|
26
|
+
export declare function acceptAlert(driver: Driver, labels: readonly string[], waitMs: number): Promise<string | null>;
|
|
27
|
+
export interface LinkState {
|
|
28
|
+
linkMode?: 'deepLink' | 'safari';
|
|
29
|
+
linkError?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Opens a deep link. `mobile: deepLink` (XCUITest driver, iOS 16.4+) first; Safari's address bar if the driver refuses
|
|
33
|
+
* it, since `driver.get` opens Siri on a real iPhone.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openLink(driver: Driver, url: string, state: LinkState): Promise<string | null>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const ALERT_POLL_MS = 250;
|
|
2
|
+
/**
|
|
3
|
+
* The default run configuration (REQ-FARM-28), the one source for the farm, this driver, the agent CLI and the Maestro flows:
|
|
4
|
+
* React profiling and the React export on, the Hermes CPU profile off (no `cpuprof`, REQ-MEAS-31), and one GC at the run end
|
|
5
|
+
* (REQ-MEAS-35). A runner turns a default off only explicitly (`profile=0`, `diag=0`, `--end-gc off`).
|
|
6
|
+
*/
|
|
7
|
+
export const RUN_DEFAULTS = { ttl: 3600, profile: 1, diag: 1 };
|
|
8
|
+
export const END_GC_DEFAULT = true;
|
|
9
|
+
// The SDK's link route under the app's own URL scheme (e.g. `myapp://`).
|
|
10
|
+
const route = (scheme) => `${scheme}devtools/perf-lab`;
|
|
11
|
+
/** `k=v&k2=v2` into pairs; an empty string gives none. */
|
|
12
|
+
export function parseSdkArgs(args) {
|
|
13
|
+
return args
|
|
14
|
+
.split('&')
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((kv) => {
|
|
17
|
+
const i = kv.indexOf('=');
|
|
18
|
+
return i < 0 ? [kv, '1'] : [kv.slice(0, i), kv.slice(i + 1)];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The activation link's settings: the defaults, then `args` (`k=v&...`, e.g. a journey module's `sdkArgs`) over them key by key, so
|
|
23
|
+
* a module that names only `profile=1` still gets `diag=1`. `activate` and `runToken` are not settings and are dropped.
|
|
24
|
+
*/
|
|
25
|
+
export function linkArgs(args = '') {
|
|
26
|
+
const fields = new Map(Object.entries(RUN_DEFAULTS).map(([k, v]) => [k, String(v)]));
|
|
27
|
+
for (const [k, v] of parseSdkArgs(args))
|
|
28
|
+
if (k !== 'activate' && k !== 'runToken')
|
|
29
|
+
fields.set(k, v);
|
|
30
|
+
return [...fields].map(([k, v]) => `${k}=${v}`).join('&');
|
|
31
|
+
}
|
|
32
|
+
export const activationLink = (scheme, token, args = '') => `${route(scheme)}?activate=1&${linkArgs(args)}&runToken=${token}`;
|
|
33
|
+
export const flushLink = (scheme) => `${route(scheme)}?flush=1&activate=0`;
|
|
34
|
+
/** Command n (a GC); the SDK takes it as a link on both platforms, and Android drivers send the same command as a broadcast. */
|
|
35
|
+
export const gcLink = (scheme, n) => `${route(scheme)}?cmd=gc&n=${n}`;
|
|
36
|
+
/** Taps the button of a system alert if one shows within `waitMs` ("Open in ...?" and any permission prompt). Returns the label tapped, or null. */
|
|
37
|
+
export async function acceptAlert(driver, labels, waitMs) {
|
|
38
|
+
const deadline = driver.now() + waitMs;
|
|
39
|
+
for (;;) {
|
|
40
|
+
let buttons = null;
|
|
41
|
+
try {
|
|
42
|
+
buttons = await driver.execute('mobile: alert', { action: 'getButtons' });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
buttons = null;
|
|
46
|
+
}
|
|
47
|
+
const hit = Array.isArray(buttons) ? labels.find((l) => buttons.includes(l)) : undefined;
|
|
48
|
+
if (hit) {
|
|
49
|
+
await driver.execute('mobile: alert', { action: 'accept', buttonLabel: hit });
|
|
50
|
+
return hit;
|
|
51
|
+
}
|
|
52
|
+
if (driver.now() > deadline)
|
|
53
|
+
return null;
|
|
54
|
+
await driver.sleep(ALERT_POLL_MS);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opens a deep link. `mobile: deepLink` (XCUITest driver, iOS 16.4+) first; Safari's address bar if the driver refuses
|
|
59
|
+
* it, since `driver.get` opens Siri on a real iPhone.
|
|
60
|
+
*/
|
|
61
|
+
export async function openLink(driver, url, state) {
|
|
62
|
+
if (state.linkMode !== 'safari') {
|
|
63
|
+
try {
|
|
64
|
+
await driver.execute('mobile: deepLink', { url, bundleId: driver.bundleId });
|
|
65
|
+
state.linkMode = 'deepLink';
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (state.linkMode === 'deepLink')
|
|
69
|
+
throw e;
|
|
70
|
+
state.linkMode = 'safari';
|
|
71
|
+
state.linkError = String(e.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (state.linkMode === 'safari') {
|
|
75
|
+
await driver.execute('mobile: launchApp', { bundleId: 'com.apple.mobilesafari' });
|
|
76
|
+
const bar = (await driver.find('accessibility id', 'TabBarItemTitle')) ?? (await driver.find('accessibility id', 'URL'));
|
|
77
|
+
if (bar)
|
|
78
|
+
await driver.click(bar);
|
|
79
|
+
const field = await driver.find('-ios predicate string', 'type == "XCUIElementTypeTextField" AND name == "URL"');
|
|
80
|
+
if (!field)
|
|
81
|
+
throw new Error('Safari address bar not found');
|
|
82
|
+
await driver.cmd('POST', `/element/${field}/value`, { text: `${url}\n` });
|
|
83
|
+
}
|
|
84
|
+
// In deepLink mode autoAcceptAlerts already answers a prompt, so this only looks once.
|
|
85
|
+
return acceptAlert(driver, ['Open'], state.linkMode === 'safari' ? 8000 : 0);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/links.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAW,CAAC;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC,yEAAyE;AACzE,MAAM,KAAK,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,GAAG,MAAM,mBAAmB,CAAC;AAEvE,0DAA0D;AAC1D,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI;SACR,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACV,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAI,GAAG,EAAE;IAChC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAiB,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrG,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,UAAU;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACpG,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAc,EAAE,KAAa,EAAE,IAAI,GAAG,EAAE,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,eAAe,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,EAAE,CAAC;AAEtJ,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,qBAAqB,CAAC;AAC3F,gIAAgI;AAChI,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,CAAS,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;AAE9F,oJAAoJ;AACpJ,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,MAAyB,EAAE,MAAc;IACzF,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC;IACvC,SAAS,CAAC;QACR,IAAI,OAAO,GAAY,IAAI,CAAC;QAC5B,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC5E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzF,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;YAC9E,OAAO,GAAG,CAAC;QACb,CAAC;QACD,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,QAAQ;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;AACH,CAAC;AAOD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,GAAW,EAAE,KAAgB;IAC1E,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC7E,KAAK,CAAC,QAAQ,GAAG,UAAU,CAAC;QAC9B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,KAAK,CAAC,QAAQ,KAAK,UAAU;gBAAE,MAAM,CAAC,CAAC;YAC3C,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAC1B,KAAK,CAAC,SAAS,GAAG,MAAM,CAAE,CAAW,CAAC,OAAO,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChC,MAAM,MAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,QAAQ,EAAE,wBAAwB,EAAE,CAAC,CAAC;QAClF,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC,CAAC;QACzH,IAAI,GAAG;YAAE,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,sDAAsD,CAAC,CAAC;QACjH,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAC5D,MAAM,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,QAAQ,EAAE,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,uFAAuF;IACvF,OAAO,WAAW,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JourneyModule } from './journey.js';
|
|
2
|
+
/** Loads the customer's journey module: its default export, or the module itself when it has no default. */
|
|
3
|
+
export declare function loadJourneyModule(path: string, cwd?: string): Promise<JourneyModule>;
|
|
4
|
+
export declare function main(argv: readonly string[]): Promise<number>;
|
package/dist/src/load.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { journeyModulePath, runDriver } from './run.js';
|
|
4
|
+
/** Loads the customer's journey module: its default export, or the module itself when it has no default. */
|
|
5
|
+
export async function loadJourneyModule(path, cwd = process.cwd()) {
|
|
6
|
+
const file = isAbsolute(path) ? path : resolve(cwd, path);
|
|
7
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
8
|
+
const m = mod.default ?? mod;
|
|
9
|
+
if (!m.app?.bundleId || !m.app.scheme)
|
|
10
|
+
throw new Error(`${path}: the journey module must export app: { bundleId, scheme }`);
|
|
11
|
+
if (m.home === undefined)
|
|
12
|
+
throw new Error(`${path}: the journey module must export home (the selector visible after launch)`);
|
|
13
|
+
if (!m.journeys || Object.keys(m.journeys).length === 0)
|
|
14
|
+
throw new Error(`${path}: the journey module must export journeys: { <name>: async (t, ctx) => {...} }`);
|
|
15
|
+
return m;
|
|
16
|
+
}
|
|
17
|
+
export async function main(argv) {
|
|
18
|
+
const path = journeyModulePath(argv);
|
|
19
|
+
if (!path)
|
|
20
|
+
throw new Error('--journey-module <file> is required');
|
|
21
|
+
return runDriver(argv, await loadJourneyModule(path));
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAExD,4GAA4G;AAC5G,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACvE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAyD,CAAC;IAC7G,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,IAAK,GAAqB,CAAC;IAChD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,4DAA4D,CAAC,CAAC;IAC5H,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,2EAA2E,CAAC,CAAC;IAC9H,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,gFAAgF,CAAC,CAAC;IAClK,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAuB;IAChD,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAClE,OAAO,SAAS,CAAC,IAAI,EAAE,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type LinkState } from './links.js';
|
|
2
|
+
import { type Locator, type Selector } from './selectors.js';
|
|
3
|
+
import { Screen } from './snapshot.js';
|
|
4
|
+
import { type Driver } from './webdriver.js';
|
|
5
|
+
export type PlatformName = 'ios' | 'android';
|
|
6
|
+
/** Everything that differs between the XCUITest and the UiAutomator2 driver; the session and the journeys do not see it. */
|
|
7
|
+
export interface Platform {
|
|
8
|
+
readonly name: PlatformName;
|
|
9
|
+
/** `model-os`, the form of `--device` (BrowserStack's deviceName and platformVersion). */
|
|
10
|
+
readonly defaultDevice: string;
|
|
11
|
+
/** Platform capabilities of a session on `device` (`model-os`). */
|
|
12
|
+
caps(device: string): Record<string, unknown>;
|
|
13
|
+
/** Sent to `/appium/settings` once the session started. */
|
|
14
|
+
readonly settings: Record<string, unknown>;
|
|
15
|
+
/** The lookups to try, in order, for a selector (UiSelector has no OR, so Android text is two). */
|
|
16
|
+
locators(sel: Selector): Locator[];
|
|
17
|
+
/** Argument of `mobile: terminateApp / activateApp`. */
|
|
18
|
+
appArg(appId: string): Record<string, string>;
|
|
19
|
+
/** Opens a deep link in the app; returns the label of a system prompt it accepted, or null. */
|
|
20
|
+
openLink(driver: Driver, url: string, state: LinkState): Promise<string | null>;
|
|
21
|
+
/** Accepts a system prompt if one shows within `waitMs`; returns what it accepted, or null. */
|
|
22
|
+
acceptAlert(driver: Driver, labels: readonly string[], waitMs: number): Promise<string | null>;
|
|
23
|
+
/**
|
|
24
|
+
* `perflab.flushed`: the lookup tried on every poll, which attribute holds the status, and a heavier lookup tried now and
|
|
25
|
+
* then when the cheap one misses (it needs `setting` on while it runs).
|
|
26
|
+
*/
|
|
27
|
+
readonly flush: {
|
|
28
|
+
lookup: Locator;
|
|
29
|
+
statusAttribute: string;
|
|
30
|
+
fallback?: {
|
|
31
|
+
lookup: Locator;
|
|
32
|
+
setting: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One read of the whole screen that answers any number of "is this selector shown" questions (a step's combined
|
|
37
|
+
* conditions). The reading also lists the nodes the platform leaves out of lookups (dearer: about twice a plain source on the S23's live screen). Absent where the platform has no cheap one: the session then asks with one lookup per condition.
|
|
38
|
+
*/
|
|
39
|
+
readScreen?(driver: Driver): Promise<Screen>;
|
|
40
|
+
/** Whether the on-screen keyboard is up: the question the journey asks, answered the way each platform can. */
|
|
41
|
+
keyboardShown(driver: Driver): Promise<boolean>;
|
|
42
|
+
/** One lookup for several resource ids at once: a tree walk costs the same for one id and for ten (UiAutomator2 walks it whole when nothing is found). */
|
|
43
|
+
readonly anyOfIds?: (ids: readonly string[]) => Locator;
|
|
44
|
+
/**
|
|
45
|
+
* Look for one element first (one cheap request when it is there); only when that one is not on screen look at all matches.
|
|
46
|
+
* The answer is the same as looking at all matches at once.
|
|
47
|
+
*/
|
|
48
|
+
readonly singleFindFirst?: boolean;
|
|
49
|
+
/** BrowserStack accepts an upload option that only means something for iOS. */
|
|
50
|
+
readonly uploadFields: Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* GC before the action (docs/plans/gc-before-action.md): asks the SDK to end its open windows, run a full GC and answer.
|
|
53
|
+
* Returns the SDK's answer (`gc:<n> ok` or `gc:<n> unavailable`), or null when no answer came (an SDK without the command).
|
|
54
|
+
*/
|
|
55
|
+
gcCommand(driver: Driver, n: number, scheme: string, state: LinkState): Promise<string | null>;
|
|
56
|
+
}
|
|
57
|
+
export declare const GC_ANSWER_WAIT_MS = 10000;
|
|
58
|
+
/** The answer for command n out of `am broadcast` output (`Broadcast completed: result=-1, data="gc:3 ok"`). */
|
|
59
|
+
export declare function parseBroadcastAnswer(out: unknown, n: number): string | null;
|
|
60
|
+
export declare const IOS: Platform;
|
|
61
|
+
export declare const ANDROID: Platform;
|
|
62
|
+
export declare const PLATFORMS: Record<PlatformName, Platform>;
|
|
63
|
+
export declare function platformOf(name: string): Platform;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ALERT_POLL_MS, acceptAlert as acceptIosAlert, gcLink, openLink as openIosLink } from './links.js';
|
|
2
|
+
import { androidAnyOfIds, androidLocators, id, locator } from './selectors.js';
|
|
3
|
+
import { Screen, parseAndroidSource } from './snapshot.js';
|
|
4
|
+
import { WdError } from './webdriver.js';
|
|
5
|
+
// The split of `model-os` is BrowserStack's: the last dash starts the OS version.
|
|
6
|
+
const modelOf = (device) => device.replace(/-[^-]*$/, '');
|
|
7
|
+
const versionOf = (device) => device.replace(/^.*-/, '');
|
|
8
|
+
const FLUSH_ID = 'perflab.flushed';
|
|
9
|
+
const ACK_ID = 'perflab.ack';
|
|
10
|
+
// Only ends the wait for an iOS answer that never comes (an SDK without the command); no window edge depends on it.
|
|
11
|
+
export const GC_ANSWER_WAIT_MS = 10_000;
|
|
12
|
+
const GC_POLL_MS = 100;
|
|
13
|
+
/** The answer for command n out of `am broadcast` output (`Broadcast completed: result=-1, data="gc:3 ok"`). */
|
|
14
|
+
export function parseBroadcastAnswer(out, n) {
|
|
15
|
+
const data = /data="([^"]*)"/.exec(String(out ?? ''))?.[1];
|
|
16
|
+
return data !== undefined && data.startsWith(`gc:${n} `) ? data : null;
|
|
17
|
+
}
|
|
18
|
+
// A command waits for the app to go idle by default; PerfLab must not change the app's pace (docs/plans/61-android-support.md 1.2).
|
|
19
|
+
const NO_WAIT_MS = 0;
|
|
20
|
+
// Appium's own session-idle limit, the same on both platforms.
|
|
21
|
+
const NEW_COMMAND_TIMEOUT_S = 300;
|
|
22
|
+
export const IOS = {
|
|
23
|
+
name: 'ios',
|
|
24
|
+
defaultDevice: 'iPhone 15 Pro-17',
|
|
25
|
+
caps: (device) => ({
|
|
26
|
+
platformName: 'iOS',
|
|
27
|
+
'appium:automationName': 'XCUITest',
|
|
28
|
+
'appium:deviceName': modelOf(device),
|
|
29
|
+
'appium:platformVersion': versionOf(device),
|
|
30
|
+
'appium:autoAcceptAlerts': true,
|
|
31
|
+
'appium:newCommandTimeout': NEW_COMMAND_TIMEOUT_S,
|
|
32
|
+
'appium:waitForIdleTimeout': NO_WAIT_MS,
|
|
33
|
+
}),
|
|
34
|
+
settings: { animationCoolOffTimeout: NO_WAIT_MS, waitForIdleTimeout: NO_WAIT_MS, shouldUseCompactResponses: true },
|
|
35
|
+
locators: (sel) => [locator(sel)],
|
|
36
|
+
// The Return key is the one element of the iOS keyboard XCUITest lists reliably.
|
|
37
|
+
keyboardShown: async (driver) => Boolean(await driver.findShown(id('Return'))),
|
|
38
|
+
appArg: (bundleId) => ({ bundleId }),
|
|
39
|
+
openLink: (driver, url, state) => openIosLink(driver, url, state),
|
|
40
|
+
acceptAlert: (driver, labels, waitMs) => acceptIosAlert(driver, labels, waitMs),
|
|
41
|
+
flush: { lookup: { using: 'accessibility id', value: FLUSH_ID }, statusAttribute: 'label' },
|
|
42
|
+
uploadFields: { ios_keychain_support: 'true' },
|
|
43
|
+
// The link, then the `perflab.ack` element. The trailing space keeps `gc:1` from matching the answer to 10.
|
|
44
|
+
async gcCommand(driver, n, scheme, state) {
|
|
45
|
+
await openIosLink(driver, gcLink(scheme, n), state);
|
|
46
|
+
const want = `type == "XCUIElementTypeStaticText" AND name == "${ACK_ID}" AND label BEGINSWITH "gc:${n} "`;
|
|
47
|
+
const deadline = driver.now() + GC_ANSWER_WAIT_MS;
|
|
48
|
+
for (;;) {
|
|
49
|
+
const el = await driver.find('-ios predicate string', want);
|
|
50
|
+
// The predicate already matched `gc:<n> `; an unreadable label is taken as ok and logged, so a run that relied on it can be found.
|
|
51
|
+
if (el)
|
|
52
|
+
return driver
|
|
53
|
+
.cmd('GET', `/element/${el}/attribute/label`)
|
|
54
|
+
.then(String)
|
|
55
|
+
.catch(() => {
|
|
56
|
+
driver.log.push({ ...driver.context, cmd: 'gc-ack-label', t: driver.now(), ms: 0, status: 1 });
|
|
57
|
+
return `gc:${n} ok`;
|
|
58
|
+
});
|
|
59
|
+
if (driver.now() > deadline)
|
|
60
|
+
return null;
|
|
61
|
+
await driver.sleep(GC_POLL_MS);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const ANDROID = {
|
|
66
|
+
name: 'android',
|
|
67
|
+
// The phone of the TapIn farm workflow (docs/plans/61-android-support.md 1.2).
|
|
68
|
+
defaultDevice: 'Google Pixel 7-13.0',
|
|
69
|
+
caps: (device) => ({
|
|
70
|
+
platformName: 'Android',
|
|
71
|
+
'appium:automationName': 'UiAutomator2',
|
|
72
|
+
'appium:deviceName': modelOf(device),
|
|
73
|
+
'appium:platformVersion': versionOf(device),
|
|
74
|
+
'appium:autoGrantPermissions': true,
|
|
75
|
+
'appium:newCommandTimeout': NEW_COMMAND_TIMEOUT_S,
|
|
76
|
+
}),
|
|
77
|
+
// enableMultiWindows: with an RN Modal open (its own Dialog window) XPath and the source still reach the activity window under it, where the flush element lives.
|
|
78
|
+
// ignoreUnimportantViews: lookups skip nodes Android marks not important for accessibility, about 30 % faster on the S23. Safe for a journey that looks for RN testIDs, texts and descriptions: in 6 recorded S23 page sources every node with an app testID (live.*) was a11y-important (4, 21 and 26 in plain sources, 40, 42 and 47 with hidden nodes; 0 unimportant). A bare wrapper view without testID would not be found.
|
|
79
|
+
settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
|
|
80
|
+
locators: androidLocators,
|
|
81
|
+
appArg: (appId) => ({ appId }),
|
|
82
|
+
// `package` pins the link to the app: a scheme two builds register would otherwise show a chooser.
|
|
83
|
+
async openLink(driver, url) {
|
|
84
|
+
await driver.execute('mobile: deepLink', { url, package: driver.bundleId });
|
|
85
|
+
return null;
|
|
86
|
+
},
|
|
87
|
+
// UiAutomator2 has the W3C alert commands, which handle the runtime-permission dialog (there are no button labels to choose from).
|
|
88
|
+
async acceptAlert(driver, _labels, waitMs) {
|
|
89
|
+
const deadline = driver.now() + waitMs;
|
|
90
|
+
for (;;) {
|
|
91
|
+
try {
|
|
92
|
+
const alertText = await driver.cmd('GET', '/alert/text');
|
|
93
|
+
await driver.cmd('POST', '/alert/accept', {});
|
|
94
|
+
return String(alertText);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (!(e instanceof WdError && e.wdError === 'no such alert'))
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
if (driver.now() > deadline)
|
|
101
|
+
return null;
|
|
102
|
+
await driver.sleep(ALERT_POLL_MS);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
// The cheap UiSelector lookup on content-desc; XPath walks every window, so it is only the fallback (a Modal open). The status is the element's text.
|
|
106
|
+
flush: {
|
|
107
|
+
lookup: { using: '-android uiautomator', value: `new UiSelector().description("${FLUSH_ID}")` },
|
|
108
|
+
statusAttribute: 'text',
|
|
109
|
+
fallback: { lookup: { using: 'xpath', value: `//*[@content-desc="${FLUSH_ID}"]` }, setting: 'allowInvisibleElements' },
|
|
110
|
+
},
|
|
111
|
+
async readScreen(driver) {
|
|
112
|
+
// The source leaves out nodes the accessibility service calls not visible unless this is on; the screen decides for itself which of them count (snapshot.ts).
|
|
113
|
+
await driver.cmd('POST', '/appium/settings', { settings: { allowInvisibleElements: true } });
|
|
114
|
+
let xml;
|
|
115
|
+
try {
|
|
116
|
+
xml = await driver.cmd('GET', '/source');
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await driver.cmd('POST', '/appium/settings', { settings: { allowInvisibleElements: false } }).catch(() => null);
|
|
120
|
+
}
|
|
121
|
+
return new Screen(parseAndroidSource(xml), await driver.screen());
|
|
122
|
+
},
|
|
123
|
+
keyboardShown: async (driver) => Boolean(await driver.execute('mobile: isKeyboardShown')),
|
|
124
|
+
singleFindFirst: true,
|
|
125
|
+
anyOfIds: androidAnyOfIds,
|
|
126
|
+
uploadFields: {},
|
|
127
|
+
// A broadcast pauses no activity (a link does). `am broadcast` returns once the SDK finished the receiver, so its output is the answer.
|
|
128
|
+
async gcCommand(driver, n) {
|
|
129
|
+
const out = await driver.execute('mobile: broadcast', {
|
|
130
|
+
action: `${driver.bundleId}.perflab.COMMAND`,
|
|
131
|
+
package: driver.bundleId,
|
|
132
|
+
extras: [['s', 'cmd', 'gc'], ['i', 'n', String(n)]],
|
|
133
|
+
// FLAG_RECEIVER_FOREGROUND: the foreground broadcast queue, not the background one that other apps' broadcasts can delay.
|
|
134
|
+
flags: '0x10000000',
|
|
135
|
+
});
|
|
136
|
+
return parseBroadcastAnswer(out, n);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
export const PLATFORMS = { ios: IOS, android: ANDROID };
|
|
140
|
+
export function platformOf(name) {
|
|
141
|
+
if (!Object.hasOwn(PLATFORMS, name))
|
|
142
|
+
throw new Error(`--platform ${name}: the Appium driver drives ios and android`);
|
|
143
|
+
return PLATFORMS[name];
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/platform.ts"],"names":[],"mappings":"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"}
|