@testingbot/mobilewright-driver 0.6.0 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-11-10
4
+
5
+ ### Added
6
+ - **Exact per-test reporting.** The driver reads Playwright's `test.info()`
7
+ inside `connect()`/`disconnect()` (which run inside the test), so each
8
+ session's test and outcome are known exactly instead of reconstructed from
9
+ timestamps. With `sessionPerTest` each session is named after its test as it
10
+ starts (`describe › title (retry N)`) and gets its verdict when the test
11
+ ends; pooled sessions fail if any test on them failed. Every test in the
12
+ Playwright report links to its TestingBot session. The timing-based join
13
+ remains as the fallback.
14
+ - **Element screenshots.** `locator.screenshot()` is cropped to the element
15
+ (points → pixels via the screen scale) and `screenshot({ scale })`
16
+ downscales; previously both returned the full screen.
17
+ - **`screen.goBack()` on iOS** performs the system left-edge back swipe
18
+ instead of throwing.
19
+ - **`snapshotTuning`** option, on by default: `waitForIdleTimeout: 0` (plus
20
+ `animationCoolOffTimeout: 0` on iOS) once per session. Measured on
21
+ production: fill + assert 7.3s → 3.5s on an Android emulator, 3.4s → 2.95s
22
+ on an iOS simulator. `false` keeps the server defaults.
23
+ - **`visibility: 'bounds'`** option: judge `isVisible` by frame size, like
24
+ local mobilecli.
25
+ - **`allocationRetryDelay`** option: a "no device available" error while the
26
+ run holds no session is retried with backoff (5s doubling to 30s) until
27
+ the pool gives up. mobilewright's pool only retries when one of its own
28
+ slots is released, so such a test previously idled until its timeout.
29
+
30
+ ### Fixed
31
+ - `fill()` / `clearText()` on iOS now clear the focused field through its
32
+ element reference. XCUITest's page source has no focused flag, and React
33
+ Native fields put the caret at the start, so the old backspace fallback
34
+ never removed anything (`fill('7')` on a field holding `0` gave `70`).
35
+ - Rotated `sessionPerTest` sessions now correct the test's `device.model` /
36
+ `device.osVersion` annotations, which described the pool slot's first
37
+ session.
38
+
3
39
  ## 0.6.0 — 2026-10-28
4
40
 
5
41
  Tracks `@mobilewright/protocol` 0.0.67.
package/README.md CHANGED
@@ -93,9 +93,11 @@ Regexes and version ranges work on both real and virtual devices: real devices r
93
93
 
94
94
  ## Sessions, naming, and reporting
95
95
 
96
- - By default a device slot's TestingBot session is reused across tests (fast: you pay device startup once). Sessions that hosted exactly one test are named after that test with its own pass/fail; sessions that hosted several get a run summary (e.g. `12/12 tests passed`).
97
- - Set **`sessionPerTest: true`** for one session, video, and named verdict **per test** — the clearest dashboard, at the cost of device startup on every test.
98
- - Tests are matched to sessions by **worker identity first**: the driver stamps Playwright's worker index on each session as it records it, and the run report reports the same index per test, so a test that ran in another worker process is never a candidate. Timing separates the tests within one worker, using the worker's own timestamps from the report when it is available.
96
+ - By default a device slot's TestingBot session is reused across tests (fast: you pay device startup once). Sessions that hosted exactly one test are named after that test with its own pass/fail; sessions that hosted several pass only if every test on them passed (e.g. `1 of 3 tests failed. checkout › pays: ...`).
97
+ - Set **`sessionPerTest: true`** for one session, video, and named verdict **per test** — the clearest dashboard, at the cost of device startup on every test. The session is named after the test as soon as it starts (`describe › title`, plus `(retry N)` on retries), and its verdict is reported the moment the test ends, while the session is still alive.
98
+ - Each test in Playwright's HTML report gets a **`TestingBot`** annotation linking to the session (and its video) that test ran on.
99
+ - Tests are matched to sessions **exactly**: `connect()`/`disconnect()` run inside the test, so the driver reads Playwright's `test.info()` there and records which test attempt ran on which session, with its outcome. The heuristic below is only the fallback when `@playwright/test` cannot be resolved from the project.
100
+ - Fallback: tests are matched to sessions by **worker identity first**: the driver stamps Playwright's worker index on each session as it records it, and the run report reports the same index per test, so a test that ran in another worker process is never a candidate. Timing separates the tests within one worker, using the worker's own timestamps from the report when it is available.
99
101
  - A session is only ever given a verdict the driver can stand behind: its own test's, or the run summary when it genuinely hosted several tests (or was the run's only session). When a session cannot be tied to a test — an ambiguous overlap under heavy parallelism, or a worker that died before recording its span — it is reported with build, extra and tags but **no** pass/fail, rather than inheriting a failure from a test that ran somewhere else. `DEBUG=testingbot:observer` names the session and the reason.
