@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.
Files changed (87) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +211 -0
  3. package/dist/actions.d.ts +25 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +76 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/app-storage.d.ts +20 -0
  8. package/dist/app-storage.d.ts.map +1 -0
  9. package/dist/app-storage.js +68 -0
  10. package/dist/app-storage.js.map +1 -0
  11. package/dist/capabilities.d.ts +27 -0
  12. package/dist/capabilities.d.ts.map +1 -0
  13. package/dist/capabilities.js +120 -0
  14. package/dist/capabilities.js.map +1 -0
  15. package/dist/ci.d.ts +8 -0
  16. package/dist/ci.d.ts.map +1 -0
  17. package/dist/ci.js +46 -0
  18. package/dist/ci.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +43 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/device-catalog.d.ts +37 -0
  24. package/dist/device-catalog.d.ts.map +1 -0
  25. package/dist/device-catalog.js +159 -0
  26. package/dist/device-catalog.js.map +1 -0
  27. package/dist/driver.d.ts +80 -0
  28. package/dist/driver.d.ts.map +1 -0
  29. package/dist/driver.js +646 -0
  30. package/dist/driver.js.map +1 -0
  31. package/dist/errors.d.ts +41 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +104 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/hierarchy.d.ts +10 -0
  36. package/dist/hierarchy.d.ts.map +1 -0
  37. package/dist/hierarchy.js +103 -0
  38. package/dist/hierarchy.js.map +1 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +3 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +19 -0
  44. package/dist/init.d.ts.map +1 -0
  45. package/dist/init.js +108 -0
  46. package/dist/init.js.map +1 -0
  47. package/dist/keepalive.d.ts +16 -0
  48. package/dist/keepalive.d.ts.map +1 -0
  49. package/dist/keepalive.js +56 -0
  50. package/dist/keepalive.js.map +1 -0
  51. package/dist/keys.d.ts +32 -0
  52. package/dist/keys.d.ts.map +1 -0
  53. package/dist/keys.js +117 -0
  54. package/dist/keys.js.map +1 -0
  55. package/dist/observer.d.ts +55 -0
  56. package/dist/observer.d.ts.map +1 -0
  57. package/dist/observer.js +148 -0
  58. package/dist/observer.js.map +1 -0
  59. package/dist/options.d.ts +154 -0
  60. package/dist/options.d.ts.map +1 -0
  61. package/dist/options.js +49 -0
  62. package/dist/options.js.map +1 -0
  63. package/dist/rest-api.d.ts +67 -0
  64. package/dist/rest-api.d.ts.map +1 -0
  65. package/dist/rest-api.js +117 -0
  66. package/dist/rest-api.js.map +1 -0
  67. package/dist/run-log.d.ts +56 -0
  68. package/dist/run-log.d.ts.map +1 -0
  69. package/dist/run-log.js +72 -0
  70. package/dist/run-log.js.map +1 -0
  71. package/dist/tunnel.d.ts +13 -0
  72. package/dist/tunnel.d.ts.map +1 -0
  73. package/dist/tunnel.js +47 -0
  74. package/dist/tunnel.js.map +1 -0
  75. package/dist/version.d.ts +4 -0
  76. package/dist/version.d.ts.map +1 -0
  77. package/dist/version.js +7 -0
  78. package/dist/version.js.map +1 -0
  79. package/dist/webdriver-client.d.ts +43 -0
  80. package/dist/webdriver-client.d.ts.map +1 -0
  81. package/dist/webdriver-client.js +109 -0
  82. package/dist/webdriver-client.js.map +1 -0
  83. package/dist/webview.d.ts +16 -0
  84. package/dist/webview.d.ts.map +1 -0
  85. package/dist/webview.js +109 -0
  86. package/dist/webview.js.map +1 -0
  87. package/package.json +69 -0
