@taqwright/taqwright 0.0.24

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 (132) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/auto-appium.d.ts +12 -0
  4. package/dist/auto-appium.js +77 -0
  5. package/dist/bin/branding.d.ts +6 -0
  6. package/dist/bin/branding.js +22 -0
  7. package/dist/bin/index.d.ts +2 -0
  8. package/dist/bin/index.js +321 -0
  9. package/dist/bin/init.d.ts +26 -0
  10. package/dist/bin/init.js +902 -0
  11. package/dist/bin/inspect.d.ts +9 -0
  12. package/dist/bin/inspect.js +91 -0
  13. package/dist/bin/report-branding.d.ts +2 -0
  14. package/dist/bin/report-branding.js +42 -0
  15. package/dist/branding-assets.d.ts +1 -0
  16. package/dist/branding-assets.js +1 -0
  17. package/dist/capabilities-helpers.d.ts +7 -0
  18. package/dist/capabilities-helpers.js +14 -0
  19. package/dist/capabilities.d.ts +6 -0
  20. package/dist/capabilities.js +86 -0
  21. package/dist/config.d.ts +17 -0
  22. package/dist/config.js +235 -0
  23. package/dist/discovery-setup.d.ts +1 -0
  24. package/dist/discovery-setup.js +61 -0
  25. package/dist/discovery.d.ts +17 -0
  26. package/dist/discovery.js +55 -0
  27. package/dist/docs/configuration.html +376 -0
  28. package/dist/docs/custom-reporters.html +265 -0
  29. package/dist/docs/docker.html +339 -0
  30. package/dist/docs/docs.js +173 -0
  31. package/dist/docs/generating-tests.html +161 -0
  32. package/dist/docs/images/taqwright-html-report.png +0 -0
  33. package/dist/docs/index.html +13 -0
  34. package/dist/docs/installation.html +686 -0
  35. package/dist/docs/parallel.html +271 -0
  36. package/dist/docs/running-tests.html +385 -0
  37. package/dist/docs/styles.css +460 -0
  38. package/dist/docs/writing-tests.html +565 -0
  39. package/dist/doctor.d.ts +33 -0
  40. package/dist/doctor.js +508 -0
  41. package/dist/expect.d.ts +38 -0
  42. package/dist/expect.js +96 -0
  43. package/dist/fixture/artifact-mode.d.ts +2 -0
  44. package/dist/fixture/artifact-mode.js +7 -0
  45. package/dist/fixture/index.d.ts +15 -0
  46. package/dist/fixture/index.js +324 -0
  47. package/dist/images/taqwright-html-report.png +0 -0
  48. package/dist/images/taqwright_favicon.png +0 -0
  49. package/dist/images/taqwright_logo.png +0 -0
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.js +7 -0
  52. package/dist/inspector/codegen-appium.d.ts +3 -0
  53. package/dist/inspector/codegen-appium.js +228 -0
  54. package/dist/inspector/devices.d.ts +41 -0
  55. package/dist/inspector/devices.js +422 -0
  56. package/dist/inspector/locator-suggester.d.ts +23 -0
  57. package/dist/inspector/locator-suggester.js +539 -0
  58. package/dist/inspector/recorder.d.ts +128 -0
  59. package/dist/inspector/recorder.js +162 -0
  60. package/dist/inspector/server.d.ts +39 -0
  61. package/dist/inspector/server.js +1210 -0
  62. package/dist/inspector/session.d.ts +84 -0
  63. package/dist/inspector/session.js +262 -0
  64. package/dist/inspector/ui.d.ts +1 -0
  65. package/dist/inspector/ui.js +5508 -0
  66. package/dist/keys.d.ts +3 -0
  67. package/dist/keys.js +28 -0
  68. package/dist/locator/index.d.ts +206 -0
  69. package/dist/locator/index.js +1506 -0
  70. package/dist/logger.d.ts +5 -0
  71. package/dist/logger.js +5 -0
  72. package/dist/mobile/index.d.ts +130 -0
  73. package/dist/mobile/index.js +762 -0
  74. package/dist/network/android.d.ts +5 -0
  75. package/dist/network/android.js +87 -0
  76. package/dist/network/ca.d.ts +10 -0
  77. package/dist/network/ca.js +136 -0
  78. package/dist/network/har.d.ts +90 -0
  79. package/dist/network/har.js +101 -0
  80. package/dist/network/host-proxy.d.ts +16 -0
  81. package/dist/network/host-proxy.js +134 -0
  82. package/dist/network/index.d.ts +26 -0
  83. package/dist/network/index.js +105 -0
  84. package/dist/network/ios-sim.d.ts +3 -0
  85. package/dist/network/ios-sim.js +29 -0
  86. package/dist/network/proxy.d.ts +13 -0
  87. package/dist/network/proxy.js +310 -0
  88. package/dist/providers/appium.d.ts +23 -0
  89. package/dist/providers/appium.js +288 -0
  90. package/dist/providers/browserstack/index.d.ts +5 -0
  91. package/dist/providers/browserstack/index.js +77 -0
  92. package/dist/providers/browserstack/utils.d.ts +1 -0
  93. package/dist/providers/browserstack/utils.js +6 -0
  94. package/dist/providers/cloud.d.ts +53 -0
  95. package/dist/providers/cloud.js +117 -0
  96. package/dist/providers/emulator/index.d.ts +8 -0
  97. package/dist/providers/emulator/index.js +47 -0
  98. package/dist/providers/index.d.ts +10 -0
  99. package/dist/providers/index.js +33 -0
  100. package/dist/providers/lambdatest/index.d.ts +28 -0
  101. package/dist/providers/lambdatest/index.js +99 -0
  102. package/dist/providers/lambdatest/utils.d.ts +1 -0
  103. package/dist/providers/lambdatest/utils.js +6 -0
  104. package/dist/providers/local/index.d.ts +9 -0
  105. package/dist/providers/local/index.js +53 -0
  106. package/dist/providers/local-session.d.ts +16 -0
  107. package/dist/providers/local-session.js +55 -0
  108. package/dist/setup/archive.d.ts +2 -0
  109. package/dist/setup/archive.js +43 -0
  110. package/dist/setup/avd.d.ts +12 -0
  111. package/dist/setup/avd.js +103 -0
  112. package/dist/setup/index.d.ts +6 -0
  113. package/dist/setup/index.js +55 -0
  114. package/dist/setup/install-android.d.ts +2 -0
  115. package/dist/setup/install-android.js +70 -0
  116. package/dist/setup/install-appium.d.ts +1 -0
  117. package/dist/setup/install-appium.js +64 -0
  118. package/dist/setup/install-jdk.d.ts +1 -0
  119. package/dist/setup/install-jdk.js +58 -0
  120. package/dist/setup/paths.d.ts +16 -0
  121. package/dist/setup/paths.js +88 -0
  122. package/dist/setup/spawn-tool.d.ts +3 -0
  123. package/dist/setup/spawn-tool.js +11 -0
  124. package/dist/tracer/index.d.ts +34 -0
  125. package/dist/tracer/index.js +687 -0
  126. package/dist/tracer/proxy.d.ts +3 -0
  127. package/dist/tracer/proxy.js +60 -0
  128. package/dist/types/index.d.ts +189 -0
  129. package/dist/types/index.js +6 -0
  130. package/dist/utils.d.ts +2 -0
  131. package/dist/utils.js +37 -0
  132. package/package.json +79 -0
