@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,28 @@
1
+ import { type TaqwrightUseOptions } from '../../types/index.js';
2
+ import { CloudProvider } from '../cloud.js';
3
+ export declare function buildCapabilities(use: TaqwrightUseOptions, projectName: string, appUrl: string): {
4
+ 'lt:options': {
5
+ w3c: boolean;
6
+ platformName: string;
7
+ deviceName: string;
8
+ platformVersion: string;
9
+ deviceOrientation: import("../../types/index.js").DeviceOrientation | undefined;
10
+ app: string;
11
+ isRealMobile: boolean;
12
+ build: string;
13
+ project: string;
14
+ video: boolean;
15
+ devicelog: boolean;
16
+ queueTimeout: number;
17
+ idleTimeout: number;
18
+ autoGrantPermissions: boolean;
19
+ autoAcceptAlerts: boolean;
20
+ enableImageInjection: boolean | undefined;
21
+ };
22
+ 'appium:settings[snapshotMaxDepth]'?: number | undefined;
23
+ platformName: string;
24
+ 'appium:automationName': string;
25
+ };
26
+ export declare class LambdaTestDeviceProvider extends CloudProvider {
27
+ constructor(use: TaqwrightUseOptions, appBundleId: string | undefined, projectName?: string);
28
+ }
@@ -0,0 +1,99 @@
1
+ import { Platform, } from '../../types/index.js';
2
+ import { CloudProvider } from '../cloud.js';
3
+ const SESSION_API = 'https://mobile-api.lambdatest.com/mobile-automation/api/v1';
4
+ const UPLOAD_API = 'https://manual-api.lambdatest.com/app/upload/realDevice';
5
+ function normalizeDevice(name, osVersion) {
6
+ const deviceAliases = { 'Google Pixel 8': 'Pixel 8' };
7
+ const osAliases = { '14.0': '14' };
8
+ return {
9
+ name: deviceAliases[name] ?? name,
10
+ osVersion: osAliases[osVersion] ?? osVersion,
11
+ };
12
+ }
13
+ export function buildCapabilities(use, projectName, appUrl) {
14
+ const device = use.device;
15
+ const resolved = normalizeDevice(device.name, device.osVersion);
16
+ const ciLabel = process.env.GITHUB_ACTIONS === 'true' ? `CI ${process.env.GITHUB_RUN_ID}` : process.env.USER;
17
+ const isIOS = use.platform === Platform.IOS;
18
+ const platformName = isIOS ? 'iOS' : 'Android';
19
+ const userCaps = { ...(use.capabilities ?? {}) };
20
+ const userLt = userCaps['lt:options'] ?? {};
21
+ delete userCaps['lt:options'];
22
+ const topLevelUser = {};
23
+ const bareUserLt = {};
24
+ for (const [k, v] of Object.entries(userCaps)) {
25
+ if (k === 'platformName' || k.includes(':'))
26
+ topLevelUser[k] = v;
27
+ else
28
+ bareUserLt[k] = v;
29
+ }
30
+ return {
31
+ platformName,
32
+ 'appium:automationName': isIOS ? 'XCUITest' : 'UiAutomator2',
33
+ ...(isIOS ? { 'appium:settings[snapshotMaxDepth]': 62 } : {}),
34
+ 'lt:options': {
35
+ w3c: true,
36
+ platformName,
37
+ deviceName: resolved.name,
38
+ platformVersion: resolved.osVersion,
39
+ deviceOrientation: device.orientation,
40
+ app: appUrl,
41
+ isRealMobile: true,
42
+ build: `${projectName} ${use.platform} ${ciLabel}`,
43
+ project: projectName,
44
+ video: true,
45
+ devicelog: true,
46
+ queueTimeout: 600,
47
+ idleTimeout: 600,
48
+ autoGrantPermissions: true,
49
+ autoAcceptAlerts: true,
50
+ enableImageInjection: device.enableCameraImageInjection,
51
+ ...userLt,
52
+ ...bareUserLt,
53
+ },
54
+ ...topLevelUser,
55
+ };
56
+ }
57
+ const lambdaTestSpec = {
58
+ provider: 'lambdatest',
59
+ credentialEnv: ['LAMBDATEST_USERNAME', 'LAMBDATEST_ACCESS_KEY'],
60
+ prebuiltScheme: 'lt://',
61
+ appUrlEnvVar: (projectName) => `LAMBDATEST_APP_URL_${projectName.toUpperCase()}`,
62
+ upload: {
63
+ endpoint: UPLOAD_API,
64
+ urlBody: (buildPath, projectName) => new URLSearchParams({
65
+ url: buildPath,
66
+ visibility: 'team',
67
+ storage: 'url',
68
+ name: projectName,
69
+ }),
70
+ fileBody: (file, fileName, projectName) => {
71
+ const form = new FormData();
72
+ form.append('visibility', 'team');
73
+ form.append('storage', 'file');
74
+ form.append('appFile', new Blob([new Uint8Array(file)]), fileName);
75
+ form.append('name', projectName);
76
+ return form;
77
+ },
78
+ },
79
+ hub: { hostname: 'mobile-hub.lambdatest.com', port: 443, path: '/wd/hub', protocol: 'https' },
80
+ buildCapabilities: ({ use, projectName, appUrl }) => buildCapabilities(use, projectName, appUrl),
81
+ syncRequest: (sessionId, details) => ({
82
+ url: `${SESSION_API}/sessions/${sessionId}`,
83
+ method: 'PATCH',
84
+ body: details.status
85
+ ? JSON.stringify({
86
+ name: details.name,
87
+ status_ind: details.status,
88
+ custom_data: details.reason,
89
+ })
90
+ : JSON.stringify({ name: details.name }),
91
+ }),
92
+ strictSync: false,
93
+ requireBundleId: true,
94
+ };
95
+ export class LambdaTestDeviceProvider extends CloudProvider {
96
+ constructor(use, appBundleId, projectName) {
97
+ super(lambdaTestSpec, use, appBundleId, projectName);
98
+ }
99
+ }
@@ -0,0 +1 @@
1
+ export declare function getAuthHeader(): string;
@@ -0,0 +1,6 @@
1
+ export function getAuthHeader() {
2
+ const userName = process.env.LAMBDATEST_USERNAME;
3
+ const accessKey = process.env.LAMBDATEST_ACCESS_KEY;
4
+ const key = Buffer.from(`${userName}:${accessKey}`).toString('base64');
5
+ return `Basic ${key}`;
6
+ }
@@ -0,0 +1,9 @@
1
+ import { type TaqwrightUseOptions, type DeviceHandle, type DeviceProvider } from '../../types/index.js';
2
+ export declare class LocalDeviceProvider implements DeviceProvider {
3
+ private readonly use;
4
+ sessionId?: string;
5
+ constructor(use: TaqwrightUseOptions, appBundleId?: string);
6
+ globalSetup(): Promise<void>;
7
+ getDevice(): Promise<DeviceHandle>;
8
+ private resolveUdid;
9
+ }
@@ -0,0 +1,53 @@
1
+ import { Platform, } from '../../types/index.js';
2
+ import { getActiveAndroidDevices, getAppBundleId, getConnectedIOSDeviceUDID } from '../appium.js';
3
+ import { buildLocalCapabilities, makeHandle, openLocalSession, resolveAndroidApp, } from '../local-session.js';
4
+ import { validateBuildPath } from '../../utils.js';
5
+ import { logger } from '../../logger.js';
6
+ export class LocalDeviceProvider {
7
+ use;
8
+ sessionId;
9
+ constructor(use, appBundleId) {
10
+ this.use = use;
11
+ if (use.device.provider !== 'local-device') {
12
+ throw new Error(`local-device provider received device.provider='${use.device.provider}'.`);
13
+ }
14
+ if (appBundleId) {
15
+ logger.log(`Bundle id (${appBundleId}) ignored for local-device provider — resolved from the build at runtime.`);
16
+ }
17
+ }
18
+ async globalSetup() {
19
+ const android = this.use.platform === Platform.ANDROID;
20
+ validateBuildPath(this.use.buildPath, android ? '.apk' : '.ipa');
21
+ if (android && !process.env.ANDROID_HOME) {
22
+ throw new Error('ANDROID_HOME is not set. Required to locate adb / build-tools. See https://developer.android.com/tools');
23
+ }
24
+ }
25
+ async getDevice() {
26
+ const device = this.use.device;
27
+ const { appPackage, appActivity } = await resolveAndroidApp(this.use);
28
+ const udid = await this.resolveUdid(device);
29
+ const capabilities = buildLocalCapabilities(this.use, device, {
30
+ appPackage,
31
+ appActivity,
32
+ udid,
33
+ });
34
+ const driver = await openLocalSession(this.use, capabilities);
35
+ this.sessionId = driver.sessionId;
36
+ const bundleId = this.use.platform === Platform.IOS && this.use.buildPath
37
+ ? await getAppBundleId(this.use.buildPath)
38
+ : this.use.appBundleId;
39
+ return makeHandle(this.use, driver, bundleId);
40
+ }
41
+ async resolveUdid(device) {
42
+ if (device.udid) {
43
+ return device.udid;
44
+ }
45
+ if (this.use.platform === Platform.IOS) {
46
+ return getConnectedIOSDeviceUDID();
47
+ }
48
+ if ((await getActiveAndroidDevices()) > 1) {
49
+ logger.warn('Multiple active Android devices detected — picking one. Set device.udid to disambiguate.');
50
+ }
51
+ return undefined;
52
+ }
53
+ }
@@ -0,0 +1,16 @@
1
+ import type { Client as WebDriverClient } from 'webdriver';
2
+ import { type TaqwrightUseOptions, type DeviceHandle, type EmulatorDeviceConfig, type LocalDeviceConfig } from '../types/index.js';
3
+ type LocalDevice = EmulatorDeviceConfig | LocalDeviceConfig;
4
+ export declare function resolveAndroidApp(use: TaqwrightUseOptions): Promise<{
5
+ appPackage?: string;
6
+ appActivity?: string;
7
+ }>;
8
+ export declare function buildLocalCapabilities(use: TaqwrightUseOptions, device: LocalDevice, parts: {
9
+ appPackage?: string;
10
+ appActivity?: string;
11
+ udid?: string;
12
+ extraCaps?: Record<string, unknown>;
13
+ }): Record<string, unknown>;
14
+ export declare function openLocalSession(use: TaqwrightUseOptions, capabilities: Record<string, unknown>): Promise<WebDriverClient>;
15
+ export declare function makeHandle(use: TaqwrightUseOptions, driver: WebDriverClient, bundleId: string | undefined): DeviceHandle;
16
+ export {};
@@ -0,0 +1,55 @@
1
+ import { Platform, } from '../types/index.js';
2
+ import { getApkDetails, installDriver, startAppiumServer } from './appium.js';
3
+ import { bootableAvdName } from '../setup/avd.js';
4
+ function appiumConnection(use) {
5
+ return {
6
+ port: use.appium?.port ?? 4723,
7
+ hostname: use.appium?.host ?? 'localhost',
8
+ path: use.appium?.path ?? '/',
9
+ logLevel: use.appium?.logLevel ?? 'warn',
10
+ ...(use.appium?.connectionTimeout !== undefined
11
+ ? { connectionRetryTimeout: use.appium.connectionTimeout }
12
+ : {}),
13
+ };
14
+ }
15
+ export async function resolveAndroidApp(use) {
16
+ if (use.platform === Platform.ANDROID && use.buildPath) {
17
+ const { packageName, launchableActivity } = await getApkDetails(use.buildPath);
18
+ return { appPackage: packageName, appActivity: launchableActivity };
19
+ }
20
+ return {};
21
+ }
22
+ export function buildLocalCapabilities(use, device, parts) {
23
+ const platform = use.platform;
24
+ return {
25
+ platformName: platform,
26
+ 'appium:automationName': platform === Platform.ANDROID ? 'uiautomator2' : 'xcuitest',
27
+ 'appium:deviceName': typeof device.name === 'string' ? device.name : device.name?.source,
28
+ 'appium:udid': parts.udid,
29
+ 'appium:app': use.buildPath,
30
+ 'appium:appPackage': parts.appPackage,
31
+ 'appium:appActivity': parts.appActivity,
32
+ 'appium:autoGrantPermissions': true,
33
+ 'appium:autoAcceptAlerts': true,
34
+ 'appium:fullReset': true,
35
+ 'appium:deviceOrientation': device.orientation,
36
+ 'appium:settings[snapshotMaxDepth]': 62,
37
+ ...(parts.extraCaps ?? {}),
38
+ ...(use.capabilities ?? {}),
39
+ };
40
+ }
41
+ export async function openLocalSession(use, capabilities) {
42
+ const driverName = use.platform === Platform.ANDROID ? 'uiautomator2' : 'xcuitest';
43
+ await installDriver(driverName);
44
+ await startAppiumServer(use.device.provider, {}, bootableAvdName(use));
45
+ const WebDriver = (await import('webdriver')).default;
46
+ return WebDriver.newSession({ ...appiumConnection(use), capabilities });
47
+ }
48
+ export function makeHandle(use, driver, bundleId) {
49
+ return {
50
+ driver,
51
+ bundleId,
52
+ options: { expectTimeout: use.expectTimeout ?? 30_000 },
53
+ provider: use.device.provider,
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ export declare function download(url: string, destFile: string, signal?: AbortSignal): Promise<void>;
2
+ export declare function extract(archiveFile: string, destDir: string): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { createWriteStream, mkdirSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { Readable } from 'node:stream';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import path from 'node:path';
6
+ export async function download(url, destFile, signal) {
7
+ mkdirSync(path.dirname(destFile), { recursive: true });
8
+ const res = await fetch(url, { redirect: 'follow', signal });
9
+ if (!res.ok || !res.body) {
10
+ throw new Error(`download failed (HTTP ${res.status}) — ${url}`);
11
+ }
12
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(destFile));
13
+ }
14
+ function run(cmd, args) {
15
+ return new Promise((resolve, reject) => {
16
+ const p = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'inherit'] });
17
+ p.on('exit', (code) => resolve(code ?? 1));
18
+ p.on('error', reject);
19
+ });
20
+ }
21
+ export async function extract(archiveFile, destDir) {
22
+ mkdirSync(destDir, { recursive: true });
23
+ const lower = archiveFile.toLowerCase();
24
+ if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
25
+ if ((await run('tar', ['-xzf', archiveFile, '-C', destDir])) !== 0) {
26
+ throw new Error(`tar extraction failed for ${archiveFile}`);
27
+ }
28
+ return;
29
+ }
30
+ if (lower.endsWith('.zip')) {
31
+ try {
32
+ if ((await run('unzip', ['-q', archiveFile, '-d', destDir])) === 0)
33
+ return;
34
+ }
35
+ catch {
36
+ }
37
+ if ((await run('tar', ['-xf', archiveFile, '-C', destDir])) === 0)
38
+ return;
39
+ throw new Error(`could not unzip ${archiveFile} — install \`unzip\` ` +
40
+ '(Linux: `sudo apt-get install unzip`) and re-run `taqwright install`.');
41
+ }
42
+ throw new Error(`unsupported archive type: ${archiveFile}`);
43
+ }
@@ -0,0 +1,12 @@
1
+ import { type TaqwrightUseOptions } from '../types/index.js';
2
+ export declare function bootableAvdName(use: TaqwrightUseOptions): string | undefined;
3
+ export declare function normalizeSysImagePath(raw: string): string;
4
+ export declare function avdHomeDir(): string;
5
+ export declare function readAvdSystemImage(avdName: string, avdHome?: string): Promise<string | undefined>;
6
+ export declare function isAvdImageInstalled(image: string, androidHome: string): boolean;
7
+ export declare function resolveAvdSdk(avdName: string): Promise<{
8
+ image?: string;
9
+ sdkRoot?: string;
10
+ }>;
11
+ export declare function androidEnvForAvd(avdName?: string): Promise<NodeJS.ProcessEnv | undefined>;
12
+ export declare function avdBootPreflightError(avdName: string, androidHome?: string | undefined): Promise<string | null>;
@@ -0,0 +1,103 @@
1
+ import { existsSync, promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { Platform } from '../types/index.js';
5
+ import { managedEnv, readManifest, systemAndroidHome } from './paths.js';
6
+ export function bootableAvdName(use) {
7
+ const device = use.device;
8
+ return use.platform === Platform.ANDROID &&
9
+ device.provider === 'emulator' &&
10
+ typeof device.name === 'string'
11
+ ? device.name
12
+ : undefined;
13
+ }
14
+ export function normalizeSysImagePath(raw) {
15
+ return raw.trim().replace(/\\/g, '/').replace(/\/+$/, '');
16
+ }
17
+ export function avdHomeDir() {
18
+ return process.env.ANDROID_AVD_HOME || path.join(os.homedir(), '.android', 'avd');
19
+ }
20
+ export async function readAvdSystemImage(avdName, avdHome = avdHomeDir()) {
21
+ const configPath = path.join(avdHome, `${avdName}.avd`, 'config.ini');
22
+ if (!existsSync(configPath))
23
+ return undefined;
24
+ let content;
25
+ try {
26
+ content = await fs.readFile(configPath, 'utf-8');
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ const m = content.match(/^image\.sysdir\.1\s*=\s*(.+)$/m);
32
+ return m ? normalizeSysImagePath(m[1]) : undefined;
33
+ }
34
+ export function isAvdImageInstalled(image, androidHome) {
35
+ return existsSync(path.join(androidHome, image));
36
+ }
37
+ function candidateSdkRoots() {
38
+ const roots = [];
39
+ const managed = readManifest()?.androidHome;
40
+ if (managed)
41
+ roots.push(managed);
42
+ if (process.env.ANDROID_HOME?.trim())
43
+ roots.push(process.env.ANDROID_HOME);
44
+ const sys = systemAndroidHome();
45
+ if (sys)
46
+ roots.push(sys);
47
+ return [...new Set(roots)];
48
+ }
49
+ export async function resolveAvdSdk(avdName) {
50
+ const image = await readAvdSystemImage(avdName);
51
+ if (!image)
52
+ return {};
53
+ const sdkRoot = candidateSdkRoots().find((r) => isAvdImageInstalled(image, r));
54
+ return { image, sdkRoot };
55
+ }
56
+ export async function androidEnvForAvd(avdName) {
57
+ const base = managedEnv();
58
+ if (!base || !avdName)
59
+ return base;
60
+ const managedHome = base.ANDROID_HOME;
61
+ const image = await readAvdSystemImage(avdName);
62
+ if (!image || isAvdImageInstalled(image, managedHome))
63
+ return base;
64
+ const sys = systemAndroidHome();
65
+ if (!sys || !isAvdImageInstalled(image, sys))
66
+ return base;
67
+ const binDirs = [
68
+ path.join(base.JAVA_HOME, 'bin'),
69
+ path.join(sys, 'platform-tools'),
70
+ path.join(sys, 'cmdline-tools', 'latest', 'bin'),
71
+ path.join(sys, 'emulator'),
72
+ ];
73
+ const appiumBin = readManifest()?.appiumBin;
74
+ if (appiumBin && existsSync(appiumBin))
75
+ binDirs.push(appiumBin);
76
+ return {
77
+ ...base,
78
+ ANDROID_HOME: sys,
79
+ ANDROID_SDK_ROOT: sys,
80
+ PATH: binDirs.join(path.delimiter) + path.delimiter + (process.env.PATH ?? ''),
81
+ };
82
+ }
83
+ export async function avdBootPreflightError(avdName, androidHome = process.env.ANDROID_HOME) {
84
+ if (!androidHome)
85
+ return null;
86
+ const image = await readAvdSystemImage(avdName);
87
+ if (!image || isAvdImageInstalled(image, androidHome))
88
+ return null;
89
+ const sys = systemAndroidHome();
90
+ if (sys && isAvdImageInstalled(image, sys))
91
+ return null;
92
+ const sdkmanager = `sdkmanager "${image.replace(/\//g, ';')}"`;
93
+ const head = `Cannot boot AVD "${avdName}": its system image "${image}" is not in the active SDK (ANDROID_HOME=${androidHome}).`;
94
+ if (readManifest()) {
95
+ const manifest = path.join(path.dirname(androidHome), 'manifest.json');
96
+ return (`${head}\n` +
97
+ 'A managed taqwright toolchain is overriding ANDROID_HOME, but this AVD belongs to your system SDK. Fix one of:\n' +
98
+ ` (a) use your system SDK — \`rm ${manifest}\` (drops the managed override), then re-run;\n` +
99
+ ` (b) install the image into the managed SDK — \`${sdkmanager}\`;\n` +
100
+ " (c) point device.name at a managed AVD (e.g. 'taqwright_api34').");
101
+ }
102
+ return `${head}\nInstall it with \`${sdkmanager}\`, or recreate the AVD against an installed image.`;
103
+ }
@@ -0,0 +1,6 @@
1
+ export interface SetupOptions {
2
+ force?: boolean;
3
+ withAvd?: boolean;
4
+ printEnv?: boolean;
5
+ }
6
+ export declare function runSetup(opts?: SetupOptions): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { manifestPath, taqwrightHome, applyManagedEnv } from './paths.js';
3
+ import { installJdk } from './install-jdk.js';
4
+ import { installAndroidSdk, installAvd } from './install-android.js';
5
+ import { installAppium } from './install-appium.js';
6
+ import { runDoctorChecks } from '../doctor.js';
7
+ export async function runSetup(opts = {}) {
8
+ const force = !!opts.force;
9
+ const home = taqwrightHome();
10
+ mkdirSync(home, { recursive: true });
11
+ console.log(`taqwright install — vendoring the Android toolchain into:\n ${home}\n`);
12
+ console.log('Downloads ~700 MB (JDK + Android cmdline-tools + Appium). ' +
13
+ 'No sudo, and nothing is exported to your shell — your global JAVA_HOME, ' +
14
+ 'ANDROID_HOME and ~/.zshrc/.bashrc are left untouched; taqwright points only ' +
15
+ 'its own commands at the dir above.\n');
16
+ console.log('1/4 JDK (Temurin 21)');
17
+ const javaHome = await installJdk(force);
18
+ console.log(` ✓ JAVA_HOME (taqwright-only) → ${javaHome}\n`);
19
+ console.log('2/4 Android SDK (cmdline-tools + platform-tools/adb)');
20
+ const androidHome = await installAndroidSdk(force, javaHome);
21
+ console.log(` ✓ ANDROID_HOME (taqwright-only) → ${androidHome}\n`);
22
+ console.log(`3/4 Appium 3 + uiautomator2 driver${process.platform === 'darwin' ? ' (+ xcuitest)' : ''}`);
23
+ const appiumBin = await installAppium(force, androidHome, javaHome);
24
+ console.log(` ✓ appium at ${appiumBin}\n`);
25
+ writeFileSync(manifestPath(), JSON.stringify({ androidHome, javaHome, appiumBin }, null, 2) + '\n');
26
+ if (opts.withAvd) {
27
+ console.log('4/4 Android Virtual Device');
28
+ await installAvd(androidHome, javaHome);
29
+ console.log(' ✓ AVD `taqwright_api34` created — boot it with `emulator -avd taqwright_api34`\n' +
30
+ ' (it shows up in `npx taqwright devices`)\n');
31
+ }
32
+ else {
33
+ console.log('4/4 AVD — skipped (pass --with-avd to also create an emulator)\n');
34
+ }
35
+ applyManagedEnv();
36
+ console.log('Verifying with doctor:\n');
37
+ const checks = await runDoctorChecks();
38
+ for (const c of checks) {
39
+ const mark = c.status === 'ok' ? '[ok]' : c.status === 'warn' ? '[--]' : '[!!]';
40
+ console.log(` ${mark} ${c.name}${c.detail ? ' — ' + c.detail : ''}`);
41
+ }
42
+ console.log('\nAndroid is ready — `npx taqwright test` uses this toolchain automatically ' +
43
+ '(your shell environment is unchanged — no exports needed).');
44
+ console.log(process.platform === 'darwin'
45
+ ? 'The xcuitest driver is installed too; for iOS you still need Xcode + a booted ' +
46
+ 'simulator (cannot be auto-installed), plus Node 24.x or 25.x.'
47
+ : 'Still manual (cannot be auto-installed): Node 24.x or 25.x and the iOS/Xcode stack.');
48
+ if (opts.printEnv) {
49
+ console.log('\nTo also use this toolchain from your own shell, add:');
50
+ console.log(` export JAVA_HOME="${javaHome}"`);
51
+ console.log(` export ANDROID_HOME="${androidHome}"`);
52
+ console.log(' export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:' +
53
+ '$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"');
54
+ }
55
+ }
@@ -0,0 +1,2 @@
1
+ export declare function installAndroidSdk(force: boolean, javaHome: string): Promise<string>;
2
+ export declare function installAvd(androidHome: string, javaHome: string): Promise<void>;
@@ -0,0 +1,70 @@
1
+ import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { androidSdkDir, downloadCacheDir } from './paths.js';
4
+ import { download, extract } from './archive.js';
5
+ import { spawnTool } from './spawn-tool.js';
6
+ const CMDLINE_VERSION = '11076708';
7
+ function toolEnv(androidHome, javaHome) {
8
+ return {
9
+ ...process.env,
10
+ JAVA_HOME: javaHome,
11
+ ANDROID_HOME: androidHome,
12
+ ANDROID_SDK_ROOT: androidHome,
13
+ PATH: `${path.join(javaHome, 'bin')}${path.delimiter}${process.env.PATH ?? ''}`,
14
+ };
15
+ }
16
+ function sdkmanagerBin(sdk) {
17
+ const bin = process.platform === 'win32' ? 'sdkmanager.bat' : 'sdkmanager';
18
+ return path.join(sdk, 'cmdline-tools', 'latest', 'bin', bin);
19
+ }
20
+ function runTool(cmd, args, env, feedYes = false) {
21
+ return new Promise((resolve, reject) => {
22
+ const p = spawnTool(cmd, args, {
23
+ stdio: [feedYes ? 'pipe' : 'ignore', 'ignore', 'inherit'],
24
+ env,
25
+ });
26
+ if (feedYes && p.stdin) {
27
+ p.stdin.write('y\n'.repeat(50));
28
+ p.stdin.end();
29
+ }
30
+ p.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`${path.basename(cmd)} exited with code ${code}`)));
31
+ p.on('error', reject);
32
+ });
33
+ }
34
+ export async function installAndroidSdk(force, javaHome) {
35
+ const sdk = androidSdkDir();
36
+ if (force && existsSync(sdk))
37
+ rmSync(sdk, { recursive: true, force: true });
38
+ const env = toolEnv(sdk, javaHome);
39
+ const cmdlineLatest = path.join(sdk, 'cmdline-tools', 'latest', 'bin');
40
+ if (!existsSync(cmdlineLatest)) {
41
+ const osName = process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'win' : 'linux';
42
+ const url = `https://dl.google.com/android/repository/commandlinetools-${osName}-${CMDLINE_VERSION}_latest.zip`;
43
+ const archive = path.join(downloadCacheDir(), `cmdline-tools-${osName}.zip`);
44
+ console.log(` • downloading Android cmdline-tools (${osName})…`);
45
+ await download(url, archive);
46
+ console.log(' • extracting cmdline-tools…');
47
+ const ctRoot = path.join(sdk, 'cmdline-tools');
48
+ rmSync(ctRoot, { recursive: true, force: true });
49
+ mkdirSync(ctRoot, { recursive: true });
50
+ await extract(archive, ctRoot);
51
+ renameSync(path.join(ctRoot, 'cmdline-tools'), path.join(ctRoot, 'latest'));
52
+ }
53
+ const sm = sdkmanagerBin(sdk);
54
+ console.log(' • accepting Android SDK licenses…');
55
+ await runTool(sm, [`--sdk_root=${sdk}`, '--licenses'], env, true);
56
+ console.log(' • installing platform-tools (adb) + build-tools (aapt2)…');
57
+ await runTool(sm, [`--sdk_root=${sdk}`, 'platform-tools', 'build-tools;34.0.0'], env);
58
+ return sdk;
59
+ }
60
+ export async function installAvd(androidHome, javaHome) {
61
+ const env = toolEnv(androidHome, javaHome);
62
+ const abi = process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64';
63
+ const sysImage = `system-images;android-34;google_apis;${abi}`;
64
+ const sm = sdkmanagerBin(androidHome);
65
+ console.log(` • installing emulator + platform + ${sysImage} (~1 GB)…`);
66
+ await runTool(sm, [`--sdk_root=${androidHome}`, 'emulator', 'platforms;android-34', sysImage], env, true);
67
+ const avdmanager = path.join(androidHome, 'cmdline-tools', 'latest', 'bin', process.platform === 'win32' ? 'avdmanager.bat' : 'avdmanager');
68
+ console.log(' • creating AVD `taqwright_api34`…');
69
+ await runTool(avdmanager, ['create', 'avd', '-n', 'taqwright_api34', '-k', sysImage, '-d', 'pixel_7', '--force'], env, true);
70
+ }
@@ -0,0 +1 @@
1
+ export declare function installAppium(force: boolean, androidHome: string, javaHome: string): Promise<string>;
@@ -0,0 +1,64 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { appiumDir, appiumHomeDir } from './paths.js';
4
+ import { installDriver } from '../providers/appium.js';
5
+ import { spawnTool } from './spawn-tool.js';
6
+ function npm(args, cwd, env) {
7
+ return new Promise((resolve, reject) => {
8
+ const p = spawnTool('npm', args, { cwd, env, stdio: ['ignore', 'ignore', 'inherit'] });
9
+ p.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`npm ${args.join(' ')} exited with code ${code}`)));
10
+ p.on('error', reject);
11
+ });
12
+ }
13
+ export async function installAppium(force, androidHome, javaHome) {
14
+ const dir = appiumDir();
15
+ const appiumHome = appiumHomeDir();
16
+ const binDir = path.join(dir, 'node_modules', '.bin');
17
+ const appiumBin = path.join(binDir, process.platform === 'win32' ? 'appium.cmd' : 'appium');
18
+ const uia2Dir = path.join(appiumHome, 'node_modules', 'appium-uiautomator2-driver');
19
+ const xcuiDir = path.join(appiumHome, 'node_modules', 'appium-xcuitest-driver');
20
+ const env = {
21
+ ...process.env,
22
+ ANDROID_HOME: androidHome,
23
+ ANDROID_SDK_ROOT: androidHome,
24
+ JAVA_HOME: javaHome,
25
+ APPIUM_HOME: appiumHome,
26
+ PATH: `${path.join(javaHome, 'bin')}${path.delimiter}` +
27
+ `${path.join(androidHome, 'platform-tools')}${path.delimiter}` +
28
+ `${process.env.PATH ?? ''}`,
29
+ };
30
+ if (force && existsSync(dir))
31
+ rmSync(dir, { recursive: true, force: true });
32
+ if (force && existsSync(appiumHome))
33
+ rmSync(appiumHome, { recursive: true, force: true });
34
+ mkdirSync(dir, { recursive: true });
35
+ mkdirSync(appiumHome, { recursive: true });
36
+ const pkg = path.join(dir, 'package.json');
37
+ if (!existsSync(pkg)) {
38
+ writeFileSync(pkg, JSON.stringify({ name: 'taqwright-managed-appium', private: true }, null, 2) + '\n');
39
+ }
40
+ if (force || !existsSync(appiumBin)) {
41
+ console.log(' • installing Appium 3…');
42
+ await npm(['install', 'appium@^3'], dir, env);
43
+ }
44
+ if (force || !existsSync(uia2Dir)) {
45
+ console.log(' • installing the uiautomator2 driver…');
46
+ await installDriver('uiautomator2', { appiumPath: appiumBin, env });
47
+ }
48
+ else {
49
+ console.log(' • uiautomator2 driver already present — skipping');
50
+ }
51
+ if (process.platform === 'darwin') {
52
+ if (force || !existsSync(xcuiDir)) {
53
+ console.log(' • installing the xcuitest driver…');
54
+ await installDriver('xcuitest', { appiumPath: appiumBin, env });
55
+ }
56
+ else {
57
+ console.log(' • xcuitest driver already present — skipping');
58
+ }
59
+ }
60
+ else {
61
+ console.log(' • xcuitest driver — skipped (iOS needs macOS)');
62
+ }
63
+ return binDir;
64
+ }
@@ -0,0 +1 @@
1
+ export declare function installJdk(force: boolean): Promise<string>;