package/dist/keys.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Key-chord handling for pressKeys()/clearText(). Chords look like
3
+ * "ctrl+a", "backspace", "meta+shift+z".
4
+ *
5
+ * Android: translated to `mobile: pressKey` keycodes + metastate.
6
+ * iOS: translated to W3C key action sequences with WebDriver unicode keys.
7
+ */
8
+ export function parseChord(chord) {
9
+ // A trailing '+' segment means the literal plus key ("ctrl++", "+").
10
+ const literalPlus = chord === '+' || chord.endsWith('++') || chord.endsWith('+ ');
11
+ const parts = chord.split('+').map((p) => p.trim()).filter(Boolean);
12
+ const key = literalPlus ? '+' : (parts.pop() ?? '');
13
+ return { modifiers: parts.map((m) => normalizeModifier(m)), key: key.toLowerCase() };
14
+ }
15
+ function normalizeModifier(mod) {
16
+ const m = mod.toLowerCase();
17
+ if (m === 'cmd' || m === 'command' || m === 'meta')
18
+ return 'meta';
19
+ if (m === 'ctrl' || m === 'control')
20
+ return 'ctrl';
21
+ if (m === 'alt' || m === 'option')
22
+ return 'alt';
23
+ return m;
24
+ }
25
+ // ─── Android (mobile: pressKey) ─────────────────────────────────
26
+ const ANDROID_KEYCODES = {
27
+ home: 3,
28
+ back: 4,
29
+ call: 5,
30
+ endcall: 6,
31
+ up: 19, down: 20, left: 21, right: 22,
32
+ volumeup: 24, volumedown: 25,
33
+ power: 26,
34
+ camera: 27,
35
+ tab: 61,
36
+ space: 62,
37
+ enter: 66,
38
+ backspace: 67,
39
+ delete: 112,
40
+ escape: 111,
41
+ menu: 82,
42
+ search: 84,
43
+ pageup: 92, pagedown: 93,
44
+ appswitch: 187,
45
+ };
46
+ const ANDROID_META = {
47
+ shift: 1, // META_SHIFT_ON
48
+ alt: 2, // META_ALT_ON
49
+ ctrl: 4096, // META_CTRL_ON
50
+ meta: 65536, // META_META_ON
51
+ };
52
+ export function toAndroidKeyPress(chord) {
53
+ let keycode = ANDROID_KEYCODES[chord.key];
54
+ if (keycode === undefined && /^[a-z]$/.test(chord.key)) {
55
+ keycode = 29 + (chord.key.charCodeAt(0) - 97); // KEYCODE_A = 29
56
+ }
57
+ if (keycode === undefined && /^[0-9]$/.test(chord.key)) {
58
+ keycode = 7 + (chord.key.charCodeAt(0) - 48); // KEYCODE_0 = 7
59
+ }
60
+ if (keycode === undefined) {
61
+ throw new Error(`TestingBotDriver: unsupported key "${chord.key}" on Android`);
62
+ }
63
+ const metastate = chord.modifiers.reduce((acc, mod) => {
64
+ const bit = ANDROID_META[mod];
65
+ if (bit === undefined)
66
+ throw new Error(`TestingBotDriver: unsupported modifier "${mod}" on Android`);
67
+ return acc | bit;
68
+ }, 0);
69
+ return metastate ? { keycode, metastate } : { keycode };
70
+ }
71
+ // ─── iOS (W3C key actions) ──────────────────────────────────────
72
+ const W3C_KEYS = {
73
+ shift: '\uE008',
74
+ ctrl: '\uE009',
75
+ alt: '\uE00A',
76
+ meta: '\uE03D',
77
+ enter: '\uE007',
78
+ tab: '\uE004',
79
+ space: ' ',
80
+ backspace: '\uE003',
81
+ delete: '\uE017',
82
+ escape: '\uE00C',
83
+ up: '\uE013', down: '\uE015', left: '\uE012', right: '\uE014',
84
+ pageup: '\uE00E', pagedown: '\uE00F',
85
+ home: '\uE011', end: '\uE010',
86
+ };
87
+ /** Build one W3C key-input source pressing the given chords in order. */
88
+ export function toW3CKeyActions(chords) {
89
+ const actions = [];
90
+ for (const chord of chords) {
91
+ const modifierKeys = chord.modifiers.map((mod) => {
92
+ const key = W3C_KEYS[mod];
93
+ if (!key)
94
+ throw new Error(`TestingBotDriver: unsupported modifier "${mod}"`);
95
+ return key;
96
+ });
97
+ const key = W3C_KEYS[chord.key] ?? (chord.key.length === 1 ? chord.key : undefined);
98
+ if (key === undefined) {
99
+ throw new Error(`TestingBotDriver: unsupported key "${chord.key}"`);
100
+ }
101
+ for (const mod of modifierKeys)
102
+ actions.push({ type: 'keyDown', value: mod });
103
+ actions.push({ type: 'keyDown', value: key }, { type: 'keyUp', value: key });
104
+ for (const mod of [...modifierKeys].reverse())
105
+ actions.push({ type: 'keyUp', value: mod });
106
+ }
107
+ return [{ type: 'key', id: 'keyboard', actions }];
108
+ }
109
+ /** Type plain text as a W3C key-input source (per-character down/up). */
110
+ export function typeTextActions(text) {
111
+ const actions = [];
112
+ for (const ch of text) {
113
+ actions.push({ type: 'keyDown', value: ch }, { type: 'keyUp', value: ch });
114
+ }
115
+ return [{ type: 'key', id: 'keyboard', actions }];
116
+ }
117
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,qEAAqE;IACrE,MAAM,WAAW,GAAG,KAAK,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAClF,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;AACvF,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC;IAClE,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACnD,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAChD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,mEAAmE;AAEnE,MAAM,gBAAgB,GAA2B;IAC/C,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE;IACrC,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE;IAC5B,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,GAAG,EAAE,EAAE;IACP,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,EAAE;IACT,SAAS,EAAE,EAAE;IACb,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,GAAG;IACX,IAAI,EAAE,EAAE;IACR,MAAM,EAAE,EAAE;IACV,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;IACxB,SAAS,EAAE,GAAG;CACf,CAAC;AAEF,MAAM,YAAY,GAA2B;IAC3C,KAAK,EAAE,CAAC,EAAO,gBAAgB;IAC/B,GAAG,EAAE,CAAC,EAAS,cAAc;IAC7B,IAAI,EAAE,IAAI,EAAK,eAAe;IAC9B,IAAI,EAAE,KAAK,EAAI,eAAe;CAC/B,CAAC;AAOF,MAAM,UAAU,iBAAiB,CAAC,KAAY;IAC5C,IAAI,OAAO,GAAuB,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,iBAAiB;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,gBAAgB;IAChE,CAAC;IACD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,KAAK,CAAC,GAAG,cAAc,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACpD,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,GAAG,cAAc,CAAC,CAAC;QACrG,OAAO,GAAG,GAAG,GAAG,CAAC;IACnB,CAAC,EAAE,CAAC,CAAC,CAAC;IACN,OAAO,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC;AAED,mEAAmE;AAEnE,MAAM,QAAQ,GAA2B;IACvC,KAAK,EAAE,QAAQ;IACf,IAAI,EAAE,QAAQ;IACd,GAAG,EAAE,QAAQ;IACb,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,GAAG,EAAE,QAAQ;IACb,KAAK,EAAE,GAAG;IACV,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ;IAC7D,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ;IACpC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ;CAC9B,CAAC;AAIF,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,GAAG,GAAG,CAAC,CAAC;YAC7E,OAAO,GAAG,CAAC;QACb,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACpF,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,sCAAsC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;QACtE,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,YAAY;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9E,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7E,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { RunResultInfo, TestInfo, TestObserver, TestResultInfo, TestRunInfo } from '@mobilewright/protocol';
2
+ import type { RestApiClient } from './rest-api.js';
3
+ export interface ReportableSession {
4
+ sessionId: string;
5
+ /** Connect/disconnect cycles (= tests) recorded for this session. */
6
+ spans: number;
7
+ /** Wall-clock bounds of the recorded cycles, when any exist. */
8
+ startedAt?: number;
9
+ endedAt?: number;
10
+ }
11
+ export interface ObserverContext {
12
+ /** Every TestingBot session of this run (allocated + worker-rotated). */
13
+ sessions(): ReportableSession[];
14
+ build?: string;
15
+ /** Local-video requests made via startRecording({ output }). */
16
+ recordings?(): {
17
+ sessionId: string;
18
+ output: string;
19
+ }[];
20
+ }
21
+ /**
22
+ * Reports outcomes to TestingBot. Sessions whose wall-clock interval maps to
23
+ * exactly one test (sessionPerTest mode) get that test's name and verdict;
24
+ * everything else — reused pool sessions hosting many tests, or ambiguous
25
+ * overlaps under high concurrency — gets the run-level verdict. Strictly
26
+ * best-effort: reporting failures never fail the test run.
27
+ */
28
+ export declare class TestingBotTestObserver implements TestObserver {
29
+ private readonly api;
30
+ private readonly context;
31
+ private totalTests;
32
+ /** Final outcome per test id — onTestEnd fires once per ATTEMPT, and a
33
+ * later retry overwrites the earlier attempt's result. */
34
+ private readonly outcomes;
35
+ constructor(api: RestApiClient, context: ObserverContext);
36
+ onRunStart(run: TestRunInfo): void;
37
+ onTestEnd(test: TestInfo, result: TestResultInfo): void;
38
+ onRunEnd(result: RunResultInfo): Promise<void>;
39
+ /**
40
+ * Fetch session videos requested via startRecording({ output }). The MP4
41
+ * finalizes only after the session ends, so this waits (bounded) for the
42
+ * asset to appear. Best-effort like all reporting.
43
+ */
44
+ private downloadRecordings;
45
+ /**
46
+ * The single test that ran inside this session, when unambiguous.
47
+ * Direction matters: connect() and disconnect() happen INSIDE the test, so
48
+ * the session interval is a sub-interval of its test's — but a test's
49
+ * reported duration also covers fixture setup (device allocation), so the
50
+ * test interval is much wider than the session's. The session midpoint must
51
+ * therefore be looked up in the test intervals, never the other way around.
52
+ */
53
+ private testForSession;
54
+ }
55
+ //# sourceMappingURL=observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.d.ts","sourceRoot":"","sources":["../src/observer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAInD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,IAAI,iBAAiB,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,UAAU,CAAC,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxD;AAWD;;;;;;GAMG;AACH,qBAAa,sBAAuB,YAAW,YAAY;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,UAAU,CAAK;IACvB;+DAC2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;gBAE/C,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe;IAKxD,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAKlC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAajD,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CpD;;;;OAIG;YACW,kBAAkB;IAoChC;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;CAgBvB"}
@@ -0,0 +1,148 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import createDebug from 'debug';
3
+ const debug = createDebug('testingbot:observer');
4
+ /**
5
+ * Reports outcomes to TestingBot. Sessions whose wall-clock interval maps to
6
+ * exactly one test (sessionPerTest mode) get that test's name and verdict;
7
+ * everything else — reused pool sessions hosting many tests, or ambiguous
8
+ * overlaps under high concurrency — gets the run-level verdict. Strictly
9
+ * best-effort: reporting failures never fail the test run.
10
+ */
11
+ export class TestingBotTestObserver {
12
+ api;
13
+ context;
14
+ totalTests = 0;
15
+ /** Final outcome per test id — onTestEnd fires once per ATTEMPT, and a
16
+ * later retry overwrites the earlier attempt's result. */
17
+ outcomes = new Map();
18
+ constructor(api, context) {
19
+ this.api = api;
20
+ this.context = context;
21
+ }
22
+ onRunStart(run) {
23
+ this.totalTests = run.totalTests;
24
+ debug('run started: %d tests', run.totalTests);
25
+ }
26
+ onTestEnd(test, result) {
27
+ const failed = result.status === 'failed' || result.status === 'timedOut' || result.status === 'interrupted';
28
+ const error = result.errors[0]?.split('\n')[0] ?? result.status;
29
+ const endedAt = Date.now();
30
+ this.outcomes.set(test.id, {
31
+ title: test.title,
32
+ failed,
33
+ summary: failed ? `${test.title}: ${error}`.slice(0, 200) : undefined,
34
+ startedAt: endedAt - result.duration,
35
+ endedAt,
36
+ });
37
+ }
38
+ async onRunEnd(result) {
39
+ if (this.outcomes.size === 0) {
40
+ // No test finished (e.g. "mobilewright test --list" fires onRunStart
41
+ // and onRunEnd without any tests running) — don't report, especially
42
+ // not to stale run-file sessions. Skipped tests still produce outcomes.
43
+ return;
44
+ }
45
+ const success = result.status === 'passed';
46
+ const outcomes = [...this.outcomes.values()];
47
+ const failures = outcomes.filter((o) => o.failed).map((o) => o.summary).filter(Boolean).slice(0, 10);
48
+ const runMessage = success
49
+ ? `${outcomes.length}/${this.totalTests} tests passed`
50
+ : `${failures.length} of ${outcomes.length} tests failed. ${failures.join(' | ')}`.slice(0, 500);
51
+ const sessions = this.context.sessions();
52
+ debug('run ended (%s), reporting to %d session(s)', result.status, sessions.length);
53
+ for (const session of sessions) {
54
+ const test = this.testForSession(session, outcomes);
55
+ try {
56
+ await this.api.updateTest(session.sessionId, test
57
+ ? {
58
+ success: !test.failed,
59
+ name: test.title,
60
+ statusMessage: test.failed ? (test.summary ?? 'failed') : 'passed',
61
+ build: this.context.build,
62
+ groups: ['mobilewright'],
63
+ }
64
+ : {
65
+ success,
66
+ statusMessage: runMessage,
67
+ build: this.context.build,
68
+ groups: ['mobilewright'],
69
+ });
70
+ debug('reported %s -> %s', session.sessionId, test ? `test "${test.title}"` : `run success=${success}`);
71
+ }
72
+ catch (err) {
73
+ // Never fail the run over reporting.
74
+ console.warn(`TestingBotDriver: could not report result for session ${session.sessionId}: ${String(err)}`);
75
+ }
76
+ }
77
+ await this.downloadRecordings();
78
+ }
79
+ /**
80
+ * Fetch session videos requested via startRecording({ output }). The MP4
81
+ * finalizes only after the session ends, so this waits (bounded) for the
82
+ * asset to appear. Best-effort like all reporting.
83
+ */
84
+ async downloadRecordings() {
85
+ const requests = this.context.recordings?.() ?? [];
86
+ const deadline = Date.now() + 120_000;
87
+ for (const request of requests) {
88
+ try {
89
+ let videoUrl;
90
+ for (;;) {
91
+ const test = await this.api.getTest(request.sessionId);
92
+ if (test.video === false) {
93
+ // Video was disabled for this session — waiting will not help.
94
+ break;
95
+ }
96
+ // assets_available is the API's "processing finished" signal; a
97
+ // populated video URL alone also suffices (the URL is presigned,
98
+ // so it is fetched right away below).
99
+ if ((test.assets_available ?? true) && typeof test.video === 'string' && test.video) {
100
+ videoUrl = test.video;
101
+ break;
102
+ }
103
+ if (Date.now() >= deadline)
104
+ break;
105
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
106
+ }
107
+ if (!videoUrl) {
108
+ console.warn(`TestingBotDriver: video for session ${request.sessionId} is unavailable (disabled or not ready in time); skipping ${request.output}`);
109
+ continue;
110
+ }
111
+ const response = await fetch(videoUrl, { signal: AbortSignal.timeout(60_000) });
112
+ if (!response.ok)
113
+ throw new Error(`HTTP ${response.status}`);
114
+ await writeFile(request.output, Buffer.from(await response.arrayBuffer()));
115
+ debug('saved recording of %s to %s', request.sessionId, request.output);
116
+ }
117
+ catch (err) {
118
+ console.warn(`TestingBotDriver: could not download video for session ${request.sessionId}: ${String(err)}`);
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * The single test that ran inside this session, when unambiguous.
124
+ * Direction matters: connect() and disconnect() happen INSIDE the test, so
125
+ * the session interval is a sub-interval of its test's — but a test's
126
+ * reported duration also covers fixture setup (device allocation), so the
127
+ * test interval is much wider than the session's. The session midpoint must
128
+ * therefore be looked up in the test intervals, never the other way around.
129
+ */
130
+ testForSession(session, outcomes) {
131
+ // A session that hosted several tests has no single correct name/verdict.
132
+ if (session.spans !== 1)
133
+ return undefined;
134
+ if (session.startedAt === undefined || session.endedAt === undefined)
135
+ return undefined;
136
+ const midpoint = (session.startedAt + session.endedAt) / 2;
137
+ // Clock skew tolerance between the worker's stamps and ours.
138
+ const SLACK_MS = 2_000;
139
+ const containing = outcomes.filter((o) => midpoint >= o.startedAt - SLACK_MS && midpoint <= o.endedAt + SLACK_MS);
140
+ if (containing.length === 1)
141
+ return containing[0];
142
+ if (containing.length > 1) {
143
+ debug('%d tests overlap session %s — falling back to the run verdict', containing.length, session.sessionId);
144
+ }
145
+ return undefined;
146
+ }
147
+ }
148
+ //# sourceMappingURL=observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.js","sourceRoot":"","sources":["../src/observer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Which allocations an app entry applies to. Specific keys win over
3
+ * platform-wide ones ('ios-real' beats 'ios').
4
+ */
5
+ export type AppSlot = 'android' | 'android-emulator' | 'android-real' | 'ios' | 'ios-simulator' | 'ios-real';
6
+ export interface TestingBotDriverOptions {
7
+ /**
8
+ * App under test per platform/device type. TestingBot sessions must start
9
+ * with an app (or a browser) — there is no app-less session or mid-session
10
+ * install — so the driver uploads these at allocation time and starts the
11
+ * session with `appium:app`. Values are local paths (.apk/.ipa/.zip) or
12
+ * existing storage URLs ("tb://...").
13
+ *
14
+ * apps: { android: './build/app.apk', 'ios-simulator': './build/app.zip', 'ios-real': './build/app.ipa' }
15
+ */
16
+ apps?: Partial<Record<AppSlot, string>>;
17
+ /** TestingBot API key. Default: TESTINGBOT_KEY (or TB_KEY) env variable. */
18
+ key?: string;
19
+ /** TestingBot API secret. Default: TESTINGBOT_SECRET (or TB_SECRET) env variable. */
20
+ secret?: string;
21
+ /** WebDriver hub URL. Default: https://hub.testingbot.com/wd/hub */
22
+ hubUrl?: string;
23
+ /** REST API base URL. Default: https://api.testingbot.com/v1 */
24
+ apiUrl?: string;
25
+ /**
26
+ * Max time to wait for a device allocation (POST /session), in ms. The hub
27
+ * queues the request until a matching device frees up and gives up after
28
+ * 390s, so the default (395s) lets the hub's own verdict arrive first.
29
+ */
30
+ allocationTimeout?: number;
31
+ /** Per-WebDriver-command timeout, in ms. Default: 60000. */
32
+ commandTimeout?: number;
33
+ /**
34
+ * TestingBot idle timeout in seconds (`tb:options.idletimeout`). Pooled
35
+ * device slots sit idle between tests, so this defaults higher than
36
+ * TestingBot's own default of 130. Default: 230.
37
+ */
38
+ idleTimeout?: number;
39
+ /** TestingBot max session duration in seconds (`tb:options.maxduration`). */
40
+ maxDuration?: number;
41
+ /** Pin the Appium version TestingBot uses (`tb:options.appiumVersion`). */
42
+ appiumVersion?: string;
43
+ /** Test name shown on the TestingBot dashboard (`tb:options.name`). */
44
+ name?: string;
45
+ /**
46
+ * Build name for grouping sessions on the dashboard (`tb:options.build`).
47
+ * Defaults from the CI environment (GitHub Actions, GitLab, CircleCI,
48
+ * Buildkite, Bitrise, Travis, Azure, Jenkins, TeamCity — e.g.
49
+ * "owner/repo #123") or the TESTINGBOT_BUILD variable.
50
+ */
51
+ build?: string;
52
+ /** Device timezone (tz database name, e.g. "Europe/Brussels"). Default: "Etc/UTC". */
53
+ timeZone?: string;
54
+ /** Route device traffic through a proxy in this country ("DE", "US", "*" = random). */
55
+ geoCountryCode?: string;
56
+ /** Simulate network conditions: a preset ("Edge", "3G", "4G", "airplane") or custom speeds. */
57
+ throttleNetwork?: string | {
58
+ downloadSpeed: number;
59
+ uploadSpeed: number;
60
+ latency: number;
61
+ loss: number;
62
+ };
63
+ /** Capture a screenshot at every test step. Default: false. */
64
+ screenshots?: boolean;
65
+ /** Record a video of each session. Default: true. */
66
+ video?: boolean;
67
+ /** Command-log capture: true, false, or 'strip-parameters' (redacts request params). */
68
+ recordLogs?: boolean | 'strip-parameters';
69
+ /** Make test results publicly accessible. Default: false. */
70
+ public?: boolean;
71
+ /** Custom metadata (release, commit hash, ...) shown on the test detail page. */
72
+ extra?: string;
73
+ /** Restrict allocation to tablets only. */
74
+ tabletOnly?: boolean;
75
+ /** Restrict allocation to phones only. */
76
+ phoneOnly?: boolean;
77
+ /**
78
+ * Route sessions through a running TestingBot Tunnel with this identifier
79
+ * (`tb:options.tunnelIdentifier`). Start the tunnel yourself, or use the
80
+ * `tunnel` option to have the driver manage one.
81
+ */
82
+ tunnelIdentifier?: string;
83
+ /**
84
+ * Have the driver start a TestingBot Tunnel before the run and stop it
85
+ * afterwards, so tests can reach localhost/staging servers. Requires the
86
+ * optional `testingbot-tunnel-launcher` package (and Java) on the machine.
87
+ * Pass `true`, or an object with a tunnel `identifier` and any other
88
+ * testingbot-tunnel-launcher options.
89
+ */
90
+ tunnel?: boolean | ({
91
+ identifier?: string;
92
+ } & Record<string, unknown>);
93
+ /** Extra W3C/Appium capabilities, merged last (escape hatch). */
94
+ capabilities?: Record<string, unknown>;
95
+ /** Extra `tb:options` entries, merged last (escape hatch). */
96
+ tbOptions?: Record<string, unknown>;
97
+ /** Report pass/fail to TestingBot after the run. Default: 'on'. */
98
+ testResults?: 'on' | 'off';
99
+ /**
100
+ * Start a fresh TestingBot session for every test instead of reusing the
101
+ * device slot's session across tests. One dashboard entry, video, and
102
+ * pass/fail per test — at the cost of session startup time (device boot)
103
+ * on every test. Default: false (mobilewright's pool reuses sessions).
104
+ */
105
+ sessionPerTest?: boolean;
106
+ }
107
+ export interface ResolvedOptions {
108
+ apps: Partial<Record<AppSlot, string>>;
109
+ key: string;
110
+ secret: string;
111
+ hubUrl: string;
112
+ apiUrl: string;
113
+ allocationTimeout: number;
114
+ commandTimeout: number;
115
+ idleTimeout: number;
116
+ maxDuration: number | undefined;
117
+ appiumVersion: string | undefined;
118
+ name: string | undefined;
119
+ build: string | undefined;
120
+ timeZone: string | undefined;
121
+ geoCountryCode: string | undefined;
122
+ throttleNetwork: string | {
123
+ downloadSpeed: number;
124
+ uploadSpeed: number;
125
+ latency: number;
126
+ loss: number;
127
+ } | undefined;
128
+ screenshots: boolean | undefined;
129
+ video: boolean;
130
+ recordLogs: boolean | 'strip-parameters' | undefined;
131
+ public: boolean | undefined;
132
+ extra: string | undefined;
133
+ tabletOnly: boolean | undefined;
134
+ phoneOnly: boolean | undefined;
135
+ tunnelIdentifier: string | undefined;
136
+ tunnel: false | ({
137
+ identifier?: string;
138
+ } & Record<string, unknown>);
139
+ capabilities: Record<string, unknown>;
140
+ tbOptions: Record<string, unknown>;
141
+ testResults: 'on' | 'off';
142
+ sessionPerTest: boolean;
143
+ }
144
+ /**
145
+ * Resolve options against environment variables and defaults. Missing
146
+ * credentials are tolerated here (`key`/`secret` become empty strings): the
147
+ * config file constructing the driver is imported in every mobilewright
148
+ * process, so throwing from the constructor would break unrelated commands.
149
+ * `prepare()` fails fast instead.
150
+ */
151
+ export declare function resolveOptions(options?: TestingBotDriverOptions): ResolvedOptions;
152
+ /** Ensure credentials are present, with an actionable message when not. */
153
+ export declare function requireCredentials(opts: ResolvedOptions): void;
154
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { detectCiBuild } from './ci.js';
2
+ /**
3
+ * Resolve options against environment variables and defaults. Missing
4
+ * credentials are tolerated here (`key`/`secret` become empty strings): the
5
+ * config file constructing the driver is imported in every mobilewright
6
+ * process, so throwing from the constructor would break unrelated commands.
7
+ * `prepare()` fails fast instead.
8
+ */
9
+ export function resolveOptions(options = {}) {
10
+ return {
11
+ apps: options.apps ?? {},
12
+ key: options.key ?? process.env['TESTINGBOT_KEY'] ?? process.env['TB_KEY'] ?? '',
13
+ secret: options.secret ?? process.env['TESTINGBOT_SECRET'] ?? process.env['TB_SECRET'] ?? '',
14
+ hubUrl: (options.hubUrl ?? 'https://hub.testingbot.com/wd/hub').replace(/\/+$/, ''),
15
+ apiUrl: (options.apiUrl ?? 'https://api.testingbot.com/v1').replace(/\/+$/, ''),
16
+ allocationTimeout: options.allocationTimeout ?? 395_000,
17
+ commandTimeout: options.commandTimeout ?? 60_000,
18
+ idleTimeout: options.idleTimeout ?? 230,
19
+ maxDuration: options.maxDuration,
20
+ appiumVersion: options.appiumVersion,
21
+ name: options.name,
22
+ build: options.build ?? detectCiBuild(),
23
+ timeZone: options.timeZone,
24
+ geoCountryCode: options.geoCountryCode,
25
+ throttleNetwork: options.throttleNetwork,
26
+ screenshots: options.screenshots,
27
+ video: options.video ?? true,
28
+ recordLogs: options.recordLogs,
29
+ public: options.public,
30
+ extra: options.extra,
31
+ tabletOnly: options.tabletOnly,
32
+ phoneOnly: options.phoneOnly,
33
+ tunnelIdentifier: options.tunnelIdentifier ?? (typeof options.tunnel === 'object' ? options.tunnel.identifier : undefined),
34
+ tunnel: options.tunnel === true ? {} : (options.tunnel || false),
35
+ capabilities: options.capabilities ?? {},
36
+ tbOptions: options.tbOptions ?? {},
37
+ testResults: options.testResults ?? 'on',
38
+ sessionPerTest: options.sessionPerTest ?? false,
39
+ };
40
+ }
41
+ /** Ensure credentials are present, with an actionable message when not. */
42
+ export function requireCredentials(opts) {
43
+ if (!opts.key || !opts.secret) {
44
+ throw new Error('TestingBot credentials are missing. Set TESTINGBOT_KEY and TESTINGBOT_SECRET ' +
45
+ 'environment variables, or pass { key, secret } to new TestingBotDriver(...). ' +
46
+ 'Find your credentials at https://testingbot.com/members/user/edit');
47
+ }
48
+ }
49
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AA4IxC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,UAAmC,EAAE;IAClE,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE;QACxB,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;QAChF,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE;QAC5F,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,IAAI,mCAAmC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACnF,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,IAAI,+BAA+B,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/E,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,IAAI,OAAO;QACvD,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,MAAM;QAChD,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,GAAG;QACvC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,aAAa,EAAE;QACvC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,eAAe,EAAE,OAAO,CAAC,eAAe;QACxC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI;QAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1H,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC;QAChE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,EAAE;QACxC,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,EAAE;QAClC,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;QACxC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK;KAChD,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACtD,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,+EAA+E;YAC/E,+EAA+E;YAC/E,mEAAmE,CACpE,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { FetchFn } from './webdriver-client.js';
2
+ export interface TestingBotDevice {
3
+ id: number;
4
+ name: string;
5
+ model_number?: string;
6
+ platform_name: 'Android' | 'iOS';
7
+ version: string | number;
8
+ available?: boolean;
9
+ }
10
+ /** One row of GET /v1/browsers — for mobile rows, a virtual device+OS combo. */
11
+ export interface BrowserEntry {
12
+ name?: string;
13
+ platform?: string;
14
+ platformName?: string;
15
+ deviceName?: string;
16
+ version?: string | number;
17
+ }
18
+ export interface StorageEntry {
19
+ app_url: string;
20
+ state?: 'PROCESSING' | 'DONE';
21
+ type?: string;
22
+ sim_only?: boolean;
23
+ url?: string;
24
+ }
25
+ export interface TestUpdate {
26
+ success?: boolean;
27
+ name?: string;
28
+ statusMessage?: string;
29
+ build?: string;
30
+ extra?: string;
31
+ groups?: string[];
32
+ }
33
+ export interface TestDetails {
34
+ id?: number | string;
35
+ /** Signed S3 URL to the recording, or false when video was disabled. */
36
+ video?: string | false;
37
+ /** True once asset processing (video, logs, screenshots) has finished. */
38
+ assets_available?: boolean;
39
+ duration?: number;
40
+ [key: string]: unknown;
41
+ }
42
+ /** Client for api.testingbot.com/v1 (HTTP Basic auth with key:secret). */
43
+ export declare class RestApiClient {
44
+ private readonly apiUrl;
45
+ private readonly auth;
46
+ private readonly fetchFn;
47
+ constructor(apiUrl: string, key: string, secret: string, fetchFn?: FetchFn);
48
+ /** Validate credentials; used by prepare() to fail fast. */
49
+ getUser(): Promise<Record<string, unknown>>;
50
+ /** Physical devices. `available` restricts to currently-acquirable ones. */
51
+ getDevices(available: boolean): Promise<TestingBotDevice[]>;
52
+ /** Simulator/emulator (and desktop-browser) combos; mobile rows carry platformName + deviceName + OS version. */
53
+ getBrowsers(): Promise<BrowserEntry[]>;
54
+ getDevice(id: string | number): Promise<TestingBotDevice>;
55
+ /** Upload an app binary; returns the storage entry with its tb:// URL. */
56
+ uploadApp(filePath: string): Promise<StorageEntry>;
57
+ getStorageEntry(appUrl: string): Promise<StorageEntry>;
58
+ /** Update a test by WebDriver session UUID (pass/fail, name, build, ...). */
59
+ updateTest(sessionId: string, update: TestUpdate): Promise<void>;
60
+ getTest(sessionId: string): Promise<TestDetails>;
61
+ /** Force-stop a running session (fallback when DELETE /session fails). */
62
+ stopTest(sessionId: string): Promise<void>;
63
+ private request;
64
+ private parse;
65
+ }
66
+ export declare function sha256(data: Buffer): string;
67
+ //# sourceMappingURL=rest-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../src/rest-api.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAKrD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,SAAS,GAAG,KAAK,CAAC;IACjC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,0EAA0E;AAC1E,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAMjF,4DAA4D;IACtD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAIjD,4EAA4E;IACtE,UAAU,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IASjE,iHAAiH;IAC3G,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAMtC,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAK/D,0EAA0E;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAalD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAK5D,6EAA6E;IACvE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAItD,0EAA0E;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAIlC,OAAO;YAeP,KAAK;CAgBpB;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C"}