@@ -0,0 +1,324 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { test as baseTest } from '@playwright/test';
3
+ import WebDriver from 'webdriver';
4
+ import { Mobile } from '../mobile/index.js';
5
+ import { Platform, } from '../types/index.js';
6
+ import { getUseOptions, loadTaqwrightConfig } from '../config.js';
7
+ import { resolvedPoolEnvKey } from '../discovery.js';
8
+ import { appiumRemoteOptions } from '../capabilities.js';
9
+ import { isPortOpen } from '../auto-appium.js';
10
+ import { startAppiumServer } from '../providers/appium.js';
11
+ import { resolveAndroidSerial, waitForAndroidDeviceReady, isTransientDeviceError, } from '../inspector/devices.js';
12
+ import { logger } from '../logger.js';
13
+ import { avdBootPreflightError, bootableAvdName } from '../setup/avd.js';
14
+ import { createDeviceProvider, isCloudProvider } from '../providers/index.js';
15
+ import { Tracer } from '../tracer/index.js';
16
+ import { wrapForTracing } from '../tracer/proxy.js';
17
+ import { shouldRetainArtifact } from './artifact-mode.js';
18
+ import { prepareNetworkProxy, configureDeviceForCapture, teardownDeviceCapture, teardownNetworkProxy, extractUdid, } from '../network/index.js';
19
+ const DEFAULT_TIMEOUT = 30_000;
20
+ const VIDEO_TIME_LIMIT_SECONDS = 1800;
21
+ const LOCAL_RETRY_ATTEMPTS = 3;
22
+ async function localAndroidSerial(use) {
23
+ const device = use.device;
24
+ const isLocal = device.provider === 'emulator' || device.provider === 'local-device';
25
+ if (use.platform !== Platform.ANDROID || !isLocal)
26
+ return undefined;
27
+ const avdName = typeof device.name === 'string' ? device.name : undefined;
28
+ return resolveAndroidSerial({ udid: device.udid, avdName });
29
+ }
30
+ async function withDeviceReadyRetry(serial, label, fn) {
31
+ let lastErr;
32
+ for (let attempt = 1; attempt <= LOCAL_RETRY_ATTEMPTS; attempt++) {
33
+ const ready = await waitForAndroidDeviceReady(serial);
34
+ if (!ready) {
35
+ logger.warn(`taqwright: ${serial} not reporting ready before ${label} attempt ${attempt}/${LOCAL_RETRY_ATTEMPTS} — trying anyway.`);
36
+ }
37
+ try {
38
+ return await fn();
39
+ }
40
+ catch (err) {
41
+ lastErr = err;
42
+ const message = err instanceof Error ? err.message : String(err);
43
+ if (attempt < LOCAL_RETRY_ATTEMPTS && isTransientDeviceError(message)) {
44
+ logger.warn(`taqwright: transient device error on ${serial} during ${label} (attempt ${attempt}/${LOCAL_RETRY_ATTEMPTS}), waiting for it to recover and retrying — ${message}`);
45
+ continue;
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
51
+ }
52
+ async function createLocalSession(use) {
53
+ const newSession = () => WebDriver.newSession(appiumRemoteOptions(use));
54
+ const serial = await localAndroidSerial(use);
55
+ if (!serial) {
56
+ const device = use.device;
57
+ const avdName = typeof device.name === 'string' ? device.name : undefined;
58
+ if (use.platform === Platform.ANDROID &&
59
+ device.provider === 'emulator' &&
60
+ avdName &&
61
+ use.appium?.autoStartDevice !== false) {
62
+ const err = await avdBootPreflightError(avdName);
63
+ if (err)
64
+ throw new Error(err);
65
+ }
66
+ return newSession();
67
+ }
68
+ return withDeviceReadyRetry(serial, 'session creation', newSession);
69
+ }
70
+ export const test = baseTest.extend({
71
+ taqwrightUse: [
72
+ async ({}, use, workerInfo) => {
73
+ const config = await loadTaqwrightConfig();
74
+ const projectName = workerInfo.project.name || undefined;
75
+ const useOpts = getUseOptions(config, projectName);
76
+ if (!useOpts) {
77
+ throw new Error(`taqwright: no project found in taqwright.config.ts${projectName ? ` matching "${projectName}"` : ''}`);
78
+ }
79
+ const idx = workerInfo.parallelIndex;
80
+ const dev = useOpts.device;
81
+ let pool = dev.pool;
82
+ if ((!pool || pool.length === 0) && dev.autoDiscover) {
83
+ const resolved = process.env[resolvedPoolEnvKey(projectName)];
84
+ if (!resolved) {
85
+ throw new Error(`taqwright: device.autoDiscover is set on project "${projectName ?? ''}" but no ` +
86
+ `resolved device pool was found — the globalSetup hook did not run. Launch via ` +
87
+ `\`taqwright test\` (or Playwright with the generated config).`);
88
+ }
89
+ pool = JSON.parse(resolved);
90
+ }
91
+ if (!pool || pool.length === 0) {
92
+ await use(useOpts);
93
+ return;
94
+ }
95
+ if (idx >= pool.length) {
96
+ throw new Error(`taqwright: worker #${idx} has no device — pool has ${pool.length} entr${pool.length === 1 ? 'y' : 'ies'}. Reduce \`workers\` or grow the pool.`);
97
+ }
98
+ const slot = pool[idx];
99
+ const baseAppiumPort = useOpts.appium?.port ?? 4723;
100
+ const partitioned = {
101
+ ...useOpts,
102
+ device: {
103
+ ...useOpts.device,
104
+ udid: slot.udid,
105
+ name: slot.name ?? useOpts.device.name,
106
+ osVersion: slot.osVersion ?? useOpts.device.osVersion,
107
+ },
108
+ appium: { ...useOpts.appium, port: baseAppiumPort + idx },
109
+ capabilities: {
110
+ ...(useOpts.capabilities ?? {}),
111
+ 'appium:systemPort': 8200 + idx,
112
+ 'appium:chromedriverPort': 9515 + idx,
113
+ 'appium:mjpegServerPort': 7810 + idx,
114
+ 'appium:wdaLocalPort': 8100 + idx,
115
+ 'appium:derivedDataPath': `/tmp/wda-${idx}`,
116
+ },
117
+ };
118
+ let proc;
119
+ if (partitioned.appium?.autoStart &&
120
+ partitioned.appium.host &&
121
+ partitioned.appium.port !== undefined &&
122
+ !(await isPortOpen(partitioned.appium.host, partitioned.appium.port))) {
123
+ proc = await startAppiumServer('worker', {
124
+ host: partitioned.appium.host,
125
+ port: partitioned.appium.port,
126
+ basePath: partitioned.appium.path,
127
+ }, bootableAvdName(partitioned));
128
+ }
129
+ try {
130
+ await use(partitioned);
131
+ }
132
+ finally {
133
+ if (proc && !proc.killed)
134
+ proc.kill();
135
+ }
136
+ },
137
+ { scope: 'worker' },
138
+ ],
139
+ deviceProvider: [
140
+ async ({ taqwrightUse }, use, workerInfo) => {
141
+ const provider = taqwrightUse.device.provider;
142
+ if (!isCloudProvider(provider)) {
143
+ await use(null);
144
+ return;
145
+ }
146
+ const dp = createDeviceProvider(taqwrightUse, workerInfo.project.name || undefined);
147
+ if (dp.globalSetup)
148
+ await dp.globalSetup();
149
+ await use(dp);
150
+ },
151
+ { scope: 'worker' },
152
+ ],
153
+ networkProxy: async ({ taqwrightUse, deviceProvider }, use, testInfo) => {
154
+ const networkMode = taqwrightUse.network ?? 'off';
155
+ if (networkMode === 'off' || deviceProvider !== null) {
156
+ await use(null);
157
+ return;
158
+ }
159
+ let handle;
160
+ try {
161
+ handle = await prepareNetworkProxy({
162
+ platform: taqwrightUse.platform,
163
+ parallelIndex: testInfo.parallelIndex,
164
+ });
165
+ }
166
+ catch (err) {
167
+ handle = null;
168
+ console.warn(`taqwright: network capture disabled — ${err.message}`);
169
+ }
170
+ try {
171
+ await use(handle);
172
+ }
173
+ finally {
174
+ if (handle)
175
+ await teardownNetworkProxy(handle);
176
+ }
177
+ },
178
+ rawDriver: async ({ taqwrightUse, deviceProvider, networkProxy }, use, testInfo) => {
179
+ if (!deviceProvider) {
180
+ const driver = await createLocalSession(taqwrightUse);
181
+ if (networkProxy) {
182
+ const udid = extractUdid(driver.capabilities) ?? taqwrightUse.device.udid;
183
+ await configureDeviceForCapture(networkProxy, {
184
+ udid,
185
+ platform: taqwrightUse.platform,
186
+ });
187
+ }
188
+ try {
189
+ await use(driver);
190
+ }
191
+ finally {
192
+ if (networkProxy) {
193
+ await teardownDeviceCapture(networkProxy);
194
+ }
195
+ try {
196
+ await driver.deleteSession();
197
+ }
198
+ catch {
199
+ }
200
+ }
201
+ return;
202
+ }
203
+ const handle = await deviceProvider.getDevice();
204
+ try {
205
+ await use(handle.driver);
206
+ }
207
+ finally {
208
+ if (deviceProvider.syncTestDetails) {
209
+ try {
210
+ const failed = testInfo.status !== 'passed';
211
+ await deviceProvider.syncTestDetails({
212
+ status: failed ? 'failed' : 'passed',
213
+ reason: testInfo.error?.message ?? (failed ? testInfo.status : 'taqwright test passed'),
214
+ name: testInfo.title,
215
+ });
216
+ }
217
+ catch {
218
+ }
219
+ }
220
+ try {
221
+ await handle.driver.deleteSession();
222
+ }
223
+ catch {
224
+ }
225
+ }
226
+ },
227
+ mobile: async ({ rawDriver, taqwrightUse, networkProxy }, use, testInfo) => {
228
+ const platform = taqwrightUse.platform;
229
+ const bundleId = taqwrightUse.appBundleId;
230
+ const timeout = taqwrightUse.expectTimeout ?? DEFAULT_TIMEOUT;
231
+ const isCloud = isCloudProvider(taqwrightUse.device.provider);
232
+ if (!isCloud && taqwrightUse.resetBetweenTests && bundleId && taqwrightUse.buildPath) {
233
+ const appRef = platform === Platform.IOS ? { bundleId } : { appId: bundleId };
234
+ const installArg = platform === Platform.IOS
235
+ ? { app: taqwrightUse.buildPath }
236
+ : { appPath: taqwrightUse.buildPath };
237
+ const doReset = async () => {
238
+ await rawDriver.executeScript('mobile: terminateApp', [appRef]).catch(() => { });
239
+ await rawDriver.executeScript('mobile: removeApp', [appRef]).catch(() => { });
240
+ await rawDriver.executeScript('mobile: installApp', [installArg]);
241
+ await rawDriver.executeScript('mobile: activateApp', [appRef]);
242
+ };
243
+ const serial = await localAndroidSerial(taqwrightUse);
244
+ if (serial)
245
+ await withDeviceReadyRetry(serial, 'reset-between-tests', doReset);
246
+ else
247
+ await doReset();
248
+ }
249
+ const mobile = Mobile.wrap(rawDriver, platform, bundleId, timeout);
250
+ const videoMode = taqwrightUse.video ?? 'off';
251
+ const videoOn = videoMode !== 'off' && !isCloud;
252
+ if (videoOn) {
253
+ const recordOpts = platform === Platform.IOS
254
+ ? {
255
+ timeLimit: String(VIDEO_TIME_LIMIT_SECONDS),
256
+ videoType: 'libx264',
257
+ videoQuality: 'medium',
258
+ }
259
+ : { timeLimit: String(VIDEO_TIME_LIMIT_SECONDS) };
260
+ await rawDriver.startRecordingScreen(recordOpts);
261
+ }
262
+ const traceMode = taqwrightUse.trace ?? 'off';
263
+ let tracer = null;
264
+ let tracedMobile = mobile;
265
+ if (traceMode !== 'off') {
266
+ tracer = new Tracer(rawDriver, platform);
267
+ tracedMobile = wrapForTracing(mobile, tracer);
268
+ }
269
+ try {
270
+ await use(tracedMobile);
271
+ }
272
+ finally {
273
+ if (videoOn) {
274
+ let data;
275
+ try {
276
+ data = (await rawDriver.stopRecordingScreen());
277
+ }
278
+ catch {
279
+ }
280
+ const failed = testInfo.status !== 'passed';
281
+ if (data && shouldRetainArtifact(videoMode, failed)) {
282
+ const buf = Buffer.from(data, 'base64');
283
+ if (buf.length > 0) {
284
+ const path = testInfo.outputPath('screen.mp4');
285
+ await fs.writeFile(path, buf);
286
+ await testInfo.attach('taqwright-video', { path, contentType: 'video/mp4' });
287
+ }
288
+ }
289
+ }
290
+ let har;
291
+ if (networkProxy) {
292
+ try {
293
+ har = (await networkProxy.flush());
294
+ }
295
+ catch {
296
+ }
297
+ const networkMode = taqwrightUse.network ?? 'off';
298
+ const failed = testInfo.status !== 'passed';
299
+ if (har && shouldRetainArtifact(networkMode, failed)) {
300
+ const path = testInfo.outputPath('network.har');
301
+ await fs.writeFile(path, JSON.stringify(har, null, 2), 'utf-8');
302
+ await testInfo.attach('taqwright-har', {
303
+ path,
304
+ contentType: 'application/json',
305
+ });
306
+ }
307
+ }
308
+ if (tracer) {
309
+ const failed = testInfo.status !== 'passed';
310
+ if (shouldRetainArtifact(traceMode, failed)) {
311
+ const path = testInfo.outputPath('trace.html');
312
+ const html = tracer.toHtml({
313
+ title: testInfo.title,
314
+ status: testInfo.status,
315
+ duration: testInfo.duration,
316
+ project: { name: testInfo.project.name },
317
+ }, { har: har ?? null });
318
+ await fs.writeFile(path, html, 'utf-8');
319
+ await testInfo.attach('taqwright-trace', { path, contentType: 'text/html' });
320
+ }
321
+ }
322
+ }
323
+ },
324
+ });
Binary file
@@ -0,0 +1,9 @@
1
+ export { test } from './fixture/index.js';
2
+ export { expect, type TaqwrightExpect, type MobileMatchers } from './expect.js';
3
+ export { Mobile } from './mobile/index.js';
4
+ export { Locator } from './locator/index.js';
5
+ export { defineConfig, loadTaqwrightConfig as loadConfig } from './config.js';
6
+ export { iosParallelCaps, type IosParallelCapsOptions } from './capabilities-helpers.js';
7
+ export { Platform, type TaqwrightConfig, type TaqwrightProjectConfig, type TaqwrightUseOptions, type AppiumServerConfig, type DeviceConfig, type EmulatorDeviceConfig, type LocalDeviceConfig, type ScrollDirection, type SwipeDirection, type HardwareButton, type BoundingBox, type ScreenSize, type GestureOptions, type GesturePointer, type TraceMode, type VideoMode, } from './types/index.js';
8
+ export type { ClickPoint, SwipeOptions, GetByOptions, LaunchAppOptions, GeoLocation, NetworkConnection, DeviceLogEntry, ScreenRecordingOptions, PauseOptions, } from './mobile/index.js';
9
+ export type { WaitForOptions, ActionOptions, LongPressOptions, ScrollIntoViewOptions, ElementSwipeOptions, DragOptions, LocatorFilterOptions, SelectOptionInput, PressSequentiallyOptions, } from './locator/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { test } from './fixture/index.js';
2
+ export { expect } from './expect.js';
3
+ export { Mobile } from './mobile/index.js';
4
+ export { Locator } from './locator/index.js';
5
+ export { defineConfig, loadTaqwrightConfig as loadConfig } from './config.js';
6
+ export { iosParallelCaps } from './capabilities-helpers.js';
7
+ export { Platform, } from './types/index.js';
@@ -0,0 +1,3 @@
1
+ import type { RecordedAction } from './recorder.js';
2
+ export declare function toStepsPython(actions: RecordedAction[]): string;
3
+ export declare function toStepsJava(actions: RecordedAction[]): string;
@@ -0,0 +1,228 @@
1
+ const PY_BY = {
2
+ id: 'AppiumBy.ID',
3
+ 'accessibility id': 'AppiumBy.ACCESSIBILITY_ID',
4
+ xpath: 'AppiumBy.XPATH',
5
+ '-android uiautomator': 'AppiumBy.ANDROID_UIAUTOMATOR',
6
+ '-ios predicate string': 'AppiumBy.IOS_PREDICATE',
7
+ '-ios class chain': 'AppiumBy.IOS_CLASS_CHAIN',
8
+ 'class name': 'AppiumBy.CLASS_NAME',
9
+ name: 'AppiumBy.NAME',
10
+ 'css selector': 'AppiumBy.CSS_SELECTOR',
11
+ };
12
+ const JAVA_BY = {
13
+ id: 'id',
14
+ 'accessibility id': 'accessibilityId',
15
+ xpath: 'xpath',
16
+ '-android uiautomator': 'androidUIAutomator',
17
+ '-ios predicate string': 'iOSNsPredicateString',
18
+ '-ios class chain': 'iOSClassChain',
19
+ 'class name': 'className',
20
+ name: 'name',
21
+ };
22
+ function str(s) {
23
+ return JSON.stringify(s);
24
+ }
25
+ function leafOf(loc) {
26
+ const d = loc.descriptor;
27
+ if (d) {
28
+ if (d.kind === 'leaf')
29
+ return { using: d.using, value: d.value };
30
+ if (d.kind === 'nth' && d.on.kind === 'leaf')
31
+ return { using: d.on.using, value: d.on.value, n: d.n };
32
+ if (d.kind === 'first' && d.on.kind === 'leaf')
33
+ return { using: d.on.using, value: d.on.value, n: 0 };
34
+ return null;
35
+ }
36
+ if (loc.using !== undefined && loc.value !== undefined)
37
+ return { using: loc.using, value: loc.value };
38
+ return null;
39
+ }
40
+ function pyTuple(leaf) {
41
+ const by = PY_BY[leaf.using];
42
+ if (!by)
43
+ return null;
44
+ return `(${by}, ${str(leaf.value)})`;
45
+ }
46
+ function pyElement(leaf) {
47
+ const tuple = pyTuple(leaf);
48
+ if (!tuple)
49
+ return null;
50
+ return leaf.n === undefined
51
+ ? `driver.find_element${tuple}`
52
+ : `driver.find_elements${tuple}[${leaf.n}]`;
53
+ }
54
+ function javaBy(leaf) {
55
+ const fn = JAVA_BY[leaf.using];
56
+ if (!fn)
57
+ return null;
58
+ return `AppiumBy.${fn}(${str(leaf.value)})`;
59
+ }
60
+ function javaElement(leaf) {
61
+ const by = javaBy(leaf);
62
+ if (!by)
63
+ return null;
64
+ return leaf.n === undefined
65
+ ? `driver.findElement(${by})`
66
+ : `driver.findElements(${by}).get(${leaf.n})`;
67
+ }
68
+ const PY_WAIT = 'WebDriverWait(driver, 10).until';
69
+ const JAVA_WAIT = 'new WebDriverWait(driver, Duration.ofSeconds(10)).until';
70
+ function pyTodo(a) {
71
+ return `# TODO: ${a.kind} has no direct Appium-Python-Client equivalent (taqwright: ${tsHint(a)})`;
72
+ }
73
+ function javaTodo(a) {
74
+ return `// TODO: ${a.kind} has no direct Appium java-client equivalent (taqwright: ${tsHint(a)})`;
75
+ }
76
+ function tsHint(a) {
77
+ return 'code' in a && typeof a.code === 'string' ? a.code : a.kind;
78
+ }
79
+ function renderPy(a) {
80
+ switch (a.kind) {
81
+ case 'comment':
82
+ return `# ${a.text}`;
83
+ case 'sendKeys':
84
+ return `driver.execute_script("mobile: type", {"text": ${str(a.text)}})`;
85
+ case 'switchContext':
86
+ return /^NATIVE_APP$/i.test(a.context)
87
+ ? `driver.switch_to.context("NATIVE_APP")`
88
+ : `driver.switch_to.context(${str(a.context)}) # NOTE: WebView handles vary across runs`;
89
+ case 'tap':
90
+ case 'swipe':
91
+ case 'screenScroll':
92
+ return pyTodo(a);
93
+ default:
94
+ break;
95
+ }
96
+ const leaf = leafOf(a);
97
+ if (!leaf)
98
+ return pyTodo(a);
99
+ const el = pyElement(leaf);
100
+ const tuple = pyTuple(leaf);
101
+ if (!el || !tuple)
102
+ return pyTodo(a);
103
+ switch (a.kind) {
104
+ case 'locatorClick':
105
+ return `${el}.click()`;
106
+ case 'locatorFill':
107
+ return `${el}.send_keys(${str(a.text)})`;
108
+ case 'locatorClear':
109
+ return `${el}.clear()`;
110
+ case 'locatorPressSequentially':
111
+ return `${el}.send_keys(${str(a.text)})${a.delay ? ' # delay not supported by send_keys' : ''}`;
112
+ case 'locatorCheck':
113
+ return `${el}.click() # check: taps to toggle — verify current state`;
114
+ case 'locatorUncheck':
115
+ return `${el}.click() # uncheck: taps to toggle — verify current state`;
116
+ case 'locatorFocus':
117
+ return `${el}.click() # focus`;
118
+ case 'assertVisible':
119
+ return `${PY_WAIT}(EC.visibility_of_element_located(${tuple}))`;
120
+ case 'assertHidden':
121
+ return `${PY_WAIT}(EC.invisibility_of_element_located(${tuple}))`;
122
+ case 'assertAttached':
123
+ return `${PY_WAIT}(EC.presence_of_element_located(${tuple}))`;
124
+ case 'assertEnabled':
125
+ return `${PY_WAIT}(EC.element_to_be_clickable(${tuple}))`;
126
+ case 'assertDisabled':
127
+ return `assert not ${el}.is_enabled()`;
128
+ case 'assertText':
129
+ return a.mode === 'contains'
130
+ ? `${PY_WAIT}(EC.text_to_be_present_in_element(${tuple}, ${str(a.expected)}))`
131
+ : `${PY_WAIT}(lambda d: d.find_element${tuple}.text == ${str(a.expected)})`;
132
+ case 'assertValue':
133
+ return `${PY_WAIT}(lambda d: d.find_element${tuple}.get_attribute("value") == ${str(a.expected)})`;
134
+ case 'assertEmpty':
135
+ return `${PY_WAIT}(lambda d: d.find_element${tuple}.text == "")`;
136
+ case 'assertChecked':
137
+ return `assert ${el}.get_attribute("checked") == "true"`;
138
+ case 'assertUnchecked':
139
+ return `assert ${el}.get_attribute("checked") != "true"`;
140
+ case 'assertCount':
141
+ return `${PY_WAIT}(lambda d: len(d.find_elements${tuple}) == ${a.expected})`;
142
+ case 'assertAttribute':
143
+ return `assert ${el}.get_attribute(${str(a.name)}) == ${str(a.expected)}`;
144
+ default:
145
+ return pyTodo(a);
146
+ }
147
+ }
148
+ function renderJava(a) {
149
+ switch (a.kind) {
150
+ case 'comment':
151
+ return `// ${a.text}`;
152
+ case 'sendKeys':
153
+ return `driver.executeScript("mobile: type", java.util.Map.of("text", ${str(a.text)}));`;
154
+ case 'switchContext':
155
+ return /^NATIVE_APP$/i.test(a.context)
156
+ ? `((io.appium.java_client.remote.SupportsContextSwitching) driver).context("NATIVE_APP");`
157
+ : `((io.appium.java_client.remote.SupportsContextSwitching) driver).context(${str(a.context)}); // NOTE: WebView handles vary across runs`;
158
+ case 'tap':
159
+ case 'swipe':
160
+ case 'screenScroll':
161
+ return javaTodo(a);
162
+ default:
163
+ break;
164
+ }
165
+ const leaf = leafOf(a);
166
+ if (!leaf)
167
+ return javaTodo(a);
168
+ const el = javaElement(leaf);
169
+ const by = javaBy(leaf);
170
+ if (!el || !by)
171
+ return javaTodo(a);
172
+ switch (a.kind) {
173
+ case 'locatorClick':
174
+ return `${el}.click();`;
175
+ case 'locatorFill':
176
+ return `${el}.sendKeys(${str(a.text)});`;
177
+ case 'locatorClear':
178
+ return `${el}.clear();`;
179
+ case 'locatorPressSequentially':
180
+ return `${el}.sendKeys(${str(a.text)});${a.delay ? ' // delay not supported by sendKeys' : ''}`;
181
+ case 'locatorCheck':
182
+ return `${el}.click(); // check: taps to toggle — verify current state`;
183
+ case 'locatorUncheck':
184
+ return `${el}.click(); // uncheck: taps to toggle — verify current state`;
185
+ case 'locatorFocus':
186
+ return `${el}.click(); // focus`;
187
+ case 'assertVisible':
188
+ return `${JAVA_WAIT}(ExpectedConditions.visibilityOfElementLocated(${by}));`;
189
+ case 'assertHidden':
190
+ return `${JAVA_WAIT}(ExpectedConditions.invisibilityOfElementLocated(${by}));`;
191
+ case 'assertAttached':
192
+ return `${JAVA_WAIT}(ExpectedConditions.presenceOfElementLocated(${by}));`;
193
+ case 'assertEnabled':
194
+ return `${JAVA_WAIT}(ExpectedConditions.elementToBeClickable(${by}));`;
195
+ case 'assertDisabled':
196
+ return `org.junit.jupiter.api.Assertions.assertFalse(${el}.isEnabled());`;
197
+ case 'assertText':
198
+ return a.mode === 'contains'
199
+ ? `${JAVA_WAIT}(ExpectedConditions.textToBePresentInElementLocated(${by}, ${str(a.expected)}));`
200
+ : `${JAVA_WAIT}(d -> d.findElement(${by}).getText().equals(${str(a.expected)}));`;
201
+ case 'assertValue':
202
+ return `${JAVA_WAIT}(d -> ${str(a.expected)}.equals(d.findElement(${by}).getAttribute("value")));`;
203
+ case 'assertEmpty':
204
+ return `${JAVA_WAIT}(d -> d.findElement(${by}).getText().isEmpty());`;
205
+ case 'assertChecked':
206
+ return `org.junit.jupiter.api.Assertions.assertEquals("true", ${el}.getAttribute("checked"));`;
207
+ case 'assertUnchecked':
208
+ return `org.junit.jupiter.api.Assertions.assertNotEquals("true", ${el}.getAttribute("checked"));`;
209
+ case 'assertCount':
210
+ return `${JAVA_WAIT}(d -> d.findElements(${by}).size() == ${a.expected});`;
211
+ case 'assertAttribute':
212
+ return `org.junit.jupiter.api.Assertions.assertEquals(${str(a.expected)}, ${el}.getAttribute(${str(a.name)}));`;
213
+ default:
214
+ return javaTodo(a);
215
+ }
216
+ }
217
+ const EMPTY_PY = '# (no actions recorded yet — interact with the device in the inspector)';
218
+ const EMPTY_JAVA = '// (no actions recorded yet — interact with the device in the inspector)';
219
+ export function toStepsPython(actions) {
220
+ if (actions.length === 0)
221
+ return EMPTY_PY + '\n';
222
+ return actions.map(renderPy).join('\n') + '\n';
223
+ }
224
+ export function toStepsJava(actions) {
225
+ if (actions.length === 0)
226
+ return EMPTY_JAVA + '\n';
227
+ return actions.map(renderJava).join('\n') + '\n';
228
+ }
@@ -0,0 +1,41 @@
1
+ export type DeviceState = 'booted' | 'shutdown' | 'booting' | 'unknown';
2
+ export interface Device {
3
+ type: 'android' | 'ios';
4
+ udid: string;
5
+ name: string;
6
+ osVersion?: string;
7
+ state: DeviceState;
8
+ avdName?: string;
9
+ runtime?: string;
10
+ bootable?: boolean;
11
+ bootHint?: string;
12
+ }
13
+ export interface DeviceListing {
14
+ android: Device[];
15
+ ios: Device[];
16
+ toolsMissing: {
17
+ adb?: boolean;
18
+ emulator?: boolean;
19
+ xcrun?: boolean;
20
+ };
21
+ }
22
+ export declare function listDevices(): Promise<DeviceListing>;
23
+ export declare function annotateAndroidBootability(devices: Device[]): Promise<Device[]>;
24
+ export declare function startAndroidEmulator(avdName: string): Promise<void>;
25
+ export declare function stopAndroidEmulator(serial: string): Promise<void>;
26
+ export declare function findSerialForAvd(online: Map<string, {
27
+ avdName?: string;
28
+ }>, avdName: string): string | undefined;
29
+ export declare function ensureAndroidAvdReady(avdName: string, opts?: {
30
+ timeoutMs?: number;
31
+ }): Promise<string>;
32
+ export declare function waitForAndroidDeviceReady(serial: string, opts?: {
33
+ timeoutMs?: number;
34
+ }): Promise<boolean>;
35
+ export declare function resolveAndroidSerial(opts: {
36
+ udid?: string;
37
+ avdName?: string;
38
+ }): Promise<string | undefined>;
39
+ export declare function isTransientDeviceError(message: string): boolean;
40
+ export declare function startIosSimulator(udid: string): Promise<void>;
41
+ export declare function stopIosSimulator(udid: string): Promise<void>;