@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.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/auto-appium.d.ts +12 -0
- package/dist/auto-appium.js +77 -0
- package/dist/bin/branding.d.ts +6 -0
- package/dist/bin/branding.js +22 -0
- package/dist/bin/index.d.ts +2 -0
- package/dist/bin/index.js +321 -0
- package/dist/bin/init.d.ts +26 -0
- package/dist/bin/init.js +902 -0
- package/dist/bin/inspect.d.ts +9 -0
- package/dist/bin/inspect.js +91 -0
- package/dist/bin/report-branding.d.ts +2 -0
- package/dist/bin/report-branding.js +42 -0
- package/dist/branding-assets.d.ts +1 -0
- package/dist/branding-assets.js +1 -0
- package/dist/capabilities-helpers.d.ts +7 -0
- package/dist/capabilities-helpers.js +14 -0
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.js +86 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +235 -0
- package/dist/discovery-setup.d.ts +1 -0
- package/dist/discovery-setup.js +61 -0
- package/dist/discovery.d.ts +17 -0
- package/dist/discovery.js +55 -0
- package/dist/docs/configuration.html +376 -0
- package/dist/docs/custom-reporters.html +265 -0
- package/dist/docs/docker.html +339 -0
- package/dist/docs/docs.js +173 -0
- package/dist/docs/generating-tests.html +161 -0
- package/dist/docs/images/taqwright-html-report.png +0 -0
- package/dist/docs/index.html +13 -0
- package/dist/docs/installation.html +686 -0
- package/dist/docs/parallel.html +271 -0
- package/dist/docs/running-tests.html +385 -0
- package/dist/docs/styles.css +460 -0
- package/dist/docs/writing-tests.html +565 -0
- package/dist/doctor.d.ts +33 -0
- package/dist/doctor.js +508 -0
- package/dist/expect.d.ts +38 -0
- package/dist/expect.js +96 -0
- package/dist/fixture/artifact-mode.d.ts +2 -0
- package/dist/fixture/artifact-mode.js +7 -0
- package/dist/fixture/index.d.ts +15 -0
- package/dist/fixture/index.js +324 -0
- package/dist/images/taqwright-html-report.png +0 -0
- package/dist/images/taqwright_favicon.png +0 -0
- package/dist/images/taqwright_logo.png +0 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/inspector/codegen-appium.d.ts +3 -0
- package/dist/inspector/codegen-appium.js +228 -0
- package/dist/inspector/devices.d.ts +41 -0
- package/dist/inspector/devices.js +422 -0
- package/dist/inspector/locator-suggester.d.ts +23 -0
- package/dist/inspector/locator-suggester.js +539 -0
- package/dist/inspector/recorder.d.ts +128 -0
- package/dist/inspector/recorder.js +162 -0
- package/dist/inspector/server.d.ts +39 -0
- package/dist/inspector/server.js +1210 -0
- package/dist/inspector/session.d.ts +84 -0
- package/dist/inspector/session.js +262 -0
- package/dist/inspector/ui.d.ts +1 -0
- package/dist/inspector/ui.js +5508 -0
- package/dist/keys.d.ts +3 -0
- package/dist/keys.js +28 -0
- package/dist/locator/index.d.ts +206 -0
- package/dist/locator/index.js +1506 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +5 -0
- package/dist/mobile/index.d.ts +130 -0
- package/dist/mobile/index.js +762 -0
- package/dist/network/android.d.ts +5 -0
- package/dist/network/android.js +87 -0
- package/dist/network/ca.d.ts +10 -0
- package/dist/network/ca.js +136 -0
- package/dist/network/har.d.ts +90 -0
- package/dist/network/har.js +101 -0
- package/dist/network/host-proxy.d.ts +16 -0
- package/dist/network/host-proxy.js +134 -0
- package/dist/network/index.d.ts +26 -0
- package/dist/network/index.js +105 -0
- package/dist/network/ios-sim.d.ts +3 -0
- package/dist/network/ios-sim.js +29 -0
- package/dist/network/proxy.d.ts +13 -0
- package/dist/network/proxy.js +310 -0
- package/dist/providers/appium.d.ts +23 -0
- package/dist/providers/appium.js +288 -0
- package/dist/providers/browserstack/index.d.ts +5 -0
- package/dist/providers/browserstack/index.js +77 -0
- package/dist/providers/browserstack/utils.d.ts +1 -0
- package/dist/providers/browserstack/utils.js +6 -0
- package/dist/providers/cloud.d.ts +53 -0
- package/dist/providers/cloud.js +117 -0
- package/dist/providers/emulator/index.d.ts +8 -0
- package/dist/providers/emulator/index.js +47 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.js +33 -0
- package/dist/providers/lambdatest/index.d.ts +28 -0
- package/dist/providers/lambdatest/index.js +99 -0
- package/dist/providers/lambdatest/utils.d.ts +1 -0
- package/dist/providers/lambdatest/utils.js +6 -0
- package/dist/providers/local/index.d.ts +9 -0
- package/dist/providers/local/index.js +53 -0
- package/dist/providers/local-session.d.ts +16 -0
- package/dist/providers/local-session.js +55 -0
- package/dist/setup/archive.d.ts +2 -0
- package/dist/setup/archive.js +43 -0
- package/dist/setup/avd.d.ts +12 -0
- package/dist/setup/avd.js +103 -0
- package/dist/setup/index.d.ts +6 -0
- package/dist/setup/index.js +55 -0
- package/dist/setup/install-android.d.ts +2 -0
- package/dist/setup/install-android.js +70 -0
- package/dist/setup/install-appium.d.ts +1 -0
- package/dist/setup/install-appium.js +64 -0
- package/dist/setup/install-jdk.d.ts +1 -0
- package/dist/setup/install-jdk.js +58 -0
- package/dist/setup/paths.d.ts +16 -0
- package/dist/setup/paths.js +88 -0
- package/dist/setup/spawn-tool.d.ts +3 -0
- package/dist/setup/spawn-tool.js +11 -0
- package/dist/tracer/index.d.ts +34 -0
- package/dist/tracer/index.js +687 -0
- package/dist/tracer/proxy.d.ts +3 -0
- package/dist/tracer/proxy.js +60 -0
- package/dist/types/index.d.ts +189 -0
- package/dist/types/index.js +6 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +37 -0
- package/package.json +79 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawn, exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Platform } from '../types/index.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
import { getLatestBuildToolsVersions } from '../utils.js';
|
|
8
|
+
import { androidEnvForAvd } from '../setup/avd.js';
|
|
9
|
+
import { spawnTool } from '../setup/spawn-tool.js';
|
|
10
|
+
const execP = promisify(exec);
|
|
11
|
+
export async function installDriver(driverName, opts = {}) {
|
|
12
|
+
const env = opts.env ?? process.env;
|
|
13
|
+
const cmd = opts.appiumPath ?? 'npx';
|
|
14
|
+
const prefix = opts.appiumPath ? [] : ['appium'];
|
|
15
|
+
await new Promise((resolve) => {
|
|
16
|
+
const p = spawnTool(cmd, [...prefix, 'driver', 'uninstall', driverName], {
|
|
17
|
+
stdio: 'pipe',
|
|
18
|
+
env,
|
|
19
|
+
});
|
|
20
|
+
p.on('exit', () => resolve());
|
|
21
|
+
p.on('error', () => resolve());
|
|
22
|
+
});
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const p = spawnTool(cmd, [...prefix, 'driver', 'install', driverName], {
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
env,
|
|
27
|
+
});
|
|
28
|
+
let out = '';
|
|
29
|
+
const cap = (d) => {
|
|
30
|
+
out += d.toString();
|
|
31
|
+
};
|
|
32
|
+
p.stdout?.on('data', cap);
|
|
33
|
+
p.stderr?.on('data', cap);
|
|
34
|
+
p.on('exit', (code) => code === 0
|
|
35
|
+
? resolve()
|
|
36
|
+
: reject(new Error(`appium driver install ${driverName} exited ${code}` +
|
|
37
|
+
(out.trim() ? `\n${lastLines(out, 30)}` : ''))));
|
|
38
|
+
p.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function lastLines(s, n) {
|
|
42
|
+
return s.trimEnd().split('\n').slice(-n).join('\n');
|
|
43
|
+
}
|
|
44
|
+
export async function startAppiumServer(provider, opts = {}, avdName) {
|
|
45
|
+
const args = [];
|
|
46
|
+
if (opts.host)
|
|
47
|
+
args.push('--address', opts.host);
|
|
48
|
+
if (opts.port !== undefined)
|
|
49
|
+
args.push('--port', String(opts.port));
|
|
50
|
+
if (opts.basePath && opts.basePath !== '/')
|
|
51
|
+
args.push('--base-path', opts.basePath);
|
|
52
|
+
args.push('--allow-insecure=*:chromedriver_autodownload');
|
|
53
|
+
const env = { ...process.env, ...((await androidEnvForAvd(avdName)) ?? {}) };
|
|
54
|
+
return spawnAppium('appium', args, provider, env, avdName).catch((err) => {
|
|
55
|
+
if (err.code === 'ENOENT') {
|
|
56
|
+
return spawnAppium('npx', ['appium', ...args], provider, env, avdName);
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function spawnAppium(cmd, args, provider, env, avdName) {
|
|
62
|
+
let emulatorStartRequested = false;
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const proc = spawnTool(cmd, args, { stdio: 'pipe', env });
|
|
65
|
+
let settled = false;
|
|
66
|
+
let stderrText = '';
|
|
67
|
+
proc.stderr?.on('data', (data) => {
|
|
68
|
+
const text = data.toString();
|
|
69
|
+
stderrText += text;
|
|
70
|
+
logger.warn(text.trimEnd());
|
|
71
|
+
});
|
|
72
|
+
proc.on('exit', (code) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
const err = new Error(`Appium server "${cmd}" exited with code ${code ?? 'null'} before starting.`);
|
|
77
|
+
if (/not recognized|not found|ENOENT/i.test(stderrText))
|
|
78
|
+
err.code = 'ENOENT';
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
proc.stdout?.on('data', async (data) => {
|
|
82
|
+
const output = data.toString();
|
|
83
|
+
logger.log(output.trimEnd());
|
|
84
|
+
if (output.includes('Error: listen EADDRINUSE')) {
|
|
85
|
+
settled = true;
|
|
86
|
+
reject(new Error('Appium server is already running on this port. Stop it before running tests.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (output.includes('Could not find online devices')) {
|
|
90
|
+
if (!emulatorStartRequested && provider === 'emulator') {
|
|
91
|
+
emulatorStartRequested = true;
|
|
92
|
+
await startAndroidEmulator(avdName, env.ANDROID_HOME).catch((err) => logger.error(err));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (output.includes('Appium REST http interface listener started')) {
|
|
96
|
+
settled = true;
|
|
97
|
+
logger.log('Appium server is up and running.');
|
|
98
|
+
resolve(proc);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
proc.on('error', (err) => {
|
|
102
|
+
if (settled)
|
|
103
|
+
return;
|
|
104
|
+
settled = true;
|
|
105
|
+
reject(err);
|
|
106
|
+
});
|
|
107
|
+
process.on('exit', () => proc.kill());
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function stopAppiumServer() {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
exec('pkill -f appium', (err, stdout) => {
|
|
113
|
+
if (err)
|
|
114
|
+
return reject(err);
|
|
115
|
+
resolve(stdout);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function getListenerPids(port) {
|
|
120
|
+
try {
|
|
121
|
+
const { stdout } = await execP(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
122
|
+
return stdout
|
|
123
|
+
.trim()
|
|
124
|
+
.split('\n')
|
|
125
|
+
.map((l) => Number.parseInt(l.trim(), 10))
|
|
126
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function isAppiumProcess(pid) {
|
|
133
|
+
try {
|
|
134
|
+
const { stdout } = await execP(`ps -p ${pid} -o command=`);
|
|
135
|
+
return /appium/i.test(stdout);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function killAppiumOnPort(port) {
|
|
142
|
+
let killed = false;
|
|
143
|
+
try {
|
|
144
|
+
const pids = await getListenerPids(port);
|
|
145
|
+
for (const pid of pids) {
|
|
146
|
+
if (!(await isAppiumProcess(pid)))
|
|
147
|
+
continue;
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 'SIGTERM');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
killed = true;
|
|
155
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, 0);
|
|
158
|
+
process.kill(pid, 'SIGKILL');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.warn(`Failed to kill Appium on port ${port}: ${String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
return killed;
|
|
168
|
+
}
|
|
169
|
+
export function isEmulatorInstalled(platform) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
if (platform !== Platform.ANDROID) {
|
|
172
|
+
return resolve(true);
|
|
173
|
+
}
|
|
174
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
175
|
+
if (!androidHome) {
|
|
176
|
+
return reject(new Error('ANDROID_HOME is not set.'));
|
|
177
|
+
}
|
|
178
|
+
const emulatorPath = path.join(androidHome, 'emulator', 'emulator');
|
|
179
|
+
exec(`${emulatorPath} -list-avds`, (err, stdout) => {
|
|
180
|
+
if (err) {
|
|
181
|
+
return reject(new Error('Could not list emulators. Install one via Android Studio AVD Manager.'));
|
|
182
|
+
}
|
|
183
|
+
const lines = stdout
|
|
184
|
+
.trim()
|
|
185
|
+
.split('\n')
|
|
186
|
+
.filter((l) => l.trim() && !l.startsWith('INFO') && !l.includes('/tmp/'));
|
|
187
|
+
if (lines.length === 0) {
|
|
188
|
+
return reject(new Error('No installed emulators found. Create one via Android Studio AVD Manager.'));
|
|
189
|
+
}
|
|
190
|
+
resolve(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export async function startAndroidEmulator(avdName, androidHome = process.env.ANDROID_HOME) {
|
|
195
|
+
if (!androidHome)
|
|
196
|
+
throw new Error('ANDROID_HOME is not set.');
|
|
197
|
+
const emulatorPath = path.join(androidHome, 'emulator', 'emulator');
|
|
198
|
+
const { stdout } = await execP(`${emulatorPath} -list-avds`);
|
|
199
|
+
const avds = stdout
|
|
200
|
+
.trim()
|
|
201
|
+
.split('\n')
|
|
202
|
+
.filter((l) => l.trim() && !l.startsWith('INFO') && !l.includes('/tmp/'));
|
|
203
|
+
if (avds.length === 0) {
|
|
204
|
+
throw new Error('No installed emulators found.');
|
|
205
|
+
}
|
|
206
|
+
const avd = avdName && avds.includes(avdName) ? avdName : avds[0];
|
|
207
|
+
logger.log(`Starting emulator: ${avd}`);
|
|
208
|
+
const child = spawn(emulatorPath, ['-avd', avd], { stdio: 'pipe' });
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
child.stdout?.on('data', (data) => {
|
|
211
|
+
const out = data.toString();
|
|
212
|
+
logger.log(`Emulator: ${out.trimEnd()}`);
|
|
213
|
+
if (out.includes("Successfully loaded snapshot 'default_boot'")) {
|
|
214
|
+
resolve();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
child.on('error', reject);
|
|
218
|
+
child.on('close', (code) => {
|
|
219
|
+
if (code !== 0)
|
|
220
|
+
reject(new Error(`Emulator exited with code ${code}`));
|
|
221
|
+
});
|
|
222
|
+
process.on('exit', () => child.kill());
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export function getAppBundleId(buildPath) {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
exec(`osascript -e 'id of app "${buildPath}"'`, (err, stdout, stderr) => {
|
|
228
|
+
if (err)
|
|
229
|
+
return reject(err);
|
|
230
|
+
if (stderr)
|
|
231
|
+
return reject(new Error(stderr));
|
|
232
|
+
const id = stdout.trim();
|
|
233
|
+
if (!id)
|
|
234
|
+
return reject(new Error('Bundle ID not found'));
|
|
235
|
+
resolve(id);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
export async function getConnectedIOSDeviceUDID() {
|
|
240
|
+
const { stdout } = await execP('xcrun xctrace list devices');
|
|
241
|
+
const realDevices = stdout
|
|
242
|
+
.split('\n')
|
|
243
|
+
.filter((l) => l.includes('iPhone') && !l.includes('Simulator'));
|
|
244
|
+
if (realDevices.length === 0) {
|
|
245
|
+
throw new Error('No connected iPhone detected. Make sure the device is plugged in and trusted.');
|
|
246
|
+
}
|
|
247
|
+
const m = realDevices[0].match(/\(([\da-fA-F-]+)\)$/);
|
|
248
|
+
if (!m || !m[1]) {
|
|
249
|
+
throw new Error('Could not parse UDID from xctrace output.');
|
|
250
|
+
}
|
|
251
|
+
return m[1];
|
|
252
|
+
}
|
|
253
|
+
export async function getActiveAndroidDevices() {
|
|
254
|
+
const { stdout } = await execP('adb devices');
|
|
255
|
+
return stdout
|
|
256
|
+
.trim()
|
|
257
|
+
.split('\n')
|
|
258
|
+
.filter((l) => l.includes('\tdevice')).length;
|
|
259
|
+
}
|
|
260
|
+
async function getLatestBuildToolsVersion() {
|
|
261
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
262
|
+
if (!androidHome)
|
|
263
|
+
throw new Error('ANDROID_HOME is not set.');
|
|
264
|
+
const buildToolsPath = path.join(androidHome, 'build-tools');
|
|
265
|
+
const files = await readdir(buildToolsPath);
|
|
266
|
+
const versions = files.filter((f) => /^\d+\.\d+\.\d+(-rc\d+)?$/.test(f));
|
|
267
|
+
const latest = getLatestBuildToolsVersions(versions);
|
|
268
|
+
if (!latest) {
|
|
269
|
+
throw new Error(`No valid build-tools found in ${buildToolsPath}. Install via Android Studio SDK Manager.`);
|
|
270
|
+
}
|
|
271
|
+
return latest;
|
|
272
|
+
}
|
|
273
|
+
export async function getApkDetails(buildPath) {
|
|
274
|
+
const androidHome = process.env.ANDROID_HOME;
|
|
275
|
+
if (!androidHome)
|
|
276
|
+
throw new Error('ANDROID_HOME is not set.');
|
|
277
|
+
const buildTools = await getLatestBuildToolsVersion();
|
|
278
|
+
const aapt = path.join(androidHome, 'build-tools', buildTools, 'aapt');
|
|
279
|
+
const { stdout, stderr } = await execP(`${aapt} dump badging "${buildPath}"`);
|
|
280
|
+
if (stderr)
|
|
281
|
+
logger.warn(`aapt: ${stderr.trim()}`);
|
|
282
|
+
const pkg = stdout.match(/package: name='(\S+)'/);
|
|
283
|
+
const activity = stdout.match(/launchable-activity: name='(\S+)'/);
|
|
284
|
+
if (!pkg || !activity) {
|
|
285
|
+
throw new Error(`Unable to parse package / launchable-activity from APK: ${buildPath}`);
|
|
286
|
+
}
|
|
287
|
+
return { packageName: pkg[1], launchableActivity: activity[1] };
|
|
288
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TaqwrightUseOptions } from '../../types/index.js';
|
|
2
|
+
import { CloudProvider } from '../cloud.js';
|
|
3
|
+
export declare class BrowserStackDeviceProvider extends CloudProvider {
|
|
4
|
+
constructor(use: TaqwrightUseOptions, appBundleId: string | undefined, projectName?: string);
|
|
5
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Platform, } from '../../types/index.js';
|
|
2
|
+
import { CloudProvider } from '../cloud.js';
|
|
3
|
+
const APP_AUTOMATE = 'https://api-cloud.browserstack.com/app-automate';
|
|
4
|
+
function buildCapabilities(use, projectName, appUrl) {
|
|
5
|
+
const platform = use.platform;
|
|
6
|
+
const device = use.device;
|
|
7
|
+
const userCaps = { ...(use.capabilities ?? {}) };
|
|
8
|
+
const userBstack = userCaps['bstack:options'] ?? {};
|
|
9
|
+
delete userCaps['bstack:options'];
|
|
10
|
+
const ciLabel = process.env.GITHUB_ACTIONS === 'true' ? `CI ${process.env.GITHUB_RUN_ID}` : process.env.USER;
|
|
11
|
+
return {
|
|
12
|
+
platformName: platform === Platform.IOS ? 'iOS' : 'Android',
|
|
13
|
+
'bstack:options': {
|
|
14
|
+
debug: true,
|
|
15
|
+
interactiveDebugging: true,
|
|
16
|
+
networkLogs: true,
|
|
17
|
+
enableCameraImageInjection: device.enableCameraImageInjection,
|
|
18
|
+
idleTimeout: 180,
|
|
19
|
+
deviceName: device.name,
|
|
20
|
+
osVersion: device.osVersion,
|
|
21
|
+
deviceOrientation: device.orientation,
|
|
22
|
+
buildName: `${projectName} ${platform}`,
|
|
23
|
+
sessionName: `${projectName} ${platform} test`,
|
|
24
|
+
buildIdentifier: ciLabel,
|
|
25
|
+
...userBstack,
|
|
26
|
+
},
|
|
27
|
+
'appium:deviceName': device.name,
|
|
28
|
+
'appium:platformVersion': device.osVersion,
|
|
29
|
+
'appium:automationName': platform === Platform.IOS ? 'XCUITest' : 'UiAutomator2',
|
|
30
|
+
'appium:autoGrantPermissions': true,
|
|
31
|
+
'appium:app': appUrl,
|
|
32
|
+
'appium:autoAcceptAlerts': true,
|
|
33
|
+
'appium:fullReset': true,
|
|
34
|
+
...(platform === Platform.IOS ? { 'appium:settings[snapshotMaxDepth]': 62 } : {}),
|
|
35
|
+
...userCaps,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const browserStackSpec = {
|
|
39
|
+
provider: 'browserstack',
|
|
40
|
+
credentialEnv: ['BROWSERSTACK_USERNAME', 'BROWSERSTACK_ACCESS_KEY'],
|
|
41
|
+
prebuiltScheme: 'bs://',
|
|
42
|
+
appUrlEnvVar: (projectName) => `BROWSERSTACK_APP_URL_${projectName.toUpperCase()}`,
|
|
43
|
+
upload: {
|
|
44
|
+
endpoint: `${APP_AUTOMATE}/upload`,
|
|
45
|
+
urlBody: (buildPath) => new URLSearchParams({ url: buildPath }),
|
|
46
|
+
fileBody: (file, fileName) => {
|
|
47
|
+
const form = new FormData();
|
|
48
|
+
form.append('file', new Blob([new Uint8Array(file)]), fileName);
|
|
49
|
+
return form;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
hub: { hostname: 'hub.browserstack.com', port: 443, path: '/wd/hub', protocol: 'https' },
|
|
53
|
+
buildCapabilities: ({ use, projectName, appUrl }) => buildCapabilities(use, projectName, appUrl),
|
|
54
|
+
syncRequest: (sessionId, details) => ({
|
|
55
|
+
url: `${APP_AUTOMATE}/sessions/${sessionId}.json`,
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
body: details.status
|
|
58
|
+
? JSON.stringify({ status: details.status, reason: details.reason })
|
|
59
|
+
: JSON.stringify({ name: details.name }),
|
|
60
|
+
}),
|
|
61
|
+
strictSync: true,
|
|
62
|
+
resolveBundleId: async (sessionId, authHeader) => {
|
|
63
|
+
const res = await fetch(`${APP_AUTOMATE}/sessions/${sessionId}.json`, {
|
|
64
|
+
headers: { Authorization: authHeader },
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`Error fetching session details: ${res.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
const data = (await res.json());
|
|
70
|
+
return data.automation_session?.app_details?.app_name ?? '';
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export class BrowserStackDeviceProvider extends CloudProvider {
|
|
74
|
+
constructor(use, appBundleId, projectName) {
|
|
75
|
+
super(browserStackSpec, use, appBundleId, projectName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getAuthHeader(): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type TaqwrightUseOptions, type DeviceHandle, type DeviceProvider } from '../types/index.js';
|
|
2
|
+
export interface CloudSpec {
|
|
3
|
+
readonly provider: 'browserstack' | 'lambdatest';
|
|
4
|
+
readonly credentialEnv: readonly [string, string];
|
|
5
|
+
readonly prebuiltScheme: string;
|
|
6
|
+
appUrlEnvVar(projectName: string): string;
|
|
7
|
+
readonly upload: {
|
|
8
|
+
readonly endpoint: string;
|
|
9
|
+
urlBody(buildPath: string, projectName: string): URLSearchParams;
|
|
10
|
+
fileBody(file: Buffer, fileName: string, projectName: string): FormData;
|
|
11
|
+
};
|
|
12
|
+
readonly hub: {
|
|
13
|
+
readonly hostname: string;
|
|
14
|
+
readonly port: number;
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly protocol: 'https';
|
|
17
|
+
};
|
|
18
|
+
buildCapabilities(args: {
|
|
19
|
+
use: TaqwrightUseOptions;
|
|
20
|
+
projectName: string;
|
|
21
|
+
appUrl: string;
|
|
22
|
+
}): Record<string, unknown>;
|
|
23
|
+
syncRequest(sessionId: string, details: {
|
|
24
|
+
status?: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
}): {
|
|
28
|
+
url: string;
|
|
29
|
+
method: string;
|
|
30
|
+
body: string;
|
|
31
|
+
};
|
|
32
|
+
readonly strictSync: boolean;
|
|
33
|
+
resolveBundleId?(sessionId: string, authHeader: string): Promise<string | undefined>;
|
|
34
|
+
readonly requireBundleId?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function basicAuth(user: string | undefined, key: string | undefined): string;
|
|
37
|
+
export declare class CloudProvider implements DeviceProvider {
|
|
38
|
+
private readonly spec;
|
|
39
|
+
protected readonly use: TaqwrightUseOptions;
|
|
40
|
+
protected readonly appBundleId: string | undefined;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
protected readonly projectName: string;
|
|
43
|
+
constructor(spec: CloudSpec, use: TaqwrightUseOptions, appBundleId: string | undefined, projectName?: string);
|
|
44
|
+
private authHeader;
|
|
45
|
+
globalSetup(): Promise<void>;
|
|
46
|
+
private resolveAppUrl;
|
|
47
|
+
getDevice(): Promise<DeviceHandle>;
|
|
48
|
+
syncTestDetails(details: {
|
|
49
|
+
status?: string;
|
|
50
|
+
reason?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
export function basicAuth(user, key) {
|
|
5
|
+
return `Basic ${Buffer.from(`${user}:${key}`).toString('base64')}`;
|
|
6
|
+
}
|
|
7
|
+
export class CloudProvider {
|
|
8
|
+
spec;
|
|
9
|
+
use;
|
|
10
|
+
appBundleId;
|
|
11
|
+
sessionId;
|
|
12
|
+
projectName;
|
|
13
|
+
constructor(spec, use, appBundleId, projectName) {
|
|
14
|
+
this.spec = spec;
|
|
15
|
+
this.use = use;
|
|
16
|
+
this.appBundleId = appBundleId;
|
|
17
|
+
if (use.device.provider !== spec.provider) {
|
|
18
|
+
throw new Error(`${spec.provider} provider received device.provider='${use.device.provider}'.`);
|
|
19
|
+
}
|
|
20
|
+
if (spec.requireBundleId && !appBundleId) {
|
|
21
|
+
throw new Error(`appBundleId is required for the ${spec.provider} provider — set it on the project.`);
|
|
22
|
+
}
|
|
23
|
+
this.projectName = projectName ?? path.basename(process.cwd());
|
|
24
|
+
}
|
|
25
|
+
authHeader() {
|
|
26
|
+
const [userVar, keyVar] = this.spec.credentialEnv;
|
|
27
|
+
return basicAuth(process.env[userVar], process.env[keyVar]);
|
|
28
|
+
}
|
|
29
|
+
async globalSetup() {
|
|
30
|
+
if (!this.use.buildPath) {
|
|
31
|
+
throw new Error('Build path not found. Set `buildPath` in your taqwright config.');
|
|
32
|
+
}
|
|
33
|
+
const [userVar, keyVar] = this.spec.credentialEnv;
|
|
34
|
+
if (!(process.env[userVar] && process.env[keyVar])) {
|
|
35
|
+
throw new Error(`${userVar} and ${keyVar} are required for the ${this.spec.provider} provider.`);
|
|
36
|
+
}
|
|
37
|
+
const appUrl = await this.resolveAppUrl(this.use.buildPath);
|
|
38
|
+
process.env[this.spec.appUrlEnvVar(this.projectName)] = appUrl;
|
|
39
|
+
}
|
|
40
|
+
async resolveAppUrl(buildPath) {
|
|
41
|
+
if (buildPath.startsWith(this.spec.prebuiltScheme)) {
|
|
42
|
+
return buildPath;
|
|
43
|
+
}
|
|
44
|
+
let body;
|
|
45
|
+
if (buildPath.startsWith('http')) {
|
|
46
|
+
body = this.spec.upload.urlBody(buildPath, this.projectName);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (!fs.existsSync(buildPath)) {
|
|
50
|
+
throw new Error(`Build file not found: ${buildPath}`);
|
|
51
|
+
}
|
|
52
|
+
const bytes = await fs.promises.readFile(buildPath);
|
|
53
|
+
body = this.spec.upload.fileBody(bytes, path.basename(buildPath), this.projectName);
|
|
54
|
+
}
|
|
55
|
+
logger.log(`Uploading: ${buildPath}`);
|
|
56
|
+
const res = await fetch(this.spec.upload.endpoint, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { Authorization: this.authHeader() },
|
|
59
|
+
body,
|
|
60
|
+
});
|
|
61
|
+
const data = (await res.json());
|
|
62
|
+
if (!data.app_url) {
|
|
63
|
+
logger.error('Build upload did not return an app URL:', data);
|
|
64
|
+
}
|
|
65
|
+
return data.app_url;
|
|
66
|
+
}
|
|
67
|
+
async getDevice() {
|
|
68
|
+
const device = this.use.device;
|
|
69
|
+
if (!device.name || !device.osVersion) {
|
|
70
|
+
throw new Error(`device.name and device.osVersion are required for the ${this.spec.provider} provider.`);
|
|
71
|
+
}
|
|
72
|
+
const envVar = this.spec.appUrlEnvVar(this.projectName);
|
|
73
|
+
const appUrl = process.env[envVar];
|
|
74
|
+
if (!appUrl) {
|
|
75
|
+
throw new Error(`process.env.${envVar} is not set — did the build upload run?`);
|
|
76
|
+
}
|
|
77
|
+
const [userVar, keyVar] = this.spec.credentialEnv;
|
|
78
|
+
const connection = {
|
|
79
|
+
...this.spec.hub,
|
|
80
|
+
logLevel: 'warn',
|
|
81
|
+
connectionRetryTimeout: this.use.appium?.connectionTimeout ?? 300_000,
|
|
82
|
+
user: process.env[userVar],
|
|
83
|
+
key: process.env[keyVar],
|
|
84
|
+
capabilities: this.spec.buildCapabilities({
|
|
85
|
+
use: this.use,
|
|
86
|
+
projectName: this.projectName,
|
|
87
|
+
appUrl,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
const WebDriver = (await import('webdriver')).default;
|
|
91
|
+
const driver = await WebDriver.newSession(connection);
|
|
92
|
+
this.sessionId = driver.sessionId;
|
|
93
|
+
const bundleId = this.spec.resolveBundleId
|
|
94
|
+
? await this.spec.resolveBundleId(this.sessionId, this.authHeader())
|
|
95
|
+
: this.appBundleId;
|
|
96
|
+
return {
|
|
97
|
+
driver,
|
|
98
|
+
bundleId,
|
|
99
|
+
options: { expectTimeout: this.use.expectTimeout ?? 30_000 },
|
|
100
|
+
provider: this.spec.provider,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async syncTestDetails(details) {
|
|
104
|
+
if (!this.sessionId) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const { url, method, body } = this.spec.syncRequest(this.sessionId, details);
|
|
108
|
+
const res = await fetch(url, {
|
|
109
|
+
method,
|
|
110
|
+
headers: { Authorization: this.authHeader(), 'Content-Type': 'application/json' },
|
|
111
|
+
body,
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok && this.spec.strictSync) {
|
|
114
|
+
throw new Error(`Failed to sync ${this.spec.provider} session details: ${res.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TaqwrightUseOptions, type DeviceHandle, type DeviceProvider } from '../../types/index.js';
|
|
2
|
+
export declare class EmulatorProvider implements DeviceProvider {
|
|
3
|
+
private readonly use;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
constructor(use: TaqwrightUseOptions, appBundleId?: string);
|
|
6
|
+
globalSetup(): Promise<void>;
|
|
7
|
+
getDevice(): Promise<DeviceHandle>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Platform, } from '../../types/index.js';
|
|
2
|
+
import { isEmulatorInstalled } 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 EmulatorProvider {
|
|
7
|
+
use;
|
|
8
|
+
sessionId;
|
|
9
|
+
constructor(use, appBundleId) {
|
|
10
|
+
this.use = use;
|
|
11
|
+
if (use.device.provider !== 'emulator') {
|
|
12
|
+
throw new Error(`emulator provider received device.provider='${use.device.provider}'.`);
|
|
13
|
+
}
|
|
14
|
+
if (appBundleId) {
|
|
15
|
+
logger.log(`Bundle id (${appBundleId}) ignored for emulator 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' : '.app');
|
|
21
|
+
if (android) {
|
|
22
|
+
if (!process.env.ANDROID_HOME) {
|
|
23
|
+
throw new Error('ANDROID_HOME is not set. Required to locate the Android SDK.');
|
|
24
|
+
}
|
|
25
|
+
if (!process.env.JAVA_HOME) {
|
|
26
|
+
throw new Error('JAVA_HOME is not set.');
|
|
27
|
+
}
|
|
28
|
+
await isEmulatorInstalled(this.use.platform);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async getDevice() {
|
|
32
|
+
const device = this.use.device;
|
|
33
|
+
const { appPackage, appActivity } = await resolveAndroidApp(this.use);
|
|
34
|
+
const capabilities = buildLocalCapabilities(this.use, device, {
|
|
35
|
+
appPackage,
|
|
36
|
+
appActivity,
|
|
37
|
+
udid: device.udid,
|
|
38
|
+
extraCaps: {
|
|
39
|
+
'appium:platformVersion': device.osVersion,
|
|
40
|
+
'appium:wdaLaunchTimeout': 300_000,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const driver = await openLocalSession(this.use, capabilities);
|
|
44
|
+
this.sessionId = driver.sessionId;
|
|
45
|
+
return makeHandle(this.use, driver, undefined);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TaqwrightUseOptions, type DeviceProvider } from '../types/index.js';
|
|
2
|
+
export { EmulatorProvider } from './emulator/index.js';
|
|
3
|
+
export { LocalDeviceProvider } from './local/index.js';
|
|
4
|
+
export { BrowserStackDeviceProvider } from './browserstack/index.js';
|
|
5
|
+
export { LambdaTestDeviceProvider } from './lambdatest/index.js';
|
|
6
|
+
export type { DeviceHandle } from '../types/index.js';
|
|
7
|
+
type ProviderConstructor = new (use: TaqwrightUseOptions, appBundleId: string | undefined, projectName?: string) => DeviceProvider;
|
|
8
|
+
export declare function isCloudProvider(provider: string | undefined): boolean;
|
|
9
|
+
export declare function getProviderClass(provider: string): ProviderConstructor;
|
|
10
|
+
export declare function createDeviceProvider(use: TaqwrightUseOptions, projectName?: string): DeviceProvider;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EmulatorProvider } from './emulator/index.js';
|
|
2
|
+
import { LocalDeviceProvider } from './local/index.js';
|
|
3
|
+
import { BrowserStackDeviceProvider } from './browserstack/index.js';
|
|
4
|
+
import { LambdaTestDeviceProvider } from './lambdatest/index.js';
|
|
5
|
+
export { EmulatorProvider } from './emulator/index.js';
|
|
6
|
+
export { LocalDeviceProvider } from './local/index.js';
|
|
7
|
+
export { BrowserStackDeviceProvider } from './browserstack/index.js';
|
|
8
|
+
export { LambdaTestDeviceProvider } from './lambdatest/index.js';
|
|
9
|
+
const REGISTRY = {
|
|
10
|
+
emulator: EmulatorProvider,
|
|
11
|
+
'local-device': LocalDeviceProvider,
|
|
12
|
+
browserstack: BrowserStackDeviceProvider,
|
|
13
|
+
lambdatest: LambdaTestDeviceProvider,
|
|
14
|
+
};
|
|
15
|
+
const CLOUD_PROVIDERS = new Set(['browserstack', 'lambdatest']);
|
|
16
|
+
export function isCloudProvider(provider) {
|
|
17
|
+
return provider !== undefined && CLOUD_PROVIDERS.has(provider);
|
|
18
|
+
}
|
|
19
|
+
export function getProviderClass(provider) {
|
|
20
|
+
const ctor = REGISTRY[provider];
|
|
21
|
+
if (!ctor) {
|
|
22
|
+
throw new Error(`No device provider registered for "${provider}".`);
|
|
23
|
+
}
|
|
24
|
+
return ctor;
|
|
25
|
+
}
|
|
26
|
+
export function createDeviceProvider(use, projectName) {
|
|
27
|
+
const provider = use.device?.provider;
|
|
28
|
+
if (!provider) {
|
|
29
|
+
throw new Error('device.provider is not set — add it to your taqwright config.');
|
|
30
|
+
}
|
|
31
|
+
const ProviderClass = getProviderClass(provider);
|
|
32
|
+
return new ProviderClass(use, use.appBundleId, projectName);
|
|
33
|
+
}
|