@testingbot/mobilewright-driver 0.1.0 → 0.2.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/README.md +87 -3
- package/dist/capabilities.d.ts +4 -3
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +21 -10
- package/dist/capabilities.js.map +1 -1
- package/dist/driver.d.ts +29 -0
- package/dist/driver.d.ts.map +1 -1
- package/dist/driver.js +138 -32
- package/dist/driver.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/observer.d.ts +18 -0
- package/dist/observer.d.ts.map +1 -1
- package/dist/observer.js +49 -3
- package/dist/observer.js.map +1 -1
- package/dist/options.d.ts +13 -2
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +2 -0
- package/dist/options.js.map +1 -1
- package/dist/rest-api.d.ts +9 -1
- package/dist/rest-api.d.ts.map +1 -1
- package/dist/rest-api.js.map +1 -1
- package/dist/run-log.d.ts +3 -2
- package/dist/run-log.d.ts.map +1 -1
- package/dist/run-log.js +2 -2
- package/dist/run-log.js.map +1 -1
- package/dist/session-commands.d.ts +75 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +119 -0
- package/dist/session-commands.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ export default defineConfig({
|
|
|
41
41
|
// TestingBot sessions start with the app pre-installed — declare it here.
|
|
42
42
|
// Local paths (.apk / .ipa / simulator .zip) upload automatically.
|
|
43
43
|
apps: {
|
|
44
|
+
// A single path, or an array whose first entry is the app under test
|
|
45
|
+
// and the rest install alongside it (e.g. a mock server or test harness).
|
|
44
46
|
android: './build/app.apk',
|
|
45
47
|
'ios-simulator': './build/app-sim.zip',
|
|
46
48
|
'ios-real': './build/app.ipa', // must be test-signed for real devices
|
|
@@ -90,6 +92,7 @@ Regexes and version ranges work on both real and virtual devices: real devices r
|
|
|
90
92
|
|
|
91
93
|
- 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`).
|
|
92
94
|
- Set **`sessionPerTest: true`** for one session, video, and named verdict **per test** — the clearest dashboard, at the cost of device startup on every test.
|
|
95
|
+
- 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.
|
|
93
96
|
- 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.
|
|
94
97
|
|
|
95
98
|
## All options
|
|
@@ -97,7 +100,9 @@ Regexes and version ranges work on both real and virtual devices: real devices r
|
|
|
97
100
|
```ts
|
|
98
101
|
new TestingBotDriver({
|
|
99
102
|
apps: { android: './build/app.apk' }, // app under test, most specific key wins
|
|
100
|
-
// ('ios-real' beats 'ios'); tb:// URLs allowed
|
|
103
|
+
// ('ios-real' beats 'ios'); tb:// URLs allowed.
|
|
104
|
+
// Array = app under test first, helper apps after:
|
|
105
|
+
// android: ['./app.apk', './mock-server.apk']
|
|
101
106
|
key: '...', // default: TESTINGBOT_KEY env
|
|
102
107
|
secret: '...', // default: TESTINGBOT_SECRET env
|
|
103
108
|
sessionPerTest: false, // true = fresh session (and video) per test
|
|
@@ -113,6 +118,8 @@ new TestingBotDriver({
|
|
|
113
118
|
timeZone: 'Europe/Brussels', // device timezone (tz database name)
|
|
114
119
|
geoCountryCode: 'DE', // route device traffic via a proxy in this country
|
|
115
120
|
throttleNetwork: '3G', // or { downloadSpeed, uploadSpeed, latency, loss }
|
|
121
|
+
autoGrantPermissions: true, // Android: auto-grant app permission dialogs
|
|
122
|
+
autoAcceptAlerts: true, // iOS: auto-accept system permission alerts
|
|
116
123
|
screenshots: true, // screenshot at every step (default false)
|
|
117
124
|
video: true, // session video (default true)
|
|
118
125
|
recordLogs: 'strip-parameters',// command logs: true | false | 'strip-parameters'
|
|
@@ -126,6 +133,67 @@ new TestingBotDriver({
|
|
|
126
133
|
});
|
|
127
134
|
```
|
|
128
135
|
|
|
136
|
+
## TestingBot commands inside tests
|
|
137
|
+
|
|
138
|
+
Some TestingBot features are runtime commands rather than session
|
|
139
|
+
capabilities. Import the `testingbot` helper and call them from a test body —
|
|
140
|
+
it targets the device session mobilewright connected for that test:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { test, expect } from '@mobilewright/test';
|
|
144
|
+
import { testingbot } from '@testingbot/mobilewright-driver';
|
|
145
|
+
|
|
146
|
+
test('checkout survives a slow network', async ({ device, screen, bundleId }) => {
|
|
147
|
+
await device.launchApp(bundleId);
|
|
148
|
+
|
|
149
|
+
await testingbot.annotate('starting checkout'); // shows in the session timeline
|
|
150
|
+
await testingbot.throttle('3G');
|
|
151
|
+
await expect(screen.getByText('Order placed')).toBeVisible();
|
|
152
|
+
|
|
153
|
+
console.log(testingbot.dashboardUrl());
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Available commands
|
|
158
|
+
|
|
159
|
+
| Command | TestingBot command | What it does |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `throttle(conditions)` | `tb:throttle` | Network conditions mid-test: `'Edge'`, `'3G'`, `'4G'`, `'airplane'`, `'disable'`, or `{ downloadSpeed, uploadSpeed, latency, loss }` (kb/s, ms). Reset automatically at the end of the test. |
|
|
162
|
+
| `annotate(text)` | `tb:test-context` | Logs a step into the session's command list, so the timeline shows what the test was doing. |
|
|
163
|
+
| `setName(name)` | `tb:test-name` | Names the session on the dashboard. |
|
|
164
|
+
| `setBuild(build)` | `tb:test-build` | Groups the session under a build. |
|
|
165
|
+
| `setTags([...])` | `tb:test-tags` | Tags the session. |
|
|
166
|
+
| `setResult(passed)` | `tb:test-result` | Overrides the pass/fail verdict (the driver reports it automatically — use this only for outcomes mobilewright cannot see). |
|
|
167
|
+
| `updateInfo({...})` | `tb:test-info` | Bulk update: `name`, `build`, `public`, `statusMessage`, `extra`. |
|
|
168
|
+
| `breakpoint()` | `tb:break` | Pauses the session for manual inspection with a live view. Debugging only — never leave it in CI. |
|
|
169
|
+
| `shell(command, args)` | `mobile: shell` | ADB shell on Android (whitelisted subset on physical devices). |
|
|
170
|
+
| `execute(script, ...args)` | any | Escape hatch for any Appium or TestingBot execute-script command. |
|
|
171
|
+
| `sessionId()` / `dashboardUrl()` | — | This test's TestingBot session id and dashboard link. |
|
|
172
|
+
|
|
173
|
+
Notes:
|
|
174
|
+
|
|
175
|
+
- **Throttling is reset automatically** when the test's device session is
|
|
176
|
+
released, so slow-network conditions never leak into the next test that
|
|
177
|
+
reuses the device.
|
|
178
|
+
- Calling a command outside a test body raises an explanatory error rather
|
|
179
|
+
than a null-reference.
|
|
180
|
+
- `tb:intercept` and `tb:network` (request mocking and network logs) are
|
|
181
|
+
Chrome/Edge-only on TestingBot and are therefore not exposed here.
|
|
182
|
+
|
|
183
|
+
### Recipe: name every session after its test
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
test.beforeEach(async ({ device }, testInfo) => {
|
|
187
|
+
await testingbot.setName(testInfo.title);
|
|
188
|
+
await testingbot.setTags([testInfo.project.name]);
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Requesting the `device` fixture is what makes the session exist before the
|
|
193
|
+
hook runs. With `sessionPerTest: true` each session gets exactly one name;
|
|
194
|
+
in pooled mode the last test to run on a session wins (the driver still
|
|
195
|
+
reports per-test verdicts afterwards — see above).
|
|
196
|
+
|
|
129
197
|
## Testing localhost / staging servers (TestingBot Tunnel)
|
|
130
198
|
|
|
131
199
|
Either run a [TestingBot Tunnel](https://testingbot.com/support/tunnel) yourself and pass its identifier:
|
|
@@ -169,8 +237,10 @@ jobs:
|
|
|
169
237
|
| `TestingBot sessions must start with an app` | Declare the app in the driver's `apps` option (per platform / device type). |
|
|
170
238
|
| `No TestingBot real device matches ...` / `No TestingBot simulator matches ...` | The criteria matched nothing in the catalog — check [available devices](https://testingbot.com/support/devices), loosen `osVersion`/`deviceName`. |
|
|
171
239
|
| Allocation waits for minutes | The matching device is busy; TestingBot queues the session until it frees up (bounded by `allocationTimeout`). |
|
|
240
|
+
| `Test timeout ... while setting up "device"` | More `workers` than your plan's parallel limit: TestingBot queues up to 2× the limit server-side and the driver waits out the rest, all inside the test-scoped device fixture. The driver warns with your plan's number — set `workers` to at most that, or raise the mobilewright `timeout`. |
|
|
172
241
|
| `... is a simulator-only build and cannot be installed on a real device` | Build a test-signed `.ipa` for `deviceType: 'real'` projects. |
|
|
173
|
-
| `
|
|
242
|
+
| `"..." is not installed on this device` | The config's `bundleId` is not the package id of the build in `apps` (check with `aapt2 dump packagename app.apk`), or a stale `bundleId`/env override is in play. |
|
|
243
|
+
| `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). |
|
|
174
244
|
| Modifier chords fail on iOS | XCUITest cannot hold modifier keys; chords are Android-only (`clearText()` handles iOS differently). |
|
|
175
245
|
| Sessions show the run summary instead of a test name | The session hosted several tests; use `sessionPerTest: true` for guaranteed per-test naming. |
|
|
176
246
|
| `DEBUG=testingbot:*` | Prints every allocation, hub command, upload, and report the driver performs. |
|
|
@@ -180,11 +250,12 @@ jobs:
|
|
|
180
250
|
| mobilewright concept | TestingBot implementation |
|
|
181
251
|
| --- | --- |
|
|
182
252
|
| device allocation | Appium session on `hub.testingbot.com` (the session UUID is the device id); busy devices queue server-side |
|
|
183
|
-
| app install | uploaded to TestingBot storage at allocation time and passed as `appium:app` — no mid-session installs |
|
|
253
|
+
| app install | uploaded to TestingBot storage at allocation time and passed as `appium:app`, helper apps as `appium:otherApps` (re-signed automatically for real iOS) — no mid-session installs |
|
|
184
254
|
| taps/swipes/gestures | W3C pointer actions |
|
|
185
255
|
| view hierarchy | Appium page source, mapped to mobilewright's `ViewNode` tree |
|
|
186
256
|
| webviews | Appium contexts (`webViewBridge`) |
|
|
187
257
|
| pass/fail reporting | `PUT /v1/tests/:session` via the `TestObserver` hooks |
|
|
258
|
+
| git metadata | commit/branch/author/subject from the run report → session `extra` + branch tag |
|
|
188
259
|
| video | recorded by TestingBot; `startRecording({ output })` downloads the MP4 locally at run end |
|
|
189
260
|
|
|
190
261
|
## Current limitations
|
|
@@ -206,6 +277,19 @@ npm run build
|
|
|
206
277
|
|
|
207
278
|
Live e2e tests against a real TestingBot account live in `e2e/` (see `e2e/mobilewright.config.ts`).
|
|
208
279
|
|
|
280
|
+
### Releasing
|
|
281
|
+
|
|
282
|
+
The version bump, tag and GitHub release are made locally — the TestingBot
|
|
283
|
+
org's IP allow list blocks GitHub-hosted runners from pushing here — and the
|
|
284
|
+
Release workflow then publishes to npm via trusted publishing (OIDC), so no
|
|
285
|
+
npm token exists anywhere:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npm version minor -m "Release v%s" # patch | minor | major
|
|
289
|
+
git push --follow-tags
|
|
290
|
+
gh release create "v$(node -p "require('./package.json').version")" --generate-notes
|
|
291
|
+
```
|
|
292
|
+
|
|
209
293
|
## License
|
|
210
294
|
|
|
211
295
|
Apache-2.0
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -16,12 +16,13 @@ export interface PinnedDevice {
|
|
|
16
16
|
* ('ios-real' beats 'ios'). Returns the raw configured value: a local path
|
|
17
17
|
* or a tb:// / https URL (the caller uploads local paths).
|
|
18
18
|
*/
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function appsForCriteria(criteria: Pick<AllocationCriteria, 'platform' | 'deviceType'>, apps: Partial<Record<AppSlot, string | string[]>>): string[];
|
|
20
20
|
/**
|
|
21
21
|
* Build the W3C new-session payload for a set of allocation criteria.
|
|
22
22
|
* Pure — catalog lookups and app uploads happen in the caller; a pinned
|
|
23
23
|
* device (real-device allocations) overrides name/version wildcards, and
|
|
24
|
-
* `
|
|
24
|
+
* `appUrls` are the already-uploaded apps (tb:// or https): the first is the
|
|
25
|
+
* app under test, any others ride along as `appium:otherApps`.
|
|
25
26
|
*/
|
|
26
|
-
export declare function buildCapabilities(criteria: AllocationCriteria, options: ResolvedOptions, pinned?: PinnedDevice,
|
|
27
|
+
export declare function buildCapabilities(criteria: AllocationCriteria, options: ResolvedOptions, pinned?: PinnedDevice, appUrls?: string[]): W3CCapabilities;
|
|
27
28
|
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACvC;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAKD;;;;GAIG;AACH,wBAAgB,
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACvC;AAED,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAKD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,YAAY,CAAC,EAC7D,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,GAChD,MAAM,EAAE,CAOV;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,eAAe,EACxB,MAAM,CAAC,EAAE,YAAY,EACrB,OAAO,GAAE,MAAM,EAAO,GACrB,eAAe,CAwGjB"}
|
package/dist/capabilities.js
CHANGED
|
@@ -5,24 +5,24 @@ const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/;
|
|
|
5
5
|
* ('ios-real' beats 'ios'). Returns the raw configured value: a local path
|
|
6
6
|
* or a tb:// / https URL (the caller uploads local paths).
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
8
|
+
export function appsForCriteria(criteria, apps) {
|
|
9
9
|
const platform = criteria.platform;
|
|
10
10
|
if (!platform)
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return apps[platform];
|
|
11
|
+
return [];
|
|
12
|
+
const specific = criteria.deviceType ? apps[`${platform}-${criteria.deviceType}`] : undefined;
|
|
13
|
+
const configured = specific ?? apps[platform];
|
|
14
|
+
if (!configured)
|
|
15
|
+
return [];
|
|
16
|
+
return Array.isArray(configured) ? configured.filter(Boolean) : [configured];
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* Build the W3C new-session payload for a set of allocation criteria.
|
|
21
20
|
* Pure — catalog lookups and app uploads happen in the caller; a pinned
|
|
22
21
|
* device (real-device allocations) overrides name/version wildcards, and
|
|
23
|
-
* `
|
|
22
|
+
* `appUrls` are the already-uploaded apps (tb:// or https): the first is the
|
|
23
|
+
* app under test, any others ride along as `appium:otherApps`.
|
|
24
24
|
*/
|
|
25
|
-
export function buildCapabilities(criteria, options, pinned,
|
|
25
|
+
export function buildCapabilities(criteria, options, pinned, appUrls = []) {
|
|
26
26
|
const platform = criteria.platform;
|
|
27
27
|
if (!platform) {
|
|
28
28
|
throw new Error('TestingBotDriver.allocate requires a platform ("ios" or "android"). ' +
|
|
@@ -35,8 +35,13 @@ export function buildCapabilities(criteria, options, pinned, appUrl) {
|
|
|
35
35
|
platformName: platform === 'ios' ? 'iOS' : 'Android',
|
|
36
36
|
'appium:automationName': platform === 'ios' ? 'XCUITest' : 'UiAutomator2',
|
|
37
37
|
};
|
|
38
|
+
const [appUrl, ...otherApps] = appUrls;
|
|
38
39
|
if (appUrl)
|
|
39
40
|
caps['appium:app'] = appUrl;
|
|
41
|
+
// TestingBot installs these alongside the app under test (and re-signs them
|
|
42
|
+
// for real iOS devices).
|
|
43
|
+
if (otherApps.length)
|
|
44
|
+
caps['appium:otherApps'] = otherApps;
|
|
40
45
|
// TestingBot has no app-less sessions: every session must start with an
|
|
41
46
|
// app or a browser. Fail here with guidance instead of a cryptic hub error.
|
|
42
47
|
const escape = options.capabilities;
|
|
@@ -76,6 +81,12 @@ export function buildCapabilities(criteria, options, pinned, appUrl) {
|
|
|
76
81
|
caps['appium:platformVersion'] = criteria.osVersion;
|
|
77
82
|
}
|
|
78
83
|
}
|
|
84
|
+
if (platform === 'android' && options.autoGrantPermissions !== undefined) {
|
|
85
|
+
caps['appium:autoGrantPermissions'] = options.autoGrantPermissions;
|
|
86
|
+
}
|
|
87
|
+
if (platform === 'ios' && options.autoAcceptAlerts !== undefined) {
|
|
88
|
+
caps['appium:autoAcceptAlerts'] = options.autoAcceptAlerts;
|
|
89
|
+
}
|
|
79
90
|
const tbOptions = {
|
|
80
91
|
key: options.key,
|
|
81
92
|
secret: options.secret,
|
package/dist/capabilities.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAgBA,kFAAkF;AAClF,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAE7C;;;;GAIG;AACH,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"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"}
|
package/dist/driver.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
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';
|
|
2
2
|
import { type TestingBotDriverOptions } from './options.js';
|
|
3
|
+
import { type ThrottlePreset, type ThrottleProfile } from './session-commands.js';
|
|
3
4
|
import { AppiumWebViewBridge } from './webview.js';
|
|
4
5
|
/**
|
|
5
6
|
* Mobilewright driver for TestingBot's device cloud.
|
|
@@ -35,6 +36,11 @@ export declare class TestingBotDriver implements MobilewrightDriver {
|
|
|
35
36
|
private active;
|
|
36
37
|
/** sessionPerTest: pool slots whose original session this instance has already probed/used. */
|
|
37
38
|
private readonly rotatedDeviceIds;
|
|
39
|
+
/** Plan limits from GET /v1/user, for the over-parallelism warning. */
|
|
40
|
+
private userInfo;
|
|
41
|
+
/** Allocations currently waiting on POST /session. */
|
|
42
|
+
private inFlightAllocations;
|
|
43
|
+
private warnedAboutParallelism;
|
|
38
44
|
constructor(options?: TestingBotDriverOptions);
|
|
39
45
|
prepare(): Promise<void>;
|
|
40
46
|
allocate(criteria: AllocationCriteria, _takenDeviceIds: ReadonlySet<string>, signal?: AbortSignal): Promise<AllocatedDevice>;
|
|
@@ -44,6 +50,12 @@ export declare class TestingBotDriver implements MobilewrightDriver {
|
|
|
44
50
|
disconnect(): Promise<void>;
|
|
45
51
|
/** Create a new hub session for this slot from the connect() config. */
|
|
46
52
|
private startFreshSession;
|
|
53
|
+
/** Run any Appium/TestingBot execute-script command on the live session. */
|
|
54
|
+
executeScript<T = unknown>(script: string, ...args: unknown[]): Promise<T>;
|
|
55
|
+
/** WebDriver session id of the device this instance is connected to. */
|
|
56
|
+
currentSessionId(): string;
|
|
57
|
+
/** `tb:throttle` — change network conditions mid-test. */
|
|
58
|
+
throttle(conditions: ThrottlePreset | ThrottleProfile): Promise<void>;
|
|
47
59
|
/**
|
|
48
60
|
* Best-effort, fire-and-forget per the protocol: Android animation scales
|
|
49
61
|
* via `mobile: shell` (may be unavailable depending on TestingBot's Appium
|
|
@@ -65,6 +77,15 @@ export declare class TestingBotDriver implements MobilewrightDriver {
|
|
|
65
77
|
getOrientation(): Promise<Orientation>;
|
|
66
78
|
setOrientation(orientation: Orientation): Promise<void>;
|
|
67
79
|
launchApp(bundleId: string, opts?: LaunchOptions): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Appium reports a missing package and a missing launcher activity with the
|
|
82
|
+
* same "Unable to resolve the launchable activity" text. Ask the device
|
|
83
|
+
* which case it is: a bundleId that is simply not installed (the usual
|
|
84
|
+
* cause — the config's `bundleId` does not match the package id of the
|
|
85
|
+
* build in `apps`) deserves a completely different fix than a real
|
|
86
|
+
* launcher-activity problem.
|
|
87
|
+
*/
|
|
88
|
+
private explainLaunchFailure;
|
|
68
89
|
terminateApp(bundleId: string): Promise<void>;
|
|
69
90
|
listApps(): Promise<AppInfo[]>;
|
|
70
91
|
getForegroundApp(): Promise<AppInfo>;
|
|
@@ -75,6 +96,14 @@ export declare class TestingBotDriver implements MobilewrightDriver {
|
|
|
75
96
|
startRecording(opts: RecordingOptions): Promise<void>;
|
|
76
97
|
stopRecording(): Promise<RecordingResult>;
|
|
77
98
|
private session;
|
|
99
|
+
/**
|
|
100
|
+
* Requesting more parallel sessions than the plan allows is not fatal —
|
|
101
|
+
* TestingBot queues the extras — but the wait happens inside mobilewright's
|
|
102
|
+
* test-scoped device fixture, so queued tests can hit the Playwright test
|
|
103
|
+
* timeout with a confusing "timed out setting up device" failure. Warn once
|
|
104
|
+
* with the actual numbers instead.
|
|
105
|
+
*/
|
|
106
|
+
private warnIfOverParallel;
|
|
78
107
|
private unpin;
|
|
79
108
|
}
|
|
80
109
|
//# sourceMappingURL=driver.d.ts.map
|
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;AAehC,OAAO,EAA4D,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"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"}
|
package/dist/driver.js
CHANGED
|
@@ -3,7 +3,7 @@ import createDebug from 'debug';
|
|
|
3
3
|
import { NoDeviceAvailableError, } from '@mobilewright/protocol';
|
|
4
4
|
import { doubleTapActions, gestureActions, longPressActions, swipeActions, tapActions, } from './actions.js';
|
|
5
5
|
import { AppStorage } from './app-storage.js';
|
|
6
|
-
import {
|
|
6
|
+
import { appsForCriteria, buildCapabilities } from './capabilities.js';
|
|
7
7
|
import { DeviceCatalog } from './device-catalog.js';
|
|
8
8
|
import { SessionNotActiveError, toAllocationError, withAllocationContext } from './errors.js';
|
|
9
9
|
import { Keepalive } from './keepalive.js';
|
|
@@ -12,6 +12,7 @@ import { TestingBotTestObserver } from './observer.js';
|
|
|
12
12
|
import { requireCredentials, resolveOptions } from './options.js';
|
|
13
13
|
import { RestApiClient } from './rest-api.js';
|
|
14
14
|
import { RunLog } from './run-log.js';
|
|
15
|
+
import { setActiveDriver } from './session-commands.js';
|
|
15
16
|
import { TunnelManager } from './tunnel.js';
|
|
16
17
|
import { AppiumWebViewBridge } from './webview.js';
|
|
17
18
|
import { WebDriverClient } from './webdriver-client.js';
|
|
@@ -71,6 +72,11 @@ export class TestingBotDriver {
|
|
|
71
72
|
active;
|
|
72
73
|
/** sessionPerTest: pool slots whose original session this instance has already probed/used. */
|
|
73
74
|
rotatedDeviceIds = new Set();
|
|
75
|
+
/** Plan limits from GET /v1/user, for the over-parallelism warning. */
|
|
76
|
+
userInfo;
|
|
77
|
+
/** Allocations currently waiting on POST /session. */
|
|
78
|
+
inFlightAllocations = 0;
|
|
79
|
+
warnedAboutParallelism = false;
|
|
74
80
|
constructor(options = {}) {
|
|
75
81
|
this.options = resolveOptions(options);
|
|
76
82
|
this.hub = new WebDriverClient(this.options.hubUrl, this.options.commandTimeout);
|
|
@@ -93,6 +99,7 @@ export class TestingBotDriver {
|
|
|
93
99
|
return [...byId.values()];
|
|
94
100
|
},
|
|
95
101
|
build: this.options.build,
|
|
102
|
+
extra: this.options.extra,
|
|
96
103
|
recordings: () => this.runLog.recordings(),
|
|
97
104
|
});
|
|
98
105
|
}
|
|
@@ -100,14 +107,15 @@ export class TestingBotDriver {
|
|
|
100
107
|
async prepare() {
|
|
101
108
|
requireCredentials(this.options);
|
|
102
109
|
this.runLog.truncate(); // drop session/app records from any previous run
|
|
103
|
-
await this.api.getUser();
|
|
104
|
-
debug('credentials verified against %s', this.options.apiUrl);
|
|
110
|
+
this.userInfo = await this.api.getUser();
|
|
111
|
+
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);
|
|
105
112
|
await this.tunnel.start(this.options);
|
|
106
113
|
}
|
|
107
114
|
async allocate(criteria, _takenDeviceIds, signal) {
|
|
108
115
|
requireCredentials(this.options);
|
|
109
116
|
// takenDeviceIds is unused: every allocate() creates a fresh, exclusive
|
|
110
117
|
// WebDriver session; physical-device contention is handled server-side.
|
|
118
|
+
this.warnIfOverParallel(criteria);
|
|
111
119
|
let pinned;
|
|
112
120
|
let reservedName;
|
|
113
121
|
if (criteria.deviceId) {
|
|
@@ -128,19 +136,19 @@ export class TestingBotDriver {
|
|
|
128
136
|
}
|
|
129
137
|
// TestingBot sessions must start with an app — resolve and upload the
|
|
130
138
|
// configured one now so it can ride along in the allocation caps.
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
?
|
|
136
|
-
: (await this.storage.ensureUploaded(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const capabilities = buildCapabilities(criteria, this.options, pinned,
|
|
139
|
+
const configuredApps = appsForCriteria(criteria, this.options.apps);
|
|
140
|
+
const appUrls = [];
|
|
141
|
+
for (const app of configuredApps) {
|
|
142
|
+
appUrls.push(isRemoteApp(app)
|
|
143
|
+
? app
|
|
144
|
+
: (await this.storage.ensureUploaded(app, criteria.deviceType)).app_url);
|
|
145
|
+
}
|
|
146
|
+
if (appUrls.length && this.options.sessionPerTest && criteria.platform) {
|
|
147
|
+
// Workers rotating sessions per test reuse these uploads instead of
|
|
148
|
+
// re-uploading the binaries once per worker process.
|
|
149
|
+
this.runLog.append({ type: 'app', platform: criteria.platform, deviceType: criteria.deviceType, urls: appUrls });
|
|
150
|
+
}
|
|
151
|
+
const capabilities = buildCapabilities(criteria, this.options, pinned, appUrls);
|
|
144
152
|
debug('allocating: %o', { ...capabilities.alwaysMatch, 'tb:options': '[redacted]' });
|
|
145
153
|
if (signal?.aborted) {
|
|
146
154
|
throw new NoDeviceAvailableError('allocation aborted before it started');
|
|
@@ -152,6 +160,7 @@ export class TestingBotDriver {
|
|
|
152
160
|
// pool aborts — is handled by deleting the session that slipped through.
|
|
153
161
|
let sessionId;
|
|
154
162
|
let caps;
|
|
163
|
+
this.inFlightAllocations += 1;
|
|
155
164
|
try {
|
|
156
165
|
const controller = new AbortController();
|
|
157
166
|
const onAbort = () => controller.abort();
|
|
@@ -184,6 +193,9 @@ export class TestingBotDriver {
|
|
|
184
193
|
this.unpin(reservedName);
|
|
185
194
|
throw err;
|
|
186
195
|
}
|
|
196
|
+
finally {
|
|
197
|
+
this.inFlightAllocations -= 1;
|
|
198
|
+
}
|
|
187
199
|
const platform = criteria.platform ?? (String(caps['platformName']).toLowerCase() === 'ios' ? 'ios' : 'android');
|
|
188
200
|
const session = {
|
|
189
201
|
sessionId,
|
|
@@ -245,6 +257,7 @@ export class TestingBotDriver {
|
|
|
245
257
|
// Liveness probe — also validates the id belongs to a live TB session.
|
|
246
258
|
await this.hub.get(poolDeviceId, '/orientation', { timeout });
|
|
247
259
|
this.active = { sessionId: poolDeviceId, platform: config.platform, deviceType: config.deviceType, connectedAt: Date.now() };
|
|
260
|
+
setActiveDriver(this);
|
|
248
261
|
debug('connected to session %s', poolDeviceId);
|
|
249
262
|
return { deviceId: poolDeviceId, platform: config.platform };
|
|
250
263
|
}
|
|
@@ -266,14 +279,23 @@ export class TestingBotDriver {
|
|
|
266
279
|
}
|
|
267
280
|
sessionId ??= await this.startFreshSession(config);
|
|
268
281
|
this.active = { sessionId, platform: config.platform, deviceType: config.deviceType, connectedAt: Date.now() };
|
|
282
|
+
setActiveDriver(this);
|
|
269
283
|
debug('connected to session %s (slot %s)', sessionId, poolDeviceId);
|
|
270
284
|
return { deviceId: poolDeviceId, platform: config.platform };
|
|
271
285
|
}
|
|
272
286
|
async disconnect() {
|
|
273
287
|
const session = this.active;
|
|
274
288
|
this.active = undefined;
|
|
289
|
+
setActiveDriver(undefined);
|
|
275
290
|
if (!session)
|
|
276
291
|
return;
|
|
292
|
+
if (session.throttled) {
|
|
293
|
+
// A pooled session outlives the test: reset network conditions so they
|
|
294
|
+
// cannot leak into whichever test gets this slot next.
|
|
295
|
+
await this.hub.execute(session.sessionId, 'tb:throttle', ['disable']).catch((err) => {
|
|
296
|
+
debug('could not reset throttling on %s: %s', session.sessionId, err);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
277
299
|
if (!this.options.sessionPerTest) {
|
|
278
300
|
// Soft detach only: the pool re-grants this slot to the next test and
|
|
279
301
|
// ends the session via release(). Still record this test's interval —
|
|
@@ -317,21 +339,38 @@ export class TestingBotDriver {
|
|
|
317
339
|
pinned = await this.catalog.pickVirtualDevice(criteria);
|
|
318
340
|
}
|
|
319
341
|
// Prefer the app the coordinator already uploaded for this platform.
|
|
320
|
-
let
|
|
321
|
-
if (!
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
?
|
|
326
|
-
: (await this.storage.ensureUploaded(
|
|
342
|
+
let appUrls = criteria.platform ? this.runLog.appUrlsFor(criteria.platform, criteria.deviceType) : undefined;
|
|
343
|
+
if (!appUrls?.length) {
|
|
344
|
+
appUrls = [];
|
|
345
|
+
for (const app of appsForCriteria(criteria, this.options.apps)) {
|
|
346
|
+
appUrls.push(isRemoteApp(app)
|
|
347
|
+
? app
|
|
348
|
+
: (await this.storage.ensureUploaded(app, criteria.deviceType)).app_url);
|
|
327
349
|
}
|
|
328
350
|
}
|
|
329
|
-
const capabilities = buildCapabilities(criteria, this.options, pinned,
|
|
351
|
+
const capabilities = buildCapabilities(criteria, this.options, pinned, appUrls);
|
|
330
352
|
const { sessionId } = await this.hub.newSession(capabilities, { timeout: this.options.allocationTimeout });
|
|
331
353
|
this.runLog.append({ type: 'session', sessionId });
|
|
332
354
|
debug('started per-test session %s — https://testingbot.com/members/tests/%s', sessionId, sessionId);
|
|
333
355
|
return sessionId;
|
|
334
356
|
}
|
|
357
|
+
// ─── TestingBot-specific session commands ─────────────────────
|
|
358
|
+
/** Run any Appium/TestingBot execute-script command on the live session. */
|
|
359
|
+
async executeScript(script, ...args) {
|
|
360
|
+
return await this.hub.execute(this.session().sessionId, script, args);
|
|
361
|
+
}
|
|
362
|
+
/** WebDriver session id of the device this instance is connected to. */
|
|
363
|
+
currentSessionId() {
|
|
364
|
+
return this.session().sessionId;
|
|
365
|
+
}
|
|
366
|
+
/** `tb:throttle` — change network conditions mid-test. */
|
|
367
|
+
async throttle(conditions) {
|
|
368
|
+
const session = this.session();
|
|
369
|
+
await this.hub.execute(session.sessionId, 'tb:throttle', [conditions]);
|
|
370
|
+
const off = conditions === 'disable' || (typeof conditions === 'object' && conditions.disable === true);
|
|
371
|
+
session.throttled = !off;
|
|
372
|
+
debug('throttle %o applied to %s', conditions, session.sessionId);
|
|
373
|
+
}
|
|
335
374
|
// ─── MobilewrightSession: device settings ──────────────────────
|
|
336
375
|
/**
|
|
337
376
|
* Best-effort, fire-and-forget per the protocol: Android animation scales
|
|
@@ -488,7 +527,48 @@ export class TestingBotDriver {
|
|
|
488
527
|
}]);
|
|
489
528
|
return;
|
|
490
529
|
}
|
|
491
|
-
|
|
530
|
+
try {
|
|
531
|
+
await this.hub.execute(sessionId, 'mobile: activateApp', [{ appId: bundleId, bundleId }]);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
if (err instanceof Error && /launchable activity|No activity found|not installed/i.test(err.message)) {
|
|
535
|
+
throw await this.explainLaunchFailure(bundleId, err);
|
|
536
|
+
}
|
|
537
|
+
throw err;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Appium reports a missing package and a missing launcher activity with the
|
|
542
|
+
* same "Unable to resolve the launchable activity" text. Ask the device
|
|
543
|
+
* which case it is: a bundleId that is simply not installed (the usual
|
|
544
|
+
* cause — the config's `bundleId` does not match the package id of the
|
|
545
|
+
* build in `apps`) deserves a completely different fix than a real
|
|
546
|
+
* launcher-activity problem.
|
|
547
|
+
*/
|
|
548
|
+
async explainLaunchFailure(bundleId, err) {
|
|
549
|
+
const session = this.session();
|
|
550
|
+
let installed;
|
|
551
|
+
try {
|
|
552
|
+
installed = await this.hub.execute(session.sessionId, 'mobile: isAppInstalled', [
|
|
553
|
+
{ appId: bundleId, bundleId },
|
|
554
|
+
]);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// Fall through with an unknown install state.
|
|
558
|
+
}
|
|
559
|
+
if (installed === false) {
|
|
560
|
+
const configured = appsForCriteria({ platform: session.platform, deviceType: session.deviceType }, this.options.apps).map((a) => `"${a}"`).join(', ');
|
|
561
|
+
return new Error(`TestingBotDriver: "${bundleId}" is not installed on this device, so it cannot be launched. ` +
|
|
562
|
+
`The session was started with ${configured || 'the app from the driver\'s `apps` option'}` +
|
|
563
|
+
` — make sure \`bundleId\` in your mobilewright config is that build's package id` +
|
|
564
|
+
(session.platform === 'android'
|
|
565
|
+
? ' (check it with: aapt2 dump packagename app.apk).'
|
|
566
|
+
: ' (check it with: unzip -p app.ipa "Payload/*.app/Info.plist" | plutil -extract CFBundleIdentifier raw -).') +
|
|
567
|
+
`\n${err.message}`);
|
|
568
|
+
}
|
|
569
|
+
return new Error(`TestingBotDriver could not launch "${bundleId}": the app is installed but has no resolvable ` +
|
|
570
|
+
"launcher activity. Pass one explicitly — device.launchApp(bundleId, { activity: '.MainActivity' }) — " +
|
|
571
|
+
'or build the app with a MAIN/LAUNCHER intent filter.\n' + err.message);
|
|
492
572
|
}
|
|
493
573
|
async terminateApp(bundleId) {
|
|
494
574
|
const { sessionId } = this.session();
|
|
@@ -525,15 +605,18 @@ export class TestingBotDriver {
|
|
|
525
605
|
// at allocation time via appium:app (declared in the driver's `apps`
|
|
526
606
|
// option). This call only verifies the request matches that app.
|
|
527
607
|
const session = this.session();
|
|
528
|
-
const declared =
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
608
|
+
const declared = appsForCriteria({ platform: session.platform, deviceType: session.deviceType }, this.options.apps);
|
|
609
|
+
for (const app of declared) {
|
|
610
|
+
if (isRemoteApp(app) || await sameFileContents(app, path)) {
|
|
611
|
+
debug('installApp(%s): already installed at allocation via appium:app/otherApps, skipping', path);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
532
614
|
}
|
|
533
615
|
throw new Error(`TestingBotDriver cannot install "${path}" mid-session: TestingBot sessions start with the ` +
|
|
534
|
-
"
|
|
535
|
-
(declared
|
|
536
|
-
? `The session was started with "${
|
|
616
|
+
"apps declared in the driver's `apps` option and support no later installs. " +
|
|
617
|
+
(declared.length
|
|
618
|
+
? `The session was started with ${declared.map((a) => `"${a}"`).join(', ')} instead — list every app ` +
|
|
619
|
+
'you install in both `installApps` and the driver\'s `apps` option (first entry = app under test).'
|
|
537
620
|
: "Declare it when constructing the driver: new TestingBotDriver({ apps: { " +
|
|
538
621
|
`'${session.platform}${session.deviceType ? `-${session.deviceType}` : ''}': '${path}' } }).`));
|
|
539
622
|
}
|
|
@@ -595,6 +678,29 @@ export class TestingBotDriver {
|
|
|
595
678
|
throw new SessionNotActiveError();
|
|
596
679
|
return this.active;
|
|
597
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Requesting more parallel sessions than the plan allows is not fatal —
|
|
683
|
+
* TestingBot queues the extras — but the wait happens inside mobilewright's
|
|
684
|
+
* test-scoped device fixture, so queued tests can hit the Playwright test
|
|
685
|
+
* timeout with a confusing "timed out setting up device" failure. Warn once
|
|
686
|
+
* with the actual numbers instead.
|
|
687
|
+
*/
|
|
688
|
+
warnIfOverParallel(criteria) {
|
|
689
|
+
if (this.warnedAboutParallelism || !this.userInfo)
|
|
690
|
+
return;
|
|
691
|
+
const isReal = criteria.deviceType === 'real';
|
|
692
|
+
const limit = isReal ? this.userInfo.max_concurrent_mobile : this.userInfo.max_concurrent;
|
|
693
|
+
if (typeof limit !== 'number' || limit <= 0)
|
|
694
|
+
return;
|
|
695
|
+
const wanted = this.allocated.size + this.inFlightAllocations + 1;
|
|
696
|
+
if (wanted > limit) {
|
|
697
|
+
this.warnedAboutParallelism = true;
|
|
698
|
+
console.warn(`TestingBotDriver: this run wants ${wanted}+ parallel ${isReal ? 'device' : 'VM'} sessions but the ` +
|
|
699
|
+
`plan allows ${limit} (TestingBot queues up to ${limit * 2} more server-side; the rest wait for a ` +
|
|
700
|
+
`freed slot). The wait counts against mobilewright's test timeout — set workers: ${limit} (or fewer) ` +
|
|
701
|
+
'in mobilewright.config.ts, or raise the test timeout to cover the expected queue time.');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
598
704
|
unpin(name) {
|
|
599
705
|
const count = this.pinCounts.get(name) ?? 0;
|
|
600
706
|
if (count <= 1)
|