100
102
  - Git metadata is reported automatically when mobilewright captures it: the commit hash, branch, author and subject land in the session's **extra** field (alongside your own `extra`, never replacing it), and the branch is added as a **tag** so the dashboard can filter by it. Enable it with `captureGitInfo: { commit: true }` in `mobilewright.config.ts` — Playwright turns it on by itself in CI.
101
103
  - Failure messages are reported to the session's status message; `build` groups sessions on the dashboard and **defaults automatically from CI environments** (GitHub Actions, GitLab, CircleCI, Buildkite, Bitrise, Travis, Azure DevOps, Jenkins, TeamCity → e.g. `owner/repo #123`), or from a `TESTINGBOT_BUILD` variable.
@@ -115,6 +117,14 @@ new TestingBotDriver({
115
117
  build: 'ci-1234', // default: auto-detected from CI env / TESTINGBOT_BUILD
116
118
  testResults: 'on', // 'off' disables pass/fail reporting
117
119
 
120
+ snapshotTuning: {}, // Appium settings applied once per session; default turns off
121
+ // the wait-for-idle before each hierarchy read (much faster
122
+ // assertions while the UI animates). false = server defaults
123
+ visibility: 'native', // 'bounds' = any element with a non-empty frame is visible
124
+ // (mobilecli-like; helps iOS toBeVisible() on overlapped views)
125
+ allocationRetryDelay: 5_000,// first backoff (ms) when no device is free and the run holds
126
+ // no session yet; doubles up to 30s until the pool gives up
127
+
118
128
  allocationTimeout: 395_000, // ms to wait for a device (TestingBot queues busy devices)
119
129
  idleTimeout: 230, // seconds before TestingBot reaps an idle session
120
130
  maxDuration: 1800, // max session length in seconds
@@ -246,6 +256,8 @@ jobs:
246
256
  | `cannot install "..." mid-session` | TestingBot has no mid-session installs — every app in `installApps` must also be listed in the driver's `apps` option (first entry = app under test, the rest install as helper apps). |
247
257
  | Modifier chords fail on iOS | XCUITest cannot hold modifier keys; chords are Android-only (`clearText()` handles iOS differently). |
248
258
  | Sessions show the run summary instead of a test name | The session hosted several tests; `sessionPerTest: true` gives each test its own session. |
259
+ | An iOS project never gets a device in a mixed Android + iOS run | mobilewright's pool keeps idle slots of the other platform. Run each platform in its own `mobilewright test --project ...` invocation, or raise `workers`. |
260
+ | `toBeVisible()` fails on iOS for an element that is on screen | XCUITest reports elements drawn under a sibling as invisible. Set `visibility: 'bounds'`. |
249
261
  | A session shows no pass/fail at all | The driver could not tell which test ran in it and declined to guess. Run with `DEBUG=testingbot:observer` — it logs the session id and why. |
250
262
  | `DEBUG=testingbot:*` | Prints every allocation, hub command, upload, and report the driver performs. |
251
263
 
@@ -265,11 +277,11 @@ jobs:
265
277
 
266
278
  ## Current limitations
267
279
 
268
- - One TestingBot session may host several mobilewright tests unless `sessionPerTest: true`; multi-test sessions report the run-level verdict.
269
- - The worker process that owns a session is never told which test it is running, so the session-to-test join is reconstructed after the run from Playwright's worker index plus timing. Without the run report (`jsonReport()`) it degrades to timing alone. Either way it is deliberately conservative and leaves a session unjudged rather than risk naming it after the wrong test.
280
+ - One TestingBot session may host several mobilewright tests unless `sessionPerTest: true`; a multi-test session keeps one name and fails if any test on it failed.
281
+ - **Run Android and iOS projects in separate invocations** (`--project android`, then `--project ios`) unless `workers` exceeds the number of projects. mobilewright's device pool never evicts an idle slot of one platform to make room for the other, so with every slot holding an Android session an iOS test waits until it times out.
270
282
  - Modifier key chords (`pressKeys(['ctrl+a'])`) work on Android only — XCUITest cannot hold modifier keys.
271
- - `pressButton` on iOS supports `HOME`, `VOLUME_UP`, `VOLUME_DOWN`; `listApps()` reports the foreground app only.
272
- - Screenshots are always PNG, and `screenshot({ clip })` is not honoured yet — the full screen comes back uncropped. `applyDeviceSettings` turns Android animations off best-effort via `mobile: shell` (a no-op on iOS).
283
+ - `pressButton` on iOS supports `HOME`, `VOLUME_UP`, `VOLUME_DOWN`, and `BACK` (performed as the system left-edge swipe, so `screen.goBack()` works in navigation stacks); `listApps()` reports the foreground app only.
284
+ - Screenshots are always PNG. `locator.screenshot()` is cropped to the element and `screenshot({ scale })` downscales. `applyDeviceSettings` turns Android animations off best-effort via `mobile: shell` (a no-op on iOS).
273
285
  - `setGeolocation` needs the platform driver's location extension to be available: emulators and simulators always are, real devices depend on the OS version TestingBot's Appium build supports (iOS 17+ for XCUITest).
274
286
  - `setFoldState` (protocol 0.0.62) throws: Appium has no hinge control for TestingBot's foldables.
275
287
  - Real iOS devices need a test-signed `.ipa`; simulator builds are rejected on real devices with a clear error.
@@ -285,6 +297,14 @@ npm run build
285
297
 
286
298
  Live e2e tests against a real TestingBot account live in `e2e/` (see `e2e/mobilewright.config.ts`).
287
299
 
300
+ `e2e/live.config.ts` runs feature checks (hierarchy speed, fill, element screenshots, iOS back navigation, per-test reporting) against TestingBot's public sample app, so it needs no local builds:
301
+
302
+ ```bash
303
+ cd e2e && npm install --no-save mobilewright@latest @mobilewright/test@latest
304
+ TESTINGBOT_KEY=... TESTINGBOT_SECRET=... npx mobilewright test -c live.config.ts --project android-emulator
305
+ # E2E_SESSION_PER_TEST=1, E2E_FORCE_FAIL=1, E2E_RETRIES=1, E2E_VISIBILITY=bounds, E2E_NO_TUNING=1 toggle scenarios
306
+ ```
307
+
288
308
  ### Releasing
289
309
 
290
310
  Update `CHANGELOG.md` first — it ships inside the published tarball.
package/dist/actions.d.ts CHANGED
@@ -18,6 +18,12 @@ export declare function tapActions(x: number, y: number): PointerSource[];
18
18
  export declare function doubleTapActions(x: number, y: number): PointerSource[];
19
19
  export declare function longPressActions(x: number, y: number, duration?: number): PointerSource[];
20
20
  export declare function swipeActions(direction: SwipeDirection, screen: ScreenSize, opts?: SwipeOptions): PointerSource[];
21
+ /**
22
+ * iOS system back gesture: a quick swipe from the left screen edge to past the
23
+ * middle, at mid-height. UINavigationController's interactive pop recognizer
24
+ * needs the touch to start within a few points of the edge.
25
+ */
26
+ export declare function edgeSwipeBackActions(screen: ScreenSize): PointerSource[];
21
27
  /** Multi-finger gesture: each pointer path becomes one W3C input source,
22
28
  * with `time` offsets converted to per-segment move durations. */
23
29
  export declare function gestureActions(gestures: GestureSequence): PointerSource[];
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAExG;;;;;GAKG;AAEH,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEzC,UAAU,aAAa;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;IACrC,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAYD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAEhE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAMtE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,aAAa,EAAE,CAEtF;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,cAAc,EACzB,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,YAAiB,GACtB,aAAa,EAAE,CA2BjB;AAED;mEACmE;AACnE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa,EAAE,CAgBzE"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAExG;;;;;GAKG;AAEH,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEzC,UAAU,aAAa;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;IACrC,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAYD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAEhE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAMtE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,aAAa,EAAE,CAEtF;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,cAAc,EACzB,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,YAAiB,GACtB,aAAa,EAAE,CA2BjB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAQxE;AAED;mEACmE;AACnE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa,EAAE,CAgBzE"}
package/dist/actions.js CHANGED
@@ -52,6 +52,20 @@ export function swipeActions(direction, screen, opts = {}) {
52
52
  up(),
53
53
  ])];
