@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 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
- | `cannot install "..." mid-session` | TestingBot has no mid-session installs — point `installApps` and the driver's `apps` at the same build. |
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
@@ -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 appForCriteria(criteria: Pick<AllocationCriteria, 'platform' | 'deviceType'>, apps: Partial<Record<AppSlot, string>>): string | undefined;
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
- * `appUrl` is the already-uploaded app under test (tb:// or https).
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, appUrl?: string): W3CCapabilities;
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,cAAc,CAC5B,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,YAAY,CAAC,EAC7D,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,GACrC,MAAM,GAAG,SAAS,CAQpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,eAAe,EACxB,MAAM,CAAC,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,MAAM,GACd,eAAe,CA6FjB"}
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"}
@@ -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 appForCriteria(criteria, apps) {
8
+ export function appsForCriteria(criteria, apps) {
9
9
  const platform = criteria.platform;
10
10
  if (!platform)
11
- return undefined;
12
- if (criteria.deviceType) {
13
- const specific = apps[`${platform}-${criteria.deviceType}`];
14
- if (specific)
15
- return specific;
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
- * `appUrl` is the already-uploaded app under test (tb:// or https).
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, appUrl) {
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,
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"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"}
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
@@ -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;AAItH,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AA8CnD;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,kBAAkB;IACzD,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAE5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAE9C,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE;;;wBAGoB;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuC;IACrE,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,kEAAkE;IAClE,OAAO,CAAC,MAAM,CAA4B;IAC1C,+FAA+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;gBAE1C,OAAO,GAAE,uBAA4B;IA6B3C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQxB,QAAQ,CACZ,QAAQ,EAAE,kBAAkB,EAC5B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;IAwGrB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxB,OAAO,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuCnD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgCjC,wEAAwE;YAC1D,iBAAiB;IAgC/B;;;;OAIG;IACG,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5D,gBAAgB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IASvC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB1B,KAAK,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAelD,UAAU,CAAC,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IASrD,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IAoBpC,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAKtC,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7C,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAU9B,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBpC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBvC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,WAAW,CAAC,IAAI,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAM7D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBnC,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrD,aAAa,IAAI,OAAO,CAAC,eAAe,CAAC;IAmB/C,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;CAKd"}
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 { appForCriteria, buildCapabilities } from './capabilities.js';
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
- let appUrl;
132
- const configuredApp = appForCriteria(criteria, this.options.apps);
133
- if (configuredApp) {
134
- appUrl = isRemoteApp(configuredApp)
135
- ? configuredApp
136
- : (await this.storage.ensureUploaded(configuredApp, criteria.deviceType)).app_url;
137
- if (this.options.sessionPerTest && criteria.platform) {
138
- // Workers rotating sessions per test reuse this upload instead of
139
- // re-uploading the binary once per worker process.
140
- this.runLog.append({ type: 'app', platform: criteria.platform, deviceType: criteria.deviceType, url: appUrl });
141
- }
142
- }
143
- const capabilities = buildCapabilities(criteria, this.options, pinned, appUrl);
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 appUrl = criteria.platform ? this.runLog.appUrlFor(criteria.platform, criteria.deviceType) : undefined;
321
- if (!appUrl) {
322
- const configuredApp = appForCriteria(criteria, this.options.apps);
323
- if (configuredApp) {
324
- appUrl = isRemoteApp(configuredApp)
325
- ? configuredApp
326
- : (await this.storage.ensureUploaded(configuredApp, criteria.deviceType)).app_url;
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, appUrl);
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
- await this.hub.execute(sessionId, 'mobile: activateApp', [{ appId: bundleId, bundleId }]);
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 = appForCriteria({ platform: session.platform, deviceType: session.deviceType }, this.options.apps);
529
- if (declared && (isRemoteApp(declared) || await sameFileContents(declared, path))) {
530
- debug('installApp(%s): already installed at allocation via appium:app, skipping', path);
531
- return;
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
- "app declared in the driver's `apps` option and support no later installs. " +
535
- (declared
536
- ? `The session was started with "${declared}" instead — point installApps and apps at the same build.`
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)