@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 +36 -0
- package/README.md +27 -7
- package/dist/actions.d.ts +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +14 -0
- package/dist/actions.js.map +1 -1
- package/dist/driver.d.ts +32 -6
- package/dist/driver.d.ts.map +1 -1
- package/dist/driver.js +195 -17
- package/dist/driver.js.map +1 -1
- package/dist/hierarchy.d.ts +8 -1
- package/dist/hierarchy.d.ts.map +1 -1
- package/dist/hierarchy.js +18 -19
- package/dist/hierarchy.js.map +1 -1
- package/dist/observer.d.ts +11 -1
- package/dist/observer.d.ts.map +1 -1
- package/dist/observer.js +35 -1
- package/dist/observer.js.map +1 -1
- package/dist/options.d.ts +25 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +3 -0
- package/dist/options.js.map +1 -1
- package/dist/png.d.ts +17 -0
- package/dist/png.d.ts.map +1 -0
- package/dist/png.js +201 -0
- package/dist/png.js.map +1 -0
- package/dist/run-log.d.ts +22 -1
- package/dist/run-log.d.ts.map +1 -1
- package/dist/run-log.js +6 -1
- package/dist/run-log.js.map +1 -1
- package/dist/test-info.d.ts +43 -0
- package/dist/test-info.d.ts.map +1 -0
- package/dist/test-info.js +71 -0
- package/dist/test-info.js.map +1 -0
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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
|
|
269
|
-
-
|
|
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
|
|
272
|
-
- Screenshots are always PNG
|
|
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[];
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -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) {
|
package/dist/actions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
|
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
|
-
|
|
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
|
-
*
|
|
91
|
-
*
|
|
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
|
|
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>;
|
package/dist/driver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,cAAc,EAEnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAEhB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,wBAAwB,CAAC;
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
450
|
-
//
|
|
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
|
-
*
|
|
461
|
-
*
|
|
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
|
-
|
|
494
|
-
const
|
|
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
|
}
|