@testingbot/mobilewright-driver 0.1.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/LICENSE +202 -0
- package/README.md +211 -0
- package/dist/actions.d.ts +25 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +76 -0
- package/dist/actions.js.map +1 -0
- package/dist/app-storage.d.ts +20 -0
- package/dist/app-storage.d.ts.map +1 -0
- package/dist/app-storage.js +68 -0
- package/dist/app-storage.js.map +1 -0
- package/dist/capabilities.d.ts +27 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +120 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/ci.d.ts +8 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +46 -0
- package/dist/ci.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +43 -0
- package/dist/cli.js.map +1 -0
- package/dist/device-catalog.d.ts +37 -0
- package/dist/device-catalog.d.ts.map +1 -0
- package/dist/device-catalog.js +159 -0
- package/dist/device-catalog.js.map +1 -0
- package/dist/driver.d.ts +80 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +646 -0
- package/dist/driver.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +104 -0
- package/dist/errors.js.map +1 -0
- package/dist/hierarchy.d.ts +10 -0
- package/dist/hierarchy.d.ts.map +1 -0
- package/dist/hierarchy.js +103 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +19 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +108 -0
- package/dist/init.js.map +1 -0
- package/dist/keepalive.d.ts +16 -0
- package/dist/keepalive.d.ts.map +1 -0
- package/dist/keepalive.js +56 -0
- package/dist/keepalive.js.map +1 -0
- package/dist/keys.d.ts +32 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +117 -0
- package/dist/keys.js.map +1 -0
- package/dist/observer.d.ts +55 -0
- package/dist/observer.d.ts.map +1 -0
- package/dist/observer.js +148 -0
- package/dist/observer.js.map +1 -0
- package/dist/options.d.ts +154 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +49 -0
- package/dist/options.js.map +1 -0
- package/dist/rest-api.d.ts +67 -0
- package/dist/rest-api.d.ts.map +1 -0
- package/dist/rest-api.js +117 -0
- package/dist/rest-api.js.map +1 -0
- package/dist/run-log.d.ts +56 -0
- package/dist/run-log.d.ts.map +1 -0
- package/dist/run-log.js +72 -0
- package/dist/run-log.js.map +1 -0
- package/dist/tunnel.d.ts +13 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +47 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/webdriver-client.d.ts +43 -0
- package/dist/webdriver-client.d.ts.map +1 -0
- package/dist/webdriver-client.js +109 -0
- package/dist/webdriver-client.js.map +1 -0
- package/dist/webview.d.ts +16 -0
- package/dist/webview.d.ts.map +1 -0
- package/dist/webview.js +109 -0
- package/dist/webview.js.map +1 -0
- package/package.json +69 -0
package/dist/rest-api.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import createDebug from 'debug';
|
|
5
|
+
import { AuthenticationError, RestApiError } from './errors.js';
|
|
6
|
+
import { USER_AGENT } from './version.js';
|
|
7
|
+
const debug = createDebug('testingbot:rest');
|
|
8
|
+
/** Client for api.testingbot.com/v1 (HTTP Basic auth with key:secret). */
|
|
9
|
+
export class RestApiClient {
|
|
10
|
+
apiUrl;
|
|
11
|
+
auth;
|
|
12
|
+
fetchFn;
|
|
13
|
+
constructor(apiUrl, key, secret, fetchFn = fetch) {
|
|
14
|
+
this.apiUrl = apiUrl.replace(/\/+$/, '');
|
|
15
|
+
this.auth = 'Basic ' + Buffer.from(`${key}:${secret}`).toString('base64');
|
|
16
|
+
this.fetchFn = fetchFn;
|
|
17
|
+
}
|
|
18
|
+
/** Validate credentials; used by prepare() to fail fast. */
|
|
19
|
+
async getUser() {
|
|
20
|
+
return await this.request('GET', '/user');
|
|
21
|
+
}
|
|
22
|
+
/** Physical devices. `available` restricts to currently-acquirable ones. */
|
|
23
|
+
async getDevices(available) {
|
|
24
|
+
const path = available ? '/devices/available' : '/devices';
|
|
25
|
+
const result = await this.request('GET', path);
|
|
26
|
+
// The API historically wraps lists as {devices: [...]}; accept both.
|
|
27
|
+
if (Array.isArray(result))
|
|
28
|
+
return result;
|
|
29
|
+
const wrapped = result.devices;
|
|
30
|
+
return wrapped ?? [];
|
|
31
|
+
}
|
|
32
|
+
/** Simulator/emulator (and desktop-browser) combos; mobile rows carry platformName + deviceName + OS version. */
|
|
33
|
+
async getBrowsers() {
|
|
34
|
+
const result = await this.request('GET', '/browsers');
|
|
35
|
+
if (Array.isArray(result))
|
|
36
|
+
return result;
|
|
37
|
+
return (result.browsers) ?? [];
|
|
38
|
+
}
|
|
39
|
+
async getDevice(id) {
|
|
40
|
+
const result = await this.request('GET', `/devices/${id}`);
|
|
41
|
+
return (result.device ?? result);
|
|
42
|
+
}
|
|
43
|
+
/** Upload an app binary; returns the storage entry with its tb:// URL. */
|
|
44
|
+
async uploadApp(filePath) {
|
|
45
|
+
const data = await readFile(filePath);
|
|
46
|
+
debug('uploading %s (%d bytes, sha256=%s)', filePath, data.length, sha256(data).slice(0, 12));
|
|
47
|
+
const form = new FormData();
|
|
48
|
+
form.append('file', new Blob([data]), basename(filePath));
|
|
49
|
+
const response = await this.fetchFn(`${this.apiUrl}/storage`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { Authorization: this.auth, 'User-Agent': USER_AGENT },
|
|
52
|
+
body: form,
|
|
53
|
+
});
|
|
54
|
+
return await this.parse(response, 'POST /storage');
|
|
55
|
+
}
|
|
56
|
+
async getStorageEntry(appUrl) {
|
|
57
|
+
const appKey = appUrl.replace(/^tb:\/\//, '');
|
|
58
|
+
return await this.request('GET', `/storage/${appKey}`);
|
|
59
|
+
}
|
|
60
|
+
/** Update a test by WebDriver session UUID (pass/fail, name, build, ...). */
|
|
61
|
+
async updateTest(sessionId, update) {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (update.success !== undefined)
|
|
64
|
+
params.set('test[success]', update.success ? '1' : '0');
|
|
65
|
+
if (update.name !== undefined)
|
|
66
|
+
params.set('test[name]', update.name);
|
|
67
|
+
if (update.statusMessage !== undefined)
|
|
68
|
+
params.set('test[status_message]', update.statusMessage);
|
|
69
|
+
if (update.build !== undefined)
|
|
70
|
+
params.set('test[build]', update.build);
|
|
71
|
+
if (update.extra !== undefined)
|
|
72
|
+
params.set('test[extra]', update.extra);
|
|
73
|
+
for (const group of update.groups ?? [])
|
|
74
|
+
params.append('groups[]', group);
|
|
75
|
+
await this.request('PUT', `/tests/${sessionId}`, params);
|
|
76
|
+
}
|
|
77
|
+
async getTest(sessionId) {
|
|
78
|
+
return await this.request('GET', `/tests/${sessionId}`);
|
|
79
|
+
}
|
|
80
|
+
/** Force-stop a running session (fallback when DELETE /session fails). */
|
|
81
|
+
async stopTest(sessionId) {
|
|
82
|
+
await this.request('PUT', `/tests/${sessionId}/stop`);
|
|
83
|
+
}
|
|
84
|
+
async request(method, path, body) {
|
|
85
|
+
debug('%s %s', method, path);
|
|
86
|
+
const response = await this.fetchFn(`${this.apiUrl}${path}`, {
|
|
87
|
+
method,
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: this.auth,
|
|
90
|
+
'User-Agent': USER_AGENT,
|
|
91
|
+
...(body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
|
|
92
|
+
},
|
|
93
|
+
body,
|
|
94
|
+
signal: AbortSignal.timeout(30_000),
|
|
95
|
+
});
|
|
96
|
+
return await this.parse(response, `${method} ${path}`);
|
|
97
|
+
}
|
|
98
|
+
async parse(response, context) {
|
|
99
|
+
const text = await response.text();
|
|
100
|
+
if (response.status === 401) {
|
|
101
|
+
throw new AuthenticationError(`TestingBot rejected the credentials (${context}). Check TESTINGBOT_KEY and TESTINGBOT_SECRET.`);
|
|
102
|
+
}
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new RestApiError(`${context} failed (HTTP ${response.status}): ${text.slice(0, 500)}`, response.status);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new RestApiError(`${context} returned non-JSON: ${text.slice(0, 200)}`, response.status);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function sha256(data) {
|
|
115
|
+
return createHash('sha256').update(data).digest('hex');
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=rest-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-api.js","sourceRoot":"","sources":["../src/rest-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny JSONL file shared between the coordinator process (allocator half,
|
|
3
|
+
* observer) and worker processes (session half) of one mobilewright run —
|
|
4
|
+
* needed by sessionPerTest, where workers create WebDriver sessions the
|
|
5
|
+
* coordinator never sees but the observer must still report on. Keyed by
|
|
6
|
+
* cwd, since all processes of a run share it; the coordinator truncates the
|
|
7
|
+
* file in prepare() so stale entries from a previous run cannot leak in.
|
|
8
|
+
*/
|
|
9
|
+
export interface SessionRecord {
|
|
10
|
+
type: 'session';
|
|
11
|
+
sessionId: string;
|
|
12
|
+
/** Wall-clock ms of connect()/disconnect() — present on records written at
|
|
13
|
+
* test end, used to attribute a test's verdict to its session. */
|
|
14
|
+
startedAt?: number;
|
|
15
|
+
endedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AppRecord {
|
|
18
|
+
type: 'app';
|
|
19
|
+
platform: string;
|
|
20
|
+
deviceType?: string;
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RecordingRecord {
|
|
24
|
+
type: 'recording';
|
|
25
|
+
sessionId: string;
|
|
26
|
+
/** Local MP4 path the caller asked stopRecording() to produce. */
|
|
27
|
+
output: string;
|
|
28
|
+
}
|
|
29
|
+
type Record_ = SessionRecord | AppRecord | RecordingRecord;
|
|
30
|
+
export interface MergedSession {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
/** Number of connect/disconnect cycles (= tests) recorded for this session. */
|
|
33
|
+
spans: number;
|
|
34
|
+
startedAt?: number;
|
|
35
|
+
endedAt?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare class RunLog {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
constructor(dir?: string, key?: string);
|
|
40
|
+
truncate(): void;
|
|
41
|
+
append(record: Record_): void;
|
|
42
|
+
read(): Record_[];
|
|
43
|
+
/**
|
|
44
|
+
* Session records merged by id. A pooled session hosts one test per
|
|
45
|
+
* connect/disconnect cycle, so it accumulates one timed record per test:
|
|
46
|
+
* `spans` counts them, and startedAt/endedAt cover their union. A session
|
|
47
|
+
* with exactly one span hosted exactly one test — attributable; more than
|
|
48
|
+
* one span means several tests shared it.
|
|
49
|
+
*/
|
|
50
|
+
sessions(): MergedSession[];
|
|
51
|
+
sessionIds(): string[];
|
|
52
|
+
recordings(): RecordingRecord[];
|
|
53
|
+
appUrlFor(platform: string, deviceType: string | undefined): string | undefined;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=run-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-log.d.ts","sourceRoot":"","sources":["../src/run-log.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB;uEACmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,KAAK,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,OAAO,GAAG,aAAa,GAAG,SAAS,GAAG,eAAe,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,MAAM;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,GAAG,GAAE,MAAiB,EAAE,GAAG,GAAE,MAAsB;IAK/D,QAAQ,IAAI,IAAI;IAQhB,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAQ7B,IAAI,IAAI,OAAO,EAAE;IAWjB;;;;;;OAMG;IACH,QAAQ,IAAI,aAAa,EAAE;IAe3B,UAAU,IAAI,MAAM,EAAE;IAItB,UAAU,IAAI,eAAe,EAAE;IAI/B,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;CAOhF"}
|
package/dist/run-log.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
export class RunLog {
|
|
6
|
+
path;
|
|
7
|
+
constructor(dir = tmpdir(), key = process.cwd()) {
|
|
8
|
+
const hash = createHash('sha256').update(key).digest('hex').slice(0, 12);
|
|
9
|
+
this.path = join(dir, `testingbot-mobilewright-${hash}.jsonl`);
|
|
10
|
+
}
|
|
11
|
+
truncate() {
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(this.path, '');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// best-effort — a failed truncate only risks over-reporting old sessions
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
append(record) {
|
|
20
|
+
try {
|
|
21
|
+
appendFileSync(this.path, JSON.stringify(record) + '\n');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// best-effort — reporting degrades, tests must not fail over this
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
read() {
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(this.path, 'utf-8')
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((line) => JSON.parse(line));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Session records merged by id. A pooled session hosts one test per
|
|
40
|
+
* connect/disconnect cycle, so it accumulates one timed record per test:
|
|
41
|
+
* `spans` counts them, and startedAt/endedAt cover their union. A session
|
|
42
|
+
* with exactly one span hosted exactly one test — attributable; more than
|
|
43
|
+
* one span means several tests shared it.
|
|
44
|
+
*/
|
|
45
|
+
sessions() {
|
|
46
|
+
const byId = new Map();
|
|
47
|
+
for (const record of this.read()) {
|
|
48
|
+
if (record.type !== 'session')
|
|
49
|
+
continue;
|
|
50
|
+
const merged = byId.get(record.sessionId) ?? { sessionId: record.sessionId, spans: 0 };
|
|
51
|
+
if (record.startedAt !== undefined && record.endedAt !== undefined) {
|
|
52
|
+
merged.spans += 1;
|
|
53
|
+
merged.startedAt = Math.min(merged.startedAt ?? record.startedAt, record.startedAt);
|
|
54
|
+
merged.endedAt = Math.max(merged.endedAt ?? record.endedAt, record.endedAt);
|
|
55
|
+
}
|
|
56
|
+
byId.set(record.sessionId, merged);
|
|
57
|
+
}
|
|
58
|
+
return [...byId.values()];
|
|
59
|
+
}
|
|
60
|
+
sessionIds() {
|
|
61
|
+
return this.sessions().map((r) => r.sessionId);
|
|
62
|
+
}
|
|
63
|
+
recordings() {
|
|
64
|
+
return this.read().filter((r) => r.type === 'recording');
|
|
65
|
+
}
|
|
66
|
+
appUrlFor(platform, deviceType) {
|
|
67
|
+
const apps = this.read().filter((r) => r.type === 'app');
|
|
68
|
+
return (apps.find((a) => a.platform === platform && a.deviceType === deviceType) ??
|
|
69
|
+
apps.find((a) => a.platform === platform))?.url;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=run-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-log.js","sourceRoot":"","sources":["../src/run-log.ts"],"names":[],"mappings":"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"}
|
package/dist/tunnel.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedOptions } from './options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Starts/stops a TestingBot Tunnel around the run via the official
|
|
4
|
+
* `testingbot-tunnel-launcher` package — an optional peer dependency, since
|
|
5
|
+
* it needs Java on the machine. Only the coordinator's prepare()/dispose()
|
|
6
|
+
* touch this; workers just put the tunnelIdentifier into their caps.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TunnelManager {
|
|
9
|
+
private running;
|
|
10
|
+
start(options: ResolvedOptions): Promise<void>;
|
|
11
|
+
stop(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAapD;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA4B;IAErC,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAO5B"}
|
package/dist/tunnel.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import createDebug from 'debug';
|
|
2
|
+
const debug = createDebug('testingbot:tunnel');
|
|
3
|
+
/**
|
|
4
|
+
* Starts/stops a TestingBot Tunnel around the run via the official
|
|
5
|
+
* `testingbot-tunnel-launcher` package — an optional peer dependency, since
|
|
6
|
+
* it needs Java on the machine. Only the coordinator's prepare()/dispose()
|
|
7
|
+
* touch this; workers just put the tunnelIdentifier into their caps.
|
|
8
|
+
*/
|
|
9
|
+
export class TunnelManager {
|
|
10
|
+
running;
|
|
11
|
+
async start(options) {
|
|
12
|
+
if (!options.tunnel || this.running)
|
|
13
|
+
return;
|
|
14
|
+
let launcher;
|
|
15
|
+
try {
|
|
16
|
+
const mod = await import('testingbot-tunnel-launcher');
|
|
17
|
+
launcher = mod.default ?? mod;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('The `tunnel` option needs the optional testingbot-tunnel-launcher package (and Java). ' +
|
|
21
|
+
'Install it with: npm install --save-dev testingbot-tunnel-launcher — or start the tunnel ' +
|
|
22
|
+
'yourself and use the `tunnelIdentifier` option instead.');
|
|
23
|
+
}
|
|
24
|
+
const { identifier, ...rest } = options.tunnel;
|
|
25
|
+
const launcherOptions = {
|
|
26
|
+
apiKey: options.key,
|
|
27
|
+
apiSecret: options.secret,
|
|
28
|
+
...(options.tunnelIdentifier ? { tunnelIdentifier: options.tunnelIdentifier } : {}),
|
|
29
|
+
...rest,
|
|
30
|
+
};
|
|
31
|
+
void identifier; // already folded into options.tunnelIdentifier at resolve time
|
|
32
|
+
debug('starting TestingBot Tunnel%s', options.tunnelIdentifier ? ` (identifier ${options.tunnelIdentifier})` : '');
|
|
33
|
+
this.running = await new Promise((resolve, reject) => {
|
|
34
|
+
launcher(launcherOptions, (err, tunnel) => (err ? reject(err) : resolve(tunnel)));
|
|
35
|
+
});
|
|
36
|
+
debug('tunnel ready');
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
const tunnel = this.running;
|
|
40
|
+
if (!tunnel)
|
|
41
|
+
return;
|
|
42
|
+
this.running = undefined;
|
|
43
|
+
await new Promise((resolve) => tunnel.close(resolve));
|
|
44
|
+
debug('tunnel closed');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=tunnel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,OAAO,CAAC;AAGhC,MAAM,KAAK,GAAG,WAAW,CAAC,mBAAmB,CAAC,CAAC;AAW/C;;;;;GAKG;AACH,MAAM,OAAO,aAAa;IAChB,OAAO,CAA4B;IAE3C,KAAK,CAAC,KAAK,CAAC,OAAwB;QAClC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAC5C,IAAI,QAAoB,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAC;YACvD,QAAQ,GAAI,GAAgC,CAAC,OAAO,IAAK,GAA6B,CAAC;QACzF,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,wFAAwF;gBACxF,2FAA2F;gBAC3F,yDAAyD,CAC1D,CAAC;QACJ,CAAC;QAED,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/C,MAAM,eAAe,GAA4B;YAC/C,MAAM,EAAE,OAAO,CAAC,GAAG;YACnB,SAAS,EAAE,OAAO,CAAC,MAAM;YACzB,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnF,GAAG,IAAI;SACR,CAAC;QACF,KAAK,UAAU,CAAC,CAAC,+DAA+D;QAEhF,KAAK,CAAC,8BAA8B,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,OAAO,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACnH,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAClE,QAAQ,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,cAAc,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5D,KAAK,CAAC,eAAe,CAAC,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,OAAO,EAAE,MAAgB,CAAC;AAEvC,8EAA8E;AAC9E,eAAO,MAAM,UAAU,QAAwE,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// Resolves to the package root from both src/ (tests) and dist/ (published).
|
|
3
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
4
|
+
export const VERSION = version;
|
|
5
|
+
/** Sent on every hub and REST request so TestingBot can attribute traffic. */
|
|
6
|
+
export const USER_AGENT = `testingbot-mobilewright-driver/${version} (node ${process.version})`;
|
|
7
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,6EAA6E;AAC7E,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAwB,CAAC;AAE7F,MAAM,CAAC,MAAM,OAAO,GAAW,OAAO,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,UAAU,GAAG,kCAAkC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAG,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type FetchFn = typeof fetch;
|
|
2
|
+
export interface NewSessionResult {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
capabilities: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
interface CommandOptions {
|
|
7
|
+
/** Overrides the client's default per-command timeout. */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Minimal fetch-based W3C WebDriver / Appium client. Deliberately not a
|
|
13
|
+
* full client library: only the commands the driver needs, with uniform
|
|
14
|
+
* timeout, abort, and error mapping.
|
|
15
|
+
*/
|
|
16
|
+
export declare class WebDriverClient {
|
|
17
|
+
private readonly hubUrl;
|
|
18
|
+
private readonly commandTimeout;
|
|
19
|
+
private readonly fetchFn;
|
|
20
|
+
constructor(hubUrl: string, commandTimeout: number, fetchFn?: FetchFn);
|
|
21
|
+
/**
|
|
22
|
+
* POST /session. Exempt from the per-command timeout — device allocation
|
|
23
|
+
* can queue for minutes; the caller bounds it via `opts.timeout`/`signal`.
|
|
24
|
+
*/
|
|
25
|
+
newSession(capabilities: {
|
|
26
|
+
alwaysMatch: Record<string, unknown>;
|
|
27
|
+
firstMatch: Record<string, unknown>[];
|
|
28
|
+
}, opts?: CommandOptions): Promise<NewSessionResult>;
|
|
29
|
+
deleteSession(sessionId: string, opts?: CommandOptions): Promise<void>;
|
|
30
|
+
get<T = unknown>(sessionId: string, path: string, opts?: CommandOptions): Promise<T>;
|
|
31
|
+
post<T = unknown>(sessionId: string, path: string, body: unknown, opts?: CommandOptions): Promise<T>;
|
|
32
|
+
/** Execute a script — used for Appium `mobile:` extension commands. */
|
|
33
|
+
execute<T = unknown>(sessionId: string, script: string, args?: unknown[], opts?: CommandOptions): Promise<T>;
|
|
34
|
+
performActions(sessionId: string, actions: unknown[], opts?: CommandOptions): Promise<void>;
|
|
35
|
+
releaseActions(sessionId: string, opts?: CommandOptions): Promise<void>;
|
|
36
|
+
/** Screenshot as a base64 PNG string. */
|
|
37
|
+
screenshot(sessionId: string, opts?: CommandOptions): Promise<string>;
|
|
38
|
+
/** Page source (Appium XML hierarchy). */
|
|
39
|
+
source(sessionId: string, opts?: CommandOptions): Promise<string>;
|
|
40
|
+
private request;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=webdriver-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webdriver-client.d.ts","sourceRoot":"","sources":["../src/webdriver-client.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,UAAU,cAAc;IACtB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAM5E;;;OAGG;IACG,UAAU,CACd,YAAY,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,EAC7F,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,gBAAgB,CAAC;IAYtB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1E,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxF,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAI9G,uEAAuE;IACjE,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,EAAO,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/F,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjF,yCAAyC;IACnC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/E,0CAA0C;IACpC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;YAI7D,OAAO;CAkDtB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import createDebug from 'debug';
|
|
2
|
+
import { WebDriverError } from './errors.js';
|
|
3
|
+
import { USER_AGENT } from './version.js';
|
|
4
|
+
const debug = createDebug('testingbot:webdriver');
|
|
5
|
+
/**
|
|
6
|
+
* Minimal fetch-based W3C WebDriver / Appium client. Deliberately not a
|
|
7
|
+
* full client library: only the commands the driver needs, with uniform
|
|
8
|
+
* timeout, abort, and error mapping.
|
|
9
|
+
*/
|
|
10
|
+
export class WebDriverClient {
|
|
11
|
+
hubUrl;
|
|
12
|
+
commandTimeout;
|
|
13
|
+
fetchFn;
|
|
14
|
+
constructor(hubUrl, commandTimeout, fetchFn = fetch) {
|
|
15
|
+
this.hubUrl = hubUrl.replace(/\/+$/, '');
|
|
16
|
+
this.commandTimeout = commandTimeout;
|
|
17
|
+
this.fetchFn = fetchFn;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* POST /session. Exempt from the per-command timeout — device allocation
|
|
21
|
+
* can queue for minutes; the caller bounds it via `opts.timeout`/`signal`.
|
|
22
|
+
*/
|
|
23
|
+
async newSession(capabilities, opts = {}) {
|
|
24
|
+
const value = await this.request('POST', '/session', { capabilities }, opts);
|
|
25
|
+
const sessionId = value.sessionId;
|
|
26
|
+
if (!sessionId) {
|
|
27
|
+
throw new WebDriverError('session not created', 'hub response did not include a sessionId', 200);
|
|
28
|
+
}
|
|
29
|
+
return { sessionId, capabilities: value.capabilities ?? {} };
|
|
30
|
+
}
|
|
31
|
+
async deleteSession(sessionId, opts = {}) {
|
|
32
|
+
await this.request('DELETE', `/session/${sessionId}`, undefined, opts);
|
|
33
|
+
}
|
|
34
|
+
async get(sessionId, path, opts = {}) {
|
|
35
|
+
return await this.request('GET', `/session/${sessionId}${path}`, undefined, opts);
|
|
36
|
+
}
|
|
37
|
+
async post(sessionId, path, body, opts = {}) {
|
|
38
|
+
return await this.request('POST', `/session/${sessionId}${path}`, body ?? {}, opts);
|
|
39
|
+
}
|
|
40
|
+
/** Execute a script — used for Appium `mobile:` extension commands. */
|
|
41
|
+
async execute(sessionId, script, args = [], opts = {}) {
|
|
42
|
+
return await this.post(sessionId, '/execute/sync', { script, args }, opts);
|
|
43
|
+
}
|
|
44
|
+
async performActions(sessionId, actions, opts = {}) {
|
|
45
|
+
await this.post(sessionId, '/actions', { actions }, opts);
|
|
46
|
+
}
|
|
47
|
+
async releaseActions(sessionId, opts = {}) {
|
|
48
|
+
await this.request('DELETE', `/session/${sessionId}/actions`, undefined, opts);
|
|
49
|
+
}
|
|
50
|
+
/** Screenshot as a base64 PNG string. */
|
|
51
|
+
async screenshot(sessionId, opts = {}) {
|
|
52
|
+
return await this.get(sessionId, '/screenshot', opts);
|
|
53
|
+
}
|
|
54
|
+
/** Page source (Appium XML hierarchy). */
|
|
55
|
+
async source(sessionId, opts = {}) {
|
|
56
|
+
return await this.get(sessionId, '/source', opts);
|
|
57
|
+
}
|
|
58
|
+
async request(method, path, body, opts) {
|
|
59
|
+
const url = `${this.hubUrl}${path}`;
|
|
60
|
+
const timeout = opts.timeout ?? this.commandTimeout;
|
|
61
|
+
const signal = opts.signal
|
|
62
|
+
? combineSignals(AbortSignal.timeout(timeout), opts.signal)
|
|
63
|
+
: AbortSignal.timeout(timeout);
|
|
64
|
+
debug('%s %s', method, path);
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await this.fetchFn(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': USER_AGENT },
|
|
70
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
76
|
+
throw new WebDriverError('timeout', `${method} ${path} timed out after ${timeout}ms`, 0);
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
let payload;
|
|
81
|
+
const text = await response.text();
|
|
82
|
+
try {
|
|
83
|
+
payload = JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new WebDriverError('invalid response', `${method} ${path} returned non-JSON (HTTP ${response.status}): ${text.slice(0, 500)}`, response.status);
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const value = payload.value;
|
|
90
|
+
throw new WebDriverError(value?.error ?? `http ${response.status}`, value?.message ?? text.slice(0, 500), response.status);
|
|
91
|
+
}
|
|
92
|
+
return payload.value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** AbortSignal.any with a fallback for Node 18/20.0-20.2 (added in 20.3). */
|
|
96
|
+
function combineSignals(a, b) {
|
|
97
|
+
if (typeof AbortSignal.any === 'function')
|
|
98
|
+
return AbortSignal.any([a, b]);
|
|
99
|
+
if (a.aborted)
|
|
100
|
+
return a;
|
|
101
|
+
if (b.aborted)
|
|
102
|
+
return b;
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const onAbort = function () { controller.abort(this.reason); };
|
|
105
|
+
a.addEventListener('abort', onAbort, { once: true });
|
|
106
|
+
b.addEventListener('abort', onAbort, { once: true });
|
|
107
|
+
return controller.signal;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=webdriver-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webdriver-client.js","sourceRoot":"","sources":["../src/webdriver-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WebViewBridge, WebViewInfo, WebViewSession } from '@mobilewright/protocol';
|
|
2
|
+
import type { WebDriverClient } from './webdriver-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* WebView support over Appium's context API. Attaching switches the ONE
|
|
5
|
+
* WebDriver context the session has — native commands issued while a webview
|
|
6
|
+
* session is open target the web layer, which is also how the reference
|
|
7
|
+
* drivers behave. close() switches back to the native context.
|
|
8
|
+
*/
|
|
9
|
+
export declare class AppiumWebViewBridge implements WebViewBridge {
|
|
10
|
+
private readonly hub;
|
|
11
|
+
private readonly sessionId;
|
|
12
|
+
constructor(hub: WebDriverClient, sessionId: () => string);
|
|
13
|
+
listWebViews(): Promise<WebViewInfo[]>;
|
|
14
|
+
attachWebView(id: string): Promise<WebViewSession>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=webview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webview.d.ts","sourceRoot":"","sources":["../src/webview.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACzF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7D;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,GAAG,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,MAAM;IAKnD,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAetC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAMzD"}
|