54
54
  }
55
+ /**
56
+ * iOS system back gesture: a quick swipe from the left screen edge to past the
57
+ * middle, at mid-height. UINavigationController's interactive pop recognizer
58
+ * needs the touch to start within a few points of the edge.
59
+ */
60
+ export function edgeSwipeBackActions(screen) {
61
+ const y = screen.height / 2;
62
+ return [pointerSource('finger1', [
63
+ move(2, y),
64
+ down(),
65
+ move(screen.width * 0.6, y, 250),
66
+ up(),
67
+ ])];
68
+ }
55
69
  /** Multi-finger gesture: each pointer path becomes one W3C input source,
56
70
  * with `time` offsets converted to per-segment move durations. */
57
71
  export function gestureActions(gestures) {
@@ -1 +1 @@
1
- {"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAkBA,SAAS,aAAa,CAAC,EAAU,EAAE,OAAoB;IACrD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC;AAChF,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,QAAQ,GAAG,CAAC,EAAa,EAAE,CAC7D,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;AAC9F,MAAM,IAAI,GAAG,GAAc,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;AACnE,MAAM,EAAE,GAAG,GAAc,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;AAC/D,MAAM,KAAK,GAAG,CAAC,QAAgB,EAAa,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAE7E,MAAM,UAAU,UAAU,CAAC,CAAS,EAAE,CAAS;IAC7C,OAAO,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAS,EAAE,CAAS;IACnD,OAAO,CAAC,aAAa,CAAC,SAAS,EAAE;YAC/B,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE;YACnC,KAAK,CAAC,EAAE,CAAC;YACT,IAAI,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE;SACxB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAS,EAAE,CAAS,EAAE,QAAQ,GAAG,GAAG;IACnE,OAAO,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,SAAyB,EACzB,MAAkB,EAClB,OAAqB,EAAE;IAEvB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAChD,MAAM,eAAe,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC;IAC1G,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAC;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC;IAEtC,IAAI,IAAI,GAAG,MAAM,CAAC;IAClB,IAAI,IAAI,GAAG,MAAM,CAAC;IAClB,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,IAAI;YAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;YAAC,MAAM;QAC3C,KAAK,MAAM;YAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;YAAC,MAAM;QAC7C,KAAK,MAAM;YAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;YAAC,MAAM;QAC7C,KAAK,OAAO;YAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;YAAC,MAAM;IAChD,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IAC5E,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAElC,OAAO,CAAC,aAAa,CAAC,SAAS,EAAE;YAC/B,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;YACpB,IAAI,EAAE;YACN,KAAK,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC;YAC1B,KAAK,CAAC,EAAE,CAAC;YACT,EAAE,EAAE;SACL,CAAC,CAAC,CAAC;AACN,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,OAAO,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,MAAM,SAAS,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,IAAI,IAAI,CAAC,CAAC;QACrC,MAAM,OAAO,GAAgB,EAAE,CAAC;QAChC,IAAI,SAAS,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,yBAAyB;QAC5E,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,IAAI,QAAQ,GAAG,SAAS,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC;YACpC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACnE,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACnB,OAAO,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
package/dist/driver.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AllocatedDevice, type AllocationCriteria, type AppInfo, type ConnectionConfig, type DeviceInfo, type DeviceSettings, type GestureSequence, type HardwareButton, type LaunchOptions, type ListDevicesOptions, type MobilewrightDriver, type Orientation, type RecordingOptions, type RecordingResult, type ScreenSize, type ScreenshotOptions, type Session, type SwipeDirection, type SwipeOptions, type TestObserver, type ViewNode } from '@mobilewright/protocol';
1
+ import { type AllocatedDevice, type Bounds, type AllocationCriteria, type AppInfo, type ConnectionConfig, type DeviceInfo, type DeviceSettings, type GestureSequence, type HardwareButton, type LaunchOptions, type ListDevicesOptions, type MobilewrightDriver, type Orientation, type RecordingOptions, type RecordingResult, type ScreenSize, type ScreenshotOptions, type Session, type SwipeDirection, type SwipeOptions, type TestObserver, type ViewNode } from '@mobilewright/protocol';
2
2
  import { type TestingBotDriverOptions } from './options.js';
3
3
  import { type ThrottlePreset, type ThrottleProfile } from './session-commands.js';
4
4
  import { AppiumWebViewBridge } from './webview.js';
@@ -60,13 +60,35 @@ export declare class TestingBotDriver implements MobilewrightDriver {
60
60
  private warnedAboutParallelism;
61
61
  constructor(options?: TestingBotDriverOptions);
62
62
  prepare(): Promise<void>;
63
- allocate(criteria: AllocationCriteria, _takenDeviceIds: ReadonlySet<string>, signal?: AbortSignal): Promise<AllocatedDevice>;
63
+ /**
64
+ * mobilewright's pool re-queues a NoDeviceAvailableError and only retries
65
+ * when one of its own slots is released. With no session held by this run,
66
+ * nothing is ever released, so the test would idle until its allocation
67
+ * timeout even if a device frees up seconds later. Retry here instead, with
68
+ * backoff, until the pool's signal aborts (or allocationTimeout without one).
69
+ */
70
+ allocate(criteria: AllocationCriteria, takenDeviceIds: ReadonlySet<string>, signal?: AbortSignal): Promise<AllocatedDevice>;
71
+ private allocateOnce;
64
72
  release(deviceId: string): Promise<void>;
65
73
  dispose(): Promise<void>;
66
74
  connect(config: ConnectionConfig): Promise<Session>;
67
75
  disconnect(): Promise<void>;
68
76
  /** Create a new hub session for this slot from the connect() config. */
69
77
  private startFreshSession;
78
+ /**
79
+ * Link the running test to its TestingBot session in Playwright's report.
80
+ * A rotated per-test session can land on a different device than the pool
81
+ * slot's first one, so the fixture's device annotations are corrected too.
82
+ */
83
+ private annotateTest;
84
+ /** sessionPerTest: name the session after its test as soon as it starts. */
85
+ private nameSessionAfterTest;
86
+ /**
87
+ * Turn off the backend's wait-for-idle before each page-source read (see the
88
+ * `snapshotTuning` option). Once per session, best-effort: a server that
89
+ * rejects a setting still leaves a working session.
90
+ */
91
+ private applySnapshotTuning;
70
92
  /** Run any Appium/TestingBot execute-script command on the live session. */
71
93
  executeScript<T = unknown>(script: string, ...args: unknown[]): Promise<T>;
72
94
  /** WebDriver session id of the device this instance is connected to. */
@@ -86,16 +108,20 @@ export declare class TestingBotDriver implements MobilewrightDriver {
86
108
  typeText(text: string): Promise<void>;
87
109
  pressKeys(keys: string[]): Promise<void>;
88
110
  clearText(): Promise<void>;
111
+ /** WebDriver id of the focused element (GET /element/active), if any. */
112
+ private activeElementId;
89
113
  /**
90
- * Neither Appium driver can replace a field's value without an element
91
- * reference, and the protocol only hands us the focused field — so clear it
92
- * the same way `clearText()` does, then type the new value.
114
+ * The protocol only hands us the focused field, so clear it the same way
115
+ * `clearText()` does, then type the new value.
93
116
  */
94
117
  fill(text: string): Promise<void>;
95
118
  swipe(direction: SwipeDirection, opts?: SwipeOptions): Promise<void>;
96
119
  gesture(gestures: GestureSequence): Promise<void>;
97
120
  pressButton(button: HardwareButton): Promise<void>;
98
- screenshot(opts?: ScreenshotOptions): Promise<Buffer>;
121
+ screenshot(opts?: ScreenshotOptions & {
122
+ clip?: Bounds;
123
+ scale?: number;
124
+ }): Promise<Buffer>;
99
125
  getScreenSize(): Promise<ScreenSize>;
100
126
  getOrientation(): Promise<Orientation>;
101
127
  setOrientation(orientation: Orientation): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"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"}
package/dist/driver.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import createDebug from 'debug';
3
3
  import { NoDeviceAvailableError, } from '@mobilewright/protocol';
4
- import { doubleTapActions, gestureActions, longPressActions, swipeActions, tapActions, } from './actions.js';
4
+ import { doubleTapActions, edgeSwipeBackActions, gestureActions, longPressActions, swipeActions, tapActions, } from './actions.js';
5
5
  import { AppStorage } from './app-storage.js';
6
6
  import { appsForCriteria, buildCapabilities } from './capabilities.js';
7
7
  import { DeviceCatalog } from './device-catalog.js';
@@ -11,7 +11,10 @@ import { parseChord, toAndroidKeyPress, toW3CKeyActions, typeTextActions } from
11
11
  import { TestingBotTestObserver } from './observer.js';
12
12
  import { requireCredentials, resolveOptions } from './options.js';
13
13
  import { RestApiClient } from './rest-api.js';
14
+ import { transformPng } from './png.js';
14
15
  import { RunLog } from './run-log.js';
16
+ import { attemptName, attemptOutcome, currentTestInfo } from './test-info.js';
17
+ import { exactVerdict } from './observer.js';
15
18
  import { setActiveDriver } from './session-commands.js';
16
19
  import { TunnelManager } from './tunnel.js';
17
20
  import { AppiumWebViewBridge } from './webview.js';
@@ -32,6 +35,11 @@ const ANDROID_BUTTON_KEYCODES = {
32
35
  APP_SWITCH: 187,
33
36
  LOCK: 26, // same as POWER — locks the screen
34
37
  };
38
+ /** Default snapshot tuning per platform (see the `snapshotTuning` option). */
39
+ const SNAPSHOT_TUNING = {
40
+ android: { waitForIdleTimeout: 0 },
41
+ ios: { waitForIdleTimeout: 0, animationCoolOffTimeout: 0 },
42
+ };
35
43
  // XCUITest's mobile: pressButton supports exactly these names on iOS.
36
44
  const IOS_BUTTON_NAMES = {
37
45
  HOME: 'home',
@@ -111,7 +119,36 @@ export class TestingBotDriver {
111
119
  debug('credentials verified against %s (plan %s, parallel %s vm / %s device)', this.options.apiUrl, this.userInfo.plan, this.userInfo.max_concurrent, this.userInfo.max_concurrent_mobile);
112
120
  await this.tunnel.start(this.options);
113
121
  }
114
- async allocate(criteria, _takenDeviceIds, signal) {
122
+ /**
123
+ * mobilewright's pool re-queues a NoDeviceAvailableError and only retries
124
+ * when one of its own slots is released. With no session held by this run,
125
+ * nothing is ever released, so the test would idle until its allocation
126
+ * timeout even if a device frees up seconds later. Retry here instead, with
127
+ * backoff, until the pool's signal aborts (or allocationTimeout without one).
128
+ */
129
+ async allocate(criteria, takenDeviceIds, signal) {
130
+ const deadline = signal ? Infinity : Date.now() + this.options.allocationTimeout;
131
+ for (let attempt = 1;; attempt++) {
132
+ try {
133
+ return await this.allocateOnce(criteria, takenDeviceIds, signal);
134
+ }
135
+ catch (err) {
136
+ if (!(err instanceof NoDeviceAvailableError) || signal?.aborted)
137
+ throw err;
138
+ if (this.allocated.size > 0) {
139
+ debug('no device available; this run holds %d session(s), handing back to the pool', this.allocated.size);
140
+ throw err;
141
+ }
142
+ const delay = Math.min(this.options.allocationRetryDelay * 2 ** (attempt - 1), 30_000);
143
+ if (Date.now() + delay > deadline)
144
+ throw err;
145
+ debug('no device available (%s); retry %d in %dms', err.message, attempt, delay);
146
+ if (!(await sleep(delay, signal)))
147
+ throw err;
148
+ }
149
+ }
150
+ }
151
+ async allocateOnce(criteria, _takenDeviceIds, signal) {
115
152
  requireCredentials(this.options);
116
153
  // takenDeviceIds is unused: every allocate() creates a fresh, exclusive
117
154
  // WebDriver session; physical-device contention is handled server-side.
@@ -197,6 +234,7 @@ export class TestingBotDriver {
197
234
  this.inFlightAllocations -= 1;
198
235
  }
199
236
  const platform = criteria.platform ?? (String(caps['platformName']).toLowerCase() === 'ios' ? 'ios' : 'android');
237
+ await this.applySnapshotTuning(sessionId, platform);
200
238
  const session = {
201
239
  sessionId,
202
240
  platform,
@@ -206,6 +244,17 @@ export class TestingBotDriver {
206
244
  };
207
245
  this.allocated.set(sessionId, session);
208
246
  this.everAllocated.set(sessionId, session);
247
+ if (this.options.sessionPerTest) {
248
+ // Workers rotating sessions on this slot need its device filter, which
249
+ // mobilewright's connect() does not forward.
250
+ this.runLog.append({
251
+ type: 'slot',
252
+ deviceId: sessionId,
253
+ deviceType: criteria.deviceType,
254
+ osVersion: criteria.osVersion,
255
+ deviceNamePattern: criteria.deviceNamePattern,
256
+ });
257
+ }
209
258
  this.keepalive.start(sessionId);
210
259
  debug('allocated session %s (%s %s) — https://testingbot.com/members/tests/%s', sessionId, session.deviceName, session.osVersion, sessionId);
211
260
  return {
@@ -258,6 +307,7 @@ export class TestingBotDriver {
258
307
  await this.hub.get(poolDeviceId, '/orientation', { timeout });
259
308
  this.active = { sessionId: poolDeviceId, platform: config.platform, deviceType: config.deviceType, connectedAt: Date.now() };
260
309
  setActiveDriver(this);
310
+ this.annotateTest(poolDeviceId);
261
311
  debug('connected to session %s', poolDeviceId);
262
312
  return { deviceId: poolDeviceId, platform: config.platform };
263
313
  }
@@ -277,9 +327,16 @@ export class TestingBotDriver {
277
327
  }
278
328
  this.rotatedDeviceIds.add(poolDeviceId);
279
329
  }
280
- sessionId ??= await this.startFreshSession(config);
330
+ let rotated;
331
+ if (!sessionId) {
332
+ const fresh = await this.startFreshSession(config, poolDeviceId);
333
+ sessionId = fresh.sessionId;
334
+ rotated = { model: fresh.model, osVersion: fresh.osVersion };
335
+ }
281
336
  this.active = { sessionId, platform: config.platform, deviceType: config.deviceType, connectedAt: Date.now() };
282
337
  setActiveDriver(this);
338
+ await this.nameSessionAfterTest(sessionId);
339
+ this.annotateTest(sessionId, rotated);
283
340
  debug('connected to session %s (slot %s)', sessionId, poolDeviceId);
284
341
  return { deviceId: poolDeviceId, platform: config.platform };
285
342
  }
@@ -294,12 +351,15 @@ export class TestingBotDriver {
294
351
  // session it cannot attribute gets no verdict at all. Nothing in teardown
295
352
  // — a hub round-trip, a hung request, the worker dying — may come between
296
353
  // the test ending and this line.
354
+ const info = currentTestInfo();
355
+ const outcome = info && attemptOutcome(info);
297
356
  this.runLog.append({
298
357
  type: 'session',
299
358
  sessionId: session.sessionId,
300
359
  startedAt: session.connectedAt,
301
360
  endedAt: Date.now(),
302
361
  workerIndex: workerIndex(),
362
+ ...(outcome ? { test: outcome } : {}),
303
363
  });
304
364
  if (session.throttled) {
305
365
  // A pooled session outlives the test: reset network conditions so they
@@ -308,6 +368,13 @@ export class TestingBotDriver {
308
368
  debug('could not reset throttling on %s: %s', session.sessionId, err);
309
369
  });
310
370
  }
371
+ if (this.options.sessionPerTest && outcome && this.options.testResults !== 'off') {
372
+ // This session hosted exactly this attempt: report its verdict now, while
373
+ // the session is alive, instead of relying on the run-end observer alone.
374
+ await this.api.updateTest(session.sessionId, exactVerdict([outcome])).catch((err) => {
375
+ debug('could not report verdict for %s: %s', session.sessionId, err);
376
+ });
377
+ }
311
378
  if (!this.options.sessionPerTest) {
312
379
  // Soft detach only: the pool re-grants this slot to the next test and
313
380
  // ends the session via release(). A pooled session that hosted exactly
@@ -322,12 +389,17 @@ export class TestingBotDriver {
322
389
  debug('ended per-test session %s', session.sessionId);
323
390
  }
324
391
  /** Create a new hub session for this slot from the connect() config. */
325
- async startFreshSession(config) {
392
+ async startFreshSession(config, poolDeviceId) {
393
+ // mobilewright's connect() passes only platform/deviceId/deviceType, so
394
+ // the slot's deviceName/osVersion filter comes from the coordinator's
395
+ // allocate() record. Values on the config win when a caller provides them.
396
+ const slot = this.runLog.slotFor(poolDeviceId);
397
+ const configName = config.deviceName instanceof RegExp ? config.deviceName.source : config.deviceName;
326
398
  const criteria = {
327
399
  platform: config.platform,
328
- deviceType: config.deviceType,
329
- osVersion: config.osVersion,
330
- deviceNamePattern: config.deviceName instanceof RegExp ? config.deviceName.source : config.deviceName,
400
+ deviceType: config.deviceType ?? slot?.deviceType,
401
+ osVersion: config.osVersion ?? slot?.osVersion,
402
+ deviceNamePattern: configName ?? slot?.deviceNamePattern,
331
403
  };
332
404
  let pinned;
333
405
  if (criteria.deviceType === 'real') {
@@ -347,10 +419,60 @@ export class TestingBotDriver {
347
419
  }
348
420
  }
349
421
  const capabilities = buildCapabilities(criteria, this.options, pinned, appUrls);
350
- const { sessionId } = await this.hub.newSession(capabilities, { timeout: this.options.allocationTimeout });
422
+ const { sessionId, capabilities: caps } = await this.hub.newSession(capabilities, { timeout: this.options.allocationTimeout });
351
423
  this.runLog.append({ type: 'session', sessionId });
424
+ await this.applySnapshotTuning(sessionId, config.platform);
352
425
  debug('started per-test session %s — https://testingbot.com/members/tests/%s', sessionId, sessionId);
353
- return sessionId;
426
+ return {
427
+ sessionId,
428
+ model: pinned?.deviceName ?? str(caps['appium:deviceName'] ?? caps['deviceName']),
429
+ osVersion: pinned?.platformVersion ?? str(caps['appium:platformVersion'] ?? caps['platformVersion']),
430
+ };
431
+ }
432
+ /**
433
+ * Link the running test to its TestingBot session in Playwright's report.
434
+ * A rotated per-test session can land on a different device than the pool
435
+ * slot's first one, so the fixture's device annotations are corrected too.
436
+ */
437
+ annotateTest(sessionId, device) {
438
+ const info = currentTestInfo();
439
+ if (!info)
440
+ return;
441
+ info.annotations.push({ type: 'TestingBot', description: `https://testingbot.com/members/tests/${sessionId}` });
442
+ for (const annotation of info.annotations) {
443
+ if (annotation.type === 'device.model' && device?.model)
444
+ annotation.description = device.model;
445
+ if (annotation.type === 'device.osVersion' && device?.osVersion)
446
+ annotation.description = device.osVersion;
447
+ }
448
+ }
449
+ /** sessionPerTest: name the session after its test as soon as it starts. */
450
+ async nameSessionAfterTest(sessionId) {
451
+ if (this.options.testResults === 'off')
452
+ return;
453
+ const info = currentTestInfo();
454
+ if (!info)
455
+ return;
456
+ await this.api.updateTest(sessionId, { name: attemptName(info) }).catch((err) => {
457
+ debug('could not name session %s: %s', sessionId, err);
458
+ });
459
+ }
460
+ /**
461
+ * Turn off the backend's wait-for-idle before each page-source read (see the
462
+ * `snapshotTuning` option). Once per session, best-effort: a server that
463
+ * rejects a setting still leaves a working session.
464
+ */
465
+ async applySnapshotTuning(sessionId, platform) {
466
+ if (this.options.snapshotTuning === false)
467
+ return;
468
+ const settings = { ...SNAPSHOT_TUNING[platform], ...this.options.snapshotTuning };
469
+ try {
470
+ await this.hub.post(sessionId, '/appium/settings', { settings });
471
+ debug('snapshot tuning applied to %s: %o', sessionId, settings);
472
+ }
473
+ catch (err) {
474
+ debug('snapshot tuning rejected on %s (%s), keeping server defaults', sessionId, err);
475
+ }
354
476
  }
355
477
  // ─── TestingBot-specific session commands ─────────────────────
356
478
  /** Run any Appium/TestingBot execute-script command on the live session. */
@@ -399,7 +521,7 @@ export class TestingBotDriver {
399
521
  const { sessionId, platform } = this.session();
400
522
  const xml = await this.hub.source(sessionId);
401
523
  const { parseHierarchy } = await import('./hierarchy.js');
402
- return parseHierarchy(xml, platform);
524
+ return parseHierarchy(xml, platform, this.options.visibility);
403
525
  }
404
526
  // ─── MobilewrightSession: input ────────────────────────────────
405
527
  async tap(x, y) {
@@ -446,8 +568,21 @@ export class TestingBotDriver {
446
568
  await this.pressKeys(['ctrl+a', 'backspace']);
447
569
  return;
448
570
  }
449
- // iOS has no working select-all chord; delete the focused field's value
450
- // one backspace per character (hierarchy tells us how many).
571
+ // iOS: clear the focused field through its element reference. Backspaces
572
+ // only delete what is left of the caret, and tapping a field (React Native
573
+ // in particular) can put the caret at the start, so key-based deletion
574
+ // leaves the value untouched.
575
+ const active = await this.activeElementId(sessionId);
576
+ if (active) {
577
+ try {
578
+ await this.hub.post(sessionId, `/element/${active}/clear`, {});
579
+ return;
580
+ }
581
+ catch (err) {
582
+ debug('clearText: element clear failed (%s), falling back to backspaces', err);
583
+ }
584
+ }
585
+ // Fallback: one backspace per character of the focused field's value.
451
586
  const { parseHierarchy } = await import('./hierarchy.js');
452
587
  const nodes = parseHierarchy(await this.hub.source(sessionId), 'ios');
453
588
  const focused = findFocused(nodes);
@@ -456,10 +591,20 @@ export class TestingBotDriver {
456
591
  await this.hub.performActions(sessionId, toW3CKeyActions(Array.from({ length }, () => ({ modifiers: [], key: 'backspace' }))));
457
592
  }
458
593
  }
594
+ /** WebDriver id of the focused element (GET /element/active), if any. */
595
+ async activeElementId(sessionId) {
596
+ try {
597
+ const ref = await this.hub.get(sessionId, '/element/active');
598
+ return ref?.['element-6066-11e4-a52e-4f304ffac5c0'] ?? ref?.['ELEMENT'];
599
+ }
600
+ catch (err) {
601
+ debug('no active element (%s)', err);
602
+ return undefined;
603
+ }
604
+ }
459
605
  /**
460
- * Neither Appium driver can replace a field's value without an element
461
- * reference, and the protocol only hands us the focused field — so clear it
462
- * the same way `clearText()` does, then type the new value.
606
+ * The protocol only hands us the focused field, so clear it the same way
607
+ * `clearText()` does, then type the new value.
463
608
  */
464
609
  async fill(text) {
465
610
  await this.clearText();
@@ -480,6 +625,13 @@ export class TestingBotDriver {
480
625
  await this.hub.execute(sessionId, 'mobile: pressKey', [{ keycode: ANDROID_BUTTON_KEYCODES[button] }]);
481
626
  return;
482
627
  }
628
+ if (button === 'BACK') {
629
+ // iOS has no back button; screen.goBack() lands here. Use the system
630
+ // edge-swipe-back gesture that UINavigationController responds to.
631
+ const screen = await this.getScreenSize();
632
+ await this.hub.performActions(sessionId, edgeSwipeBackActions(screen));
633
+ return;
634
+ }
483
635
  const name = IOS_BUTTON_NAMES[button];
484
636
  if (!name) {
485
637
  throw new Error(`TestingBotDriver: hardware button "${button}" is not supported on iOS`);
@@ -490,8 +642,24 @@ export class TestingBotDriver {
490
642
  async screenshot(opts) {
491
643
  const { sessionId } = this.session();
492
644
  // TestingBot/Appium produce PNG; a jpeg format request falls back to PNG.
493
- const base64 = await this.hub.screenshot(sessionId);
494
- const buffer = Buffer.from(base64, 'base64');
645
+ let buffer = Buffer.from(await this.hub.screenshot(sessionId), 'base64');
646
+ const factor = opts?.scale !== undefined && opts.scale > 0 && opts.scale < 1 ? opts.scale : 1;
647
+ if (opts?.clip || factor < 1) {
648
+ // `clip` is in screen points (locator.screenshot() passes element
649
+ // bounds); the PNG is in physical pixels.
650
+ const ratio = opts?.clip ? (await this.getScreenSize()).scale : 1;
651
+ const rect = opts?.clip && {
652
+ x: opts.clip.x * ratio,
653
+ y: opts.clip.y * ratio,
654
+ width: opts.clip.width * ratio,
655
+ height: opts.clip.height * ratio,
656
+ };
657
+ const transformed = transformPng(buffer, rect, factor);
658
+ if (transformed)
659
+ buffer = transformed;
660
+ else
661
+ debug('screenshot: could not crop/scale this PNG, returning the full screen');
662
+ }
495
663
  if (opts?.path)
496
664
  await writeFile(opts.path, buffer);
497
665
  return buffer;
@@ -779,6 +947,16 @@ function describeAllocation(criteria) {
779
947
  return parts.length ? parts.join(', ') : 'the requested device';
780
948
  }
781
949
  /** Values usable directly as appium:app without an upload. */
950
+ /** Resolves true after `ms`, or false as soon as `signal` aborts. */
951
+ function sleep(ms, signal) {
952
+ return new Promise((resolve) => {
953
+ if (signal?.aborted)
954
+ return resolve(false);
955
+ const onAbort = () => { clearTimeout(timer); resolve(false); };
956
+ const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(true); }, ms);
957
+ signal?.addEventListener('abort', onAbort, { once: true });
958
+ });
959
+ }
782
960
  function isRemoteApp(value) {
783
961
  return /^(tb|https?):\/\//i.test(value);
784
962
  }