@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
package/dist/bin/init.js
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import { mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { resolve, join, basename, relative, dirname } from 'node:path';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { stdin, stdout } from 'node:process';
|
|
8
|
+
import { runSetup } from '../setup/index.js';
|
|
9
|
+
import { download } from '../setup/archive.js';
|
|
10
|
+
import { spawnTool } from '../setup/spawn-tool.js';
|
|
11
|
+
import { detectAndroidToolchain, listAvds } from '../doctor.js';
|
|
12
|
+
const execP = promisify(exec);
|
|
13
|
+
const DEMO_APK_FILENAME = 'DemoApp-v1.0.0.apk';
|
|
14
|
+
const DEMO_APP_BUNDLE_ID = 'com.taqelah.demo_app';
|
|
15
|
+
const DEMO_APK_URL = 'https://github.com/taqelah/demo-app/releases/download/v1.0.0/DemoApp-v1.0.0.apk';
|
|
16
|
+
const DEMO_AVD_NAME = 'taqwright_api34';
|
|
17
|
+
export async function runInit(argDir, opts = {}) {
|
|
18
|
+
const scripted = argDir !== undefined &&
|
|
19
|
+
opts.testDir !== undefined &&
|
|
20
|
+
opts.platform !== undefined &&
|
|
21
|
+
opts.install !== undefined;
|
|
22
|
+
const interactive = Boolean(stdin.isTTY) && !scripted;
|
|
23
|
+
const isMac = process.platform === 'darwin';
|
|
24
|
+
console.log('\ntaqwright init β scaffold a new project\n');
|
|
25
|
+
let targetDir;
|
|
26
|
+
let testDir;
|
|
27
|
+
let platforms;
|
|
28
|
+
let install;
|
|
29
|
+
let installToolchain;
|
|
30
|
+
let withAvd;
|
|
31
|
+
let demoApp;
|
|
32
|
+
let deviceAvdName;
|
|
33
|
+
let detected;
|
|
34
|
+
if (!interactive) {
|
|
35
|
+
const dirInput = argDir ?? './taqwright-tests';
|
|
36
|
+
targetDir = resolve(process.cwd(), dirInput);
|
|
37
|
+
const locErr = projectLocationError(targetDir);
|
|
38
|
+
if (locErr) {
|
|
39
|
+
console.error(`error: ${locErr}.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (existsSync(targetDir) && (await isNonEmpty(targetDir)) && !opts.yes) {
|
|
43
|
+
console.error(`error: "${targetDir}" is not empty. Re-run with --yes to write into it anyway.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
testDir = opts.testDir ?? 'tests';
|
|
47
|
+
if (!isValidTestDir(testDir)) {
|
|
48
|
+
console.error(`error: invalid --test-dir "${testDir}" β ${TEST_DIR_HINT}.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
platforms =
|
|
52
|
+
opts.platform === 'both' ? ['android', 'ios'] : [(opts.platform ?? 'android')];
|
|
53
|
+
install = opts.install ?? true;
|
|
54
|
+
installToolchain = opts.installToolchain ?? false;
|
|
55
|
+
withAvd = (opts.withAvd ?? false) && installToolchain;
|
|
56
|
+
demoApp = (opts.demoApp ?? false) && platforms.includes('android');
|
|
57
|
+
if (!stdin.isTTY && !scripted) {
|
|
58
|
+
console.log(`(no TTY β running non-interactively: dir=${dirInput}, testDir=${testDir}, ` +
|
|
59
|
+
`platform=${opts.platform ?? 'android'}, install=${install})\n`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
64
|
+
console.log('Press Enter to accept the default shown in (parentheses).\n');
|
|
65
|
+
try {
|
|
66
|
+
targetDir = await askProjectDir(rl, argDir);
|
|
67
|
+
if (existsSync(targetDir) && (await isNonEmpty(targetDir)) && !opts.yes) {
|
|
68
|
+
const proceed = await yesNo(rl, `Directory "${targetDir}" is not empty β continue and write into it?`, false);
|
|
69
|
+
if (!proceed) {
|
|
70
|
+
console.log('aborted.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
testDir = opts.testDir ?? (await askTestDir(rl));
|
|
75
|
+
if (!isValidTestDir(testDir)) {
|
|
76
|
+
console.error(`error: invalid --test-dir "${testDir}" β ${TEST_DIR_HINT}.`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const platformInput = opts.platform ??
|
|
80
|
+
(await askChoice(rl, 'Platform', platformChoices(isMac), 'android'));
|
|
81
|
+
platforms = platformInput === 'both' ? ['android', 'ios'] : [platformInput];
|
|
82
|
+
install = opts.install ?? true;
|
|
83
|
+
if (opts.installToolchain !== undefined) {
|
|
84
|
+
installToolchain = opts.installToolchain;
|
|
85
|
+
}
|
|
86
|
+
else if (!platforms.includes('android')) {
|
|
87
|
+
installToolchain = false;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
detected = await detectAndroidToolchain();
|
|
91
|
+
printAndroidToolchainStatus(detected);
|
|
92
|
+
if (detected.ready) {
|
|
93
|
+
console.log(' β detected a working Android toolchain β skipping install.');
|
|
94
|
+
if (!detected.avd) {
|
|
95
|
+
console.log(' (no AVD found β add one with `taqwright install --with-avd`, or use a device/cloud)');
|
|
96
|
+
}
|
|
97
|
+
installToolchain = false;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
installToolchain = await yesNo(rl, 'Auto-install the Android toolchain now? (~700 MB: JDK + Android SDK + Appium β ' +
|
|
101
|
+
"a complete self-contained set; won't touch your system tools)", false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (opts.withAvd !== undefined) {
|
|
105
|
+
withAvd = opts.withAvd;
|
|
106
|
+
}
|
|
107
|
+
else if (!installToolchain) {
|
|
108
|
+
withAvd = false;
|
|
109
|
+
}
|
|
110
|
+
else if (detected?.avd) {
|
|
111
|
+
withAvd = await yesNo(rl, `You already have an AVD (${detected.avdNames.join(', ')}) β ` +
|
|
112
|
+
'create the managed taqwright_api34 too? (~1 GB: system image + AVD)', false);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
withAvd = await yesNo(rl, 'Also create an Android emulator now? (~1 GB: system image + AVD). ' +
|
|
116
|
+
'Skip and no emulator is created β boot the example test on a physical ' +
|
|
117
|
+
'device, or add one later with `taqwright install --with-avd`', true);
|
|
118
|
+
}
|
|
119
|
+
if (platforms.includes('android') && !withAvd && detected?.avd) {
|
|
120
|
+
deviceAvdName =
|
|
121
|
+
detected.avdNames.length === 1
|
|
122
|
+
? detected.avdNames[0]
|
|
123
|
+
: await askAvdChoice(rl, detected.avdNames);
|
|
124
|
+
}
|
|
125
|
+
demoApp = (opts.demoApp ?? true) && platforms.includes('android');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
rl.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const platErr = platformSupportError(isMac, platforms);
|
|
132
|
+
if (platErr) {
|
|
133
|
+
console.error(`error: ${platErr}.`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const projectName = basename(targetDir);
|
|
137
|
+
const pkgName = toPackageName(projectName);
|
|
138
|
+
if (projectName !== pkgName) {
|
|
139
|
+
console.log(`note: folder "${projectName}" kept as-is, but the package name was normalized to "${pkgName}".`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(join(targetDir, testDir), { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error(`error: cannot create "${targetDir}": ${err.message}\n` +
|
|
146
|
+
' Check the path and your write permissions, then try again.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
let demoAppReady = false;
|
|
150
|
+
if (demoApp) {
|
|
151
|
+
const apkPath = join(targetDir, 'app', DEMO_APK_FILENAME);
|
|
152
|
+
process.stdout.write(`\nDownloading the demo app (${DEMO_APK_FILENAME})β¦ `);
|
|
153
|
+
try {
|
|
154
|
+
await download(DEMO_APK_URL, apkPath, AbortSignal.timeout(120_000));
|
|
155
|
+
demoAppReady = true;
|
|
156
|
+
console.log('done.');
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.log('failed.');
|
|
160
|
+
console.error(` Could not fetch the demo app (${err.message}).\n` +
|
|
161
|
+
' Scaffolding continues β drop an APK in app/ and set buildPath/appBundleId,\n' +
|
|
162
|
+
` or download it manually from ${DEMO_APK_URL}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const scopeForBoth = platforms.length === 2 && demoAppReady;
|
|
166
|
+
const demoAvdReady = demoAppReady && withAvd;
|
|
167
|
+
const files = [
|
|
168
|
+
['package.json', packageJsonTemplate(pkgName)],
|
|
169
|
+
['.npmrc', npmrcTemplate()],
|
|
170
|
+
['tsconfig.json', tsconfigTemplate(testDir)],
|
|
171
|
+
[
|
|
172
|
+
'taqwright.config.ts',
|
|
173
|
+
configTemplate(platforms, testDir, {
|
|
174
|
+
demoApp: demoAppReady,
|
|
175
|
+
demoAvd: demoAvdReady,
|
|
176
|
+
scoped: scopeForBoth,
|
|
177
|
+
deviceName: deviceAvdName,
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
];
|
|
181
|
+
if (scopeForBoth) {
|
|
182
|
+
files.push([join(testDir, 'android', 'example.spec.ts'), exampleTestTemplate(true)]);
|
|
183
|
+
files.push([join(testDir, 'ios', 'example.spec.ts'), exampleTestTemplate(false)]);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
files.push([join(testDir, 'example.spec.ts'), exampleTestTemplate(demoAppReady)]);
|
|
187
|
+
}
|
|
188
|
+
files.push(['.gitignore', gitignoreTemplate()]);
|
|
189
|
+
let overwrite = !!opts.yes;
|
|
190
|
+
if (!overwrite && interactive) {
|
|
191
|
+
const conflicts = files.filter(([rel]) => existsSync(join(targetDir, rel)));
|
|
192
|
+
if (conflicts.length > 0) {
|
|
193
|
+
const rl2 = createInterface({ input: stdin, output: stdout });
|
|
194
|
+
try {
|
|
195
|
+
overwrite = await yesNo(rl2, `${conflicts.length} file(s) already exist β overwrite them?`, false);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
rl2.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const written = [];
|
|
203
|
+
const skipped = [];
|
|
204
|
+
for (const [rel, content] of files) {
|
|
205
|
+
const dest = join(targetDir, rel);
|
|
206
|
+
if (existsSync(dest) && !overwrite) {
|
|
207
|
+
skipped.push(rel);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
212
|
+
await writeFile(dest, content);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error(`error: failed writing ${rel}: ${err.message}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
written.push(rel);
|
|
219
|
+
}
|
|
220
|
+
const showRoot = relative(process.cwd(), targetDir) || '.';
|
|
221
|
+
if (written.length) {
|
|
222
|
+
console.log('\nCreated:');
|
|
223
|
+
for (const rel of written)
|
|
224
|
+
console.log(' ' + join(showRoot, rel));
|
|
225
|
+
if (demoAppReady)
|
|
226
|
+
console.log(' ' + join(showRoot, 'app', DEMO_APK_FILENAME));
|
|
227
|
+
}
|
|
228
|
+
if (skipped.length) {
|
|
229
|
+
console.log('\nSkipped (already exist β re-run with --yes to overwrite):');
|
|
230
|
+
for (const rel of skipped)
|
|
231
|
+
console.log(' ' + join(showRoot, rel));
|
|
232
|
+
}
|
|
233
|
+
const cdHint = relative(process.cwd(), targetDir) || '.';
|
|
234
|
+
if (install) {
|
|
235
|
+
const linkedDev = await isTaqwrightGloballyLinked();
|
|
236
|
+
if (linkedDev) {
|
|
237
|
+
console.log('\nDetected globally-linked taqwright β will `npm link @taqwright/taqwright` after install (instead of fetching from the registry).');
|
|
238
|
+
}
|
|
239
|
+
console.log('\nRunning npm install β¦');
|
|
240
|
+
const code = await runNpm(['install'], targetDir);
|
|
241
|
+
if (code !== 0) {
|
|
242
|
+
console.error(`\nnpm install exited with code ${code}.`);
|
|
243
|
+
if (!linkedDev) {
|
|
244
|
+
console.error('\n@taqwright/taqwright installs from git+ssh://git@github.com/taqelah/taqwright.git,');
|
|
245
|
+
console.error('so a failure here usually means no SSH access to the private taqelah/taqwright repo.');
|
|
246
|
+
console.error('Verify your GitHub SSH key with: ssh -T git@github.com');
|
|
247
|
+
console.error('To use a local taqwright build instead:');
|
|
248
|
+
console.error(' cd /path/to/taqwright && npm link');
|
|
249
|
+
console.error(` cd ${cdHint} && npm install && npm link @taqwright/taqwright`);
|
|
250
|
+
}
|
|
251
|
+
process.exit(code);
|
|
252
|
+
}
|
|
253
|
+
if (linkedDev) {
|
|
254
|
+
const linkCode = await runNpm(['link', '@taqwright/taqwright'], targetDir);
|
|
255
|
+
if (linkCode !== 0) {
|
|
256
|
+
console.error('npm link @taqwright/taqwright failed.');
|
|
257
|
+
process.exit(linkCode);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let toolchainInstalled = false;
|
|
262
|
+
if (installToolchain && platforms.includes('android')) {
|
|
263
|
+
console.log((withAvd
|
|
264
|
+
? '\nInstalling the Android toolchain + emulator β this can take several minutesβ¦'
|
|
265
|
+
: '\nInstalling the Android toolchain β this can take a few minutesβ¦') +
|
|
266
|
+
"\n(installs under taqwright's own dir β your shell's JAVA_HOME/PATH stays as-is)\n");
|
|
267
|
+
try {
|
|
268
|
+
await runSetup({ withAvd });
|
|
269
|
+
toolchainInstalled = true;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error(`\ntaqwright install failed: ${err.message}`);
|
|
273
|
+
console.error('Scaffolding succeeded; retry the toolchain later with: npx taqwright install');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (installToolchain && !platforms.includes('android')) {
|
|
277
|
+
console.log('\n(Skipping --install-toolchain: `taqwright install` provisions the Android stack, ' +
|
|
278
|
+
'but this project is iOS-only.)');
|
|
279
|
+
}
|
|
280
|
+
console.log('\nNext steps:');
|
|
281
|
+
console.log(` cd ${cdHint}`);
|
|
282
|
+
if (!install)
|
|
283
|
+
console.log(' npm install');
|
|
284
|
+
if (platforms.includes('android')) {
|
|
285
|
+
const hasAvd = detected?.avd ?? (await listAvds()).length > 0;
|
|
286
|
+
if (toolchainInstalled && !withAvd && !hasAvd) {
|
|
287
|
+
console.log(' npx taqwright install --with-avd # add an Android emulator (~1 GB), or use a physical device');
|
|
288
|
+
}
|
|
289
|
+
else if (!toolchainInstalled && !detected?.ready) {
|
|
290
|
+
console.log(hasAvd
|
|
291
|
+
? ' npx taqwright install # Android toolchain (JDK + SDK + Appium) β you already have an emulator'
|
|
292
|
+
: ' npx taqwright install --with-avd # Android toolchain + emulator (JDK + SDK + Appium + AVD); drop --with-avd to skip the ~1 GB emulator');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log(' npx taqwright test');
|
|
296
|
+
console.log('\nCommands:');
|
|
297
|
+
console.log(' npx taqwright doctor');
|
|
298
|
+
console.log(' npx taqwright codegen');
|
|
299
|
+
console.log(' npx taqwright test');
|
|
300
|
+
console.log(' npx taqwright show-report');
|
|
301
|
+
if (!demoAppReady && platforms.includes('android')) {
|
|
302
|
+
console.log('\nNo demo app was added β the example test is a no-op stub. Drop an APK in\n' +
|
|
303
|
+
'app/ and set buildPath/appBundleId in taqwright.config.ts, or re-run\n' +
|
|
304
|
+
'`npx taqwright init --demo-app` to fetch the demo app.');
|
|
305
|
+
}
|
|
306
|
+
if (demoAppReady && !demoAvdReady) {
|
|
307
|
+
if (deviceAvdName) {
|
|
308
|
+
console.log(`\nThe config targets the "${deviceAvdName}" AVD β taqwright boots it automatically\n` +
|
|
309
|
+
'when you run the test above (or uses it if already running).');
|
|
310
|
+
}
|
|
311
|
+
else if (detected?.avd) {
|
|
312
|
+
console.log('\nBoot one of your emulators (or connect a device) and set device.name in\n' +
|
|
313
|
+
'taqwright.config.ts before running the test above.');
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.log('\nNo emulator was found β run `npx taqwright install --with-avd` to create one (or\n' +
|
|
317
|
+
'connect a device), then set device.name + autoStartDevice in taqwright.config.ts.');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
console.log('');
|
|
321
|
+
}
|
|
322
|
+
async function ask(rl, label, def) {
|
|
323
|
+
const answer = (await rl.question(`? ${label} (${def}): `)).trim();
|
|
324
|
+
return answer || def;
|
|
325
|
+
}
|
|
326
|
+
async function askChoice(rl, label, choices, def) {
|
|
327
|
+
const list = choices.join('/');
|
|
328
|
+
while (true) {
|
|
329
|
+
const raw = (await rl.question(`? ${label} [${list}]: `)).trim().toLowerCase();
|
|
330
|
+
if (!raw)
|
|
331
|
+
return def;
|
|
332
|
+
if (choices.includes(raw))
|
|
333
|
+
return raw;
|
|
334
|
+
console.log(` please answer with one of: ${choices.join(', ')}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function yesNo(rl, label, def) {
|
|
338
|
+
const hint = def ? 'Y/n' : 'y/N';
|
|
339
|
+
while (true) {
|
|
340
|
+
const raw = (await rl.question(`? ${label} (${hint}): `)).trim().toLowerCase();
|
|
341
|
+
if (!raw)
|
|
342
|
+
return def;
|
|
343
|
+
if (['y', 'yes'].includes(raw))
|
|
344
|
+
return true;
|
|
345
|
+
if (['n', 'no'].includes(raw))
|
|
346
|
+
return false;
|
|
347
|
+
console.log(' please answer y or n');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function askAvdChoice(rl, avds) {
|
|
351
|
+
const placeholder = avds.length + 1;
|
|
352
|
+
console.log(' Detected AVDs:');
|
|
353
|
+
avds.forEach((a, i) => console.log(` ${i + 1}) ${a}`));
|
|
354
|
+
console.log(` ${placeholder}) leave placeholder (edit the config later)`);
|
|
355
|
+
while (true) {
|
|
356
|
+
const raw = (await ask(rl, `Which AVD should the config target? (1-${placeholder})`, '1')).trim();
|
|
357
|
+
const n = Number(raw);
|
|
358
|
+
if (Number.isInteger(n) && n >= 1 && n <= avds.length)
|
|
359
|
+
return avds[n - 1];
|
|
360
|
+
if (n === placeholder)
|
|
361
|
+
return undefined;
|
|
362
|
+
console.log(` please enter a number 1-${placeholder}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function isNonEmpty(dir) {
|
|
366
|
+
try {
|
|
367
|
+
const entries = await readdir(dir);
|
|
368
|
+
return entries.length > 0;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const RESERVED_DIR_NAMES = new Set([
|
|
375
|
+
'con',
|
|
376
|
+
'prn',
|
|
377
|
+
'aux',
|
|
378
|
+
'nul',
|
|
379
|
+
'com1',
|
|
380
|
+
'com2',
|
|
381
|
+
'com3',
|
|
382
|
+
'com4',
|
|
383
|
+
'com5',
|
|
384
|
+
'com6',
|
|
385
|
+
'com7',
|
|
386
|
+
'com8',
|
|
387
|
+
'com9',
|
|
388
|
+
'lpt1',
|
|
389
|
+
'lpt2',
|
|
390
|
+
'lpt3',
|
|
391
|
+
'lpt4',
|
|
392
|
+
'lpt5',
|
|
393
|
+
'lpt6',
|
|
394
|
+
'lpt7',
|
|
395
|
+
'lpt8',
|
|
396
|
+
'lpt9',
|
|
397
|
+
'app',
|
|
398
|
+
'node_modules',
|
|
399
|
+
'playwright-report',
|
|
400
|
+
'dist',
|
|
401
|
+
]);
|
|
402
|
+
const TEST_DIR_HINT = 'use letters, digits, dot, dash or underscore (no spaces/special chars), ' +
|
|
403
|
+
'and avoid reserved names like "app" or "node_modules"';
|
|
404
|
+
export function isReservedDirName(name) {
|
|
405
|
+
return RESERVED_DIR_NAMES.has(name.toLowerCase());
|
|
406
|
+
}
|
|
407
|
+
export function platformChoices(isMac) {
|
|
408
|
+
return isMac ? ['android', 'ios', 'both'] : ['android'];
|
|
409
|
+
}
|
|
410
|
+
export function platformSupportError(isMac, platforms) {
|
|
411
|
+
if (!isMac && platforms.includes('ios')) {
|
|
412
|
+
return 'iOS testing requires macOS (Xcode + simulators). On Windows/Linux, use --platform android';
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function printAndroidToolchainStatus(tc) {
|
|
417
|
+
const mark = (ok) => (ok ? 'β' : 'β');
|
|
418
|
+
const jv = tc.jdkVersion ? ` v${tc.jdkVersion}` : '';
|
|
419
|
+
const jdkLine = tc.jdk === 'ok'
|
|
420
|
+
? `β JDK (java${jv})`
|
|
421
|
+
: tc.jdk === 'too-old'
|
|
422
|
+
? `β JDK${jv} β too old, need 17+`
|
|
423
|
+
: tc.jdk === 'unknown'
|
|
424
|
+
? 'β JDK (java) β version unreadable'
|
|
425
|
+
: 'β JDK (java) β not found';
|
|
426
|
+
const av = tc.appiumVersion ? ` (v${tc.appiumVersion})` : '';
|
|
427
|
+
const appiumLine = tc.appium === 'recommended'
|
|
428
|
+
? `β Appium 3.x${av}`
|
|
429
|
+
: tc.appium === 'best-effort'
|
|
430
|
+
? `β Appium 2.x${av} β best-effort, not the supported version`
|
|
431
|
+
: tc.appium === 'unsupported'
|
|
432
|
+
? `β Appium${av} β unsupported version`
|
|
433
|
+
: 'β Appium β not found';
|
|
434
|
+
console.log('\nAndroid toolchain:');
|
|
435
|
+
console.log(` ${jdkLine}`);
|
|
436
|
+
console.log(` ${mark(tc.sdk)} Android SDK (adb)`);
|
|
437
|
+
console.log(` ${appiumLine}`);
|
|
438
|
+
console.log(` ${mark(tc.uiautomator2)} uiautomator2 driver`);
|
|
439
|
+
console.log(tc.avd
|
|
440
|
+
? ` β Android emulator (AVD): ${tc.avdNames.join(', ')}`
|
|
441
|
+
: ' β Android emulator (AVD) β none');
|
|
442
|
+
}
|
|
443
|
+
function projectLocationError(targetDir) {
|
|
444
|
+
const name = basename(targetDir);
|
|
445
|
+
if (isReservedDirName(name)) {
|
|
446
|
+
return `"${name}" is a reserved name β choose a different project folder`;
|
|
447
|
+
}
|
|
448
|
+
const exists = existsSync(targetDir);
|
|
449
|
+
const t = projectTargetError(exists, exists && statSync(targetDir).isDirectory());
|
|
450
|
+
if (t)
|
|
451
|
+
return `"${targetDir}" ${t}`;
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
async function askProjectDir(rl, argDir) {
|
|
455
|
+
while (true) {
|
|
456
|
+
const dirInput = argDir ?? (await ask(rl, 'Project location', './taqwright-tests'));
|
|
457
|
+
const targetDir = resolve(process.cwd(), dirInput);
|
|
458
|
+
const err = projectLocationError(targetDir);
|
|
459
|
+
if (!err)
|
|
460
|
+
return targetDir;
|
|
461
|
+
if (argDir !== undefined) {
|
|
462
|
+
console.error(`error: ${err}.`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
console.log(` ${err} β try another.`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
export function isValidTestDir(name) {
|
|
469
|
+
if (!name)
|
|
470
|
+
return false;
|
|
471
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(name))
|
|
472
|
+
return false;
|
|
473
|
+
if (isReservedDirName(name))
|
|
474
|
+
return false;
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
async function askTestDir(rl) {
|
|
478
|
+
while (true) {
|
|
479
|
+
const name = await ask(rl, 'Test folder name', 'tests');
|
|
480
|
+
if (isValidTestDir(name))
|
|
481
|
+
return name;
|
|
482
|
+
console.log(` please ${TEST_DIR_HINT}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
export function projectTargetError(exists, isDirectory) {
|
|
486
|
+
if (exists && !isDirectory)
|
|
487
|
+
return 'exists and is not a directory';
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
export function toPackageName(raw) {
|
|
491
|
+
const cleaned = raw
|
|
492
|
+
.toLowerCase()
|
|
493
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
494
|
+
.replace(/^[._-]+/, '')
|
|
495
|
+
.replace(/-+$/, '');
|
|
496
|
+
return cleaned || 'taqwright-tests';
|
|
497
|
+
}
|
|
498
|
+
function runNpm(args, cwd) {
|
|
499
|
+
return new Promise((resolve_) => {
|
|
500
|
+
const child = spawnTool('npm', args, { cwd, stdio: 'inherit' });
|
|
501
|
+
child.on('exit', (code, signal) => {
|
|
502
|
+
if (signal)
|
|
503
|
+
resolve_(128);
|
|
504
|
+
else
|
|
505
|
+
resolve_(code ?? 0);
|
|
506
|
+
});
|
|
507
|
+
child.on('error', (err) => {
|
|
508
|
+
console.error(`failed to spawn npm ${args.join(' ')}:`, err.message);
|
|
509
|
+
resolve_(1);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function isTaqwrightGloballyLinked() {
|
|
514
|
+
try {
|
|
515
|
+
const { stdout } = await execP('npm root -g');
|
|
516
|
+
return existsSync(join(stdout.trim(), '@taqwright', 'taqwright'));
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function packageJsonTemplate(name) {
|
|
523
|
+
const obj = {
|
|
524
|
+
name,
|
|
525
|
+
private: true,
|
|
526
|
+
version: '0.0.1',
|
|
527
|
+
type: 'module',
|
|
528
|
+
scripts: {
|
|
529
|
+
test: 'taqwright test',
|
|
530
|
+
codegen: 'taqwright codegen',
|
|
531
|
+
doctor: 'taqwright doctor',
|
|
532
|
+
devices: 'taqwright devices',
|
|
533
|
+
report: 'taqwright show-report',
|
|
534
|
+
},
|
|
535
|
+
devDependencies: {
|
|
536
|
+
'@taqwright/taqwright': 'git+ssh://git@github.com/taqelah/taqwright.git',
|
|
537
|
+
'@types/node': '^24.0.0',
|
|
538
|
+
typescript: '^5.4.0',
|
|
539
|
+
},
|
|
540
|
+
engines: {
|
|
541
|
+
node: '>=24.0.0 <26.0.0',
|
|
542
|
+
},
|
|
543
|
+
overrides: {
|
|
544
|
+
'@wdio/config': {
|
|
545
|
+
glob: '^13',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
return JSON.stringify(obj, null, 2) + '\n';
|
|
550
|
+
}
|
|
551
|
+
function tsconfigTemplate(testDir) {
|
|
552
|
+
const obj = {
|
|
553
|
+
compilerOptions: {
|
|
554
|
+
target: 'ES2022',
|
|
555
|
+
module: 'NodeNext',
|
|
556
|
+
moduleResolution: 'NodeNext',
|
|
557
|
+
esModuleInterop: true,
|
|
558
|
+
strict: true,
|
|
559
|
+
skipLibCheck: true,
|
|
560
|
+
resolveJsonModule: true,
|
|
561
|
+
types: ['node'],
|
|
562
|
+
},
|
|
563
|
+
include: [`${testDir}/**/*`, 'taqwright.config.ts'],
|
|
564
|
+
};
|
|
565
|
+
return JSON.stringify(obj, null, 2) + '\n';
|
|
566
|
+
}
|
|
567
|
+
export function configTemplate(platforms, testDir, opts) {
|
|
568
|
+
const projects = platforms
|
|
569
|
+
.map((p) => projectBlock(p, {
|
|
570
|
+
demoApp: opts.demoApp,
|
|
571
|
+
demoAvd: opts.demoAvd,
|
|
572
|
+
deviceName: opts.deviceName,
|
|
573
|
+
scopedTestMatch: opts.scoped ? `'**/${p}/**'` : undefined,
|
|
574
|
+
}))
|
|
575
|
+
.join(',\n');
|
|
576
|
+
return `import { defineConfig, Platform } from '@taqwright/taqwright';
|
|
577
|
+
|
|
578
|
+
// Every config knob is listed here. Essentials are uncommented; everything
|
|
579
|
+
// else is a commented placeholder you can enable by removing the leading
|
|
580
|
+
// "// ". Hover any field in your editor for the full type docs.
|
|
581
|
+
export default defineConfig({
|
|
582
|
+
testDir: './${testDir}',
|
|
583
|
+
timeout: 60_000,
|
|
584
|
+
expectTimeout: 30_000,
|
|
585
|
+
// 'html' writes playwright-report/ β view it with: npx taqwright show-report
|
|
586
|
+
reporter: [['list'], ['html', { open: 'never', title: 'Taqwright Test Report' }]],
|
|
587
|
+
|
|
588
|
+
// βββ Optional top-level overrides βββββββββββββββββββββββββββββββββ
|
|
589
|
+
// retries: 1,
|
|
590
|
+
// outputDir: './test-results',
|
|
591
|
+
// fullyParallel: false,
|
|
592
|
+
// forbidOnly: !!process.env.CI,
|
|
593
|
+
// testMatch: ['**/*.spec.ts'],
|
|
594
|
+
// testIgnore: ['**/wip/**'],
|
|
595
|
+
// globalSetup: './setup.ts',
|
|
596
|
+
// globalTeardown: './teardown.ts',
|
|
597
|
+
|
|
598
|
+
projects: [
|
|
599
|
+
${projects},
|
|
600
|
+
|
|
601
|
+
// βββ Cloud examples (BrowserStack / LambdaTest) βββββββββββββββββ
|
|
602
|
+
// Uncomment a block below to add a cloud project. Set the matching
|
|
603
|
+
// env vars before launching:
|
|
604
|
+
// BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY
|
|
605
|
+
// LAMBDATEST_USERNAME / LAMBDATEST_ACCESS_KEY
|
|
606
|
+
// For now, cloud devices are wired through the inspector
|
|
607
|
+
// ('taqwright inspect'); cloud test-runner support lands separately.
|
|
608
|
+
//
|
|
609
|
+
// {
|
|
610
|
+
// name: 'browserstack',
|
|
611
|
+
// use: {
|
|
612
|
+
// platform: Platform.ANDROID,
|
|
613
|
+
// device: {
|
|
614
|
+
// provider: 'browserstack',
|
|
615
|
+
// name: 'Google Pixel 8',
|
|
616
|
+
// osVersion: '14.0',
|
|
617
|
+
// orientation: 'portrait',
|
|
618
|
+
// },
|
|
619
|
+
// resetBetweenTests: true,
|
|
620
|
+
// buildPath: 'bs://<app-id-from-app-upload>',
|
|
621
|
+
// appBundleId: 'com.example.app',
|
|
622
|
+
// },
|
|
623
|
+
// },
|
|
624
|
+
// {
|
|
625
|
+
// name: 'lambdatest',
|
|
626
|
+
// use: {
|
|
627
|
+
// platform: Platform.IOS,
|
|
628
|
+
// device: {
|
|
629
|
+
// provider: 'lambdatest',
|
|
630
|
+
// name: 'iPhone 15',
|
|
631
|
+
// osVersion: '17',
|
|
632
|
+
// },
|
|
633
|
+
// resetBetweenTests: true,
|
|
634
|
+
// buildPath: 'lt://<app-id-from-app-upload>',
|
|
635
|
+
// appBundleId: 'com.example.MyApp',
|
|
636
|
+
// },
|
|
637
|
+
// },
|
|
638
|
+
],
|
|
639
|
+
});
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
642
|
+
function projectBlock(p, opts) {
|
|
643
|
+
const { demoApp, demoAvd, deviceName, scopedTestMatch } = opts;
|
|
644
|
+
const isAndroid = p === 'android';
|
|
645
|
+
const demoWired = isAndroid && demoApp;
|
|
646
|
+
const demoAvdWired = isAndroid && demoAvd;
|
|
647
|
+
const platformConst = isAndroid ? 'Platform.ANDROID' : 'Platform.IOS';
|
|
648
|
+
const projectName = isAndroid ? 'android' : 'ios';
|
|
649
|
+
const deviceNameLine = demoAvdWired
|
|
650
|
+
? ` name: '${DEMO_AVD_NAME}', // AVD from \`taqwright install --with-avd\``
|
|
651
|
+
: isAndroid && deviceName
|
|
652
|
+
? ` name: '${deviceName}', // your detected AVD`
|
|
653
|
+
: demoWired
|
|
654
|
+
? ' // name: /Pixel/, // no managed AVD β bring a running device, or `taqwright install --with-avd`'
|
|
655
|
+
: isAndroid
|
|
656
|
+
? ' // name: /Pixel/,'
|
|
657
|
+
: ' name: /iPhone/,';
|
|
658
|
+
const autoStartDeviceLine = demoAvdWired
|
|
659
|
+
? ` autoStartDevice: true, // cold-boots the ${DEMO_AVD_NAME} AVD`
|
|
660
|
+
: isAndroid && deviceName
|
|
661
|
+
? ` autoStartDevice: true, // cold-boots the ${deviceName} AVD`
|
|
662
|
+
: ' // autoStartDevice: true,';
|
|
663
|
+
const exampleUdid = isAndroid ? "'emulator-5554'" : "'00000000-0000-0000-0000-000000000000'";
|
|
664
|
+
const exampleOsVersion = isAndroid ? "'14'" : "'17'";
|
|
665
|
+
const examplePath = isAndroid ? "'/absolute/path/to/app.apk'" : "'/absolute/path/to/MyApp.app'";
|
|
666
|
+
const exampleBundleId = isAndroid ? "'com.example.app'" : "'com.example.MyApp'";
|
|
667
|
+
const testMatchLine = scopedTestMatch
|
|
668
|
+
? ` testMatch: [${scopedTestMatch}],`
|
|
669
|
+
: ` // testMatch: ['**/${projectName}/*.spec.ts'],`;
|
|
670
|
+
const resetBlock = isAndroid && demoApp
|
|
671
|
+
? ` // βββ Reset between tests ββββββββββββββββββββββββββββββββββββ
|
|
672
|
+
// Bound to the bundled demo app (app/${DEMO_APK_FILENAME}).
|
|
673
|
+
// resetBetweenTests reinstalls + relaunches it fresh before every
|
|
674
|
+
// test, so each starts from a known state. All three are
|
|
675
|
+
// type-required together.
|
|
676
|
+
resetBetweenTests: true,
|
|
677
|
+
buildPath: './app/${DEMO_APK_FILENAME}',
|
|
678
|
+
appBundleId: '${DEMO_APP_BUNDLE_ID}',`
|
|
679
|
+
: ` // βββ Reset between tests ββββββββββββββββββββββββββββββββββββ
|
|
680
|
+
// Uncomment all three lines below to terminate β uninstall β
|
|
681
|
+
// reinstall β relaunch the app before every test. Required if
|
|
682
|
+
// you want each test to start from a known state. The TS type
|
|
683
|
+
// for use enforces all three together.
|
|
684
|
+
//
|
|
685
|
+
// resetBetweenTests: true,
|
|
686
|
+
// buildPath: ${examplePath},
|
|
687
|
+
// appBundleId: ${exampleBundleId},`;
|
|
688
|
+
return ` {
|
|
689
|
+
name: '${projectName}',
|
|
690
|
+
use: {
|
|
691
|
+
platform: ${platformConst},
|
|
692
|
+
device: {
|
|
693
|
+
provider: 'emulator',
|
|
694
|
+
${deviceNameLine}
|
|
695
|
+
// osVersion: ${exampleOsVersion},
|
|
696
|
+
// udid: ${exampleUdid},
|
|
697
|
+
// orientation: 'portrait',
|
|
698
|
+
//
|
|
699
|
+
// βββ Parallel runs (optional) ββββββββββββββββββββββββββββ
|
|
700
|
+
// Declare a pool of devices to fan tests out across, then
|
|
701
|
+
// bump \`workers\` at the top of this config to match. Worker
|
|
702
|
+
// N picks pool[N]; \`workers > pool.length\` fails fast. Each
|
|
703
|
+
// worker gets its own Appium + driver ports auto-staggered.
|
|
704
|
+
// pool: [
|
|
705
|
+
// { udid: 'emulator-5554', name: 'Pixel_7_API_34' },
|
|
706
|
+
// { udid: 'emulator-5556', name: 'Pixel_7_API_34_2' },
|
|
707
|
+
// { udid: 'emulator-5558', name: 'Pixel_7_API_34_3' },
|
|
708
|
+
// ],
|
|
709
|
+
//
|
|
710
|
+
// Or skip the pool entirely and let taqwright discover local
|
|
711
|
+
// devices and partition them across \`workers\` for you β it
|
|
712
|
+
// cold-boots shutdown AVDs/simulators to reach the count and
|
|
713
|
+
// fails fast if too few are available. Mutually exclusive with
|
|
714
|
+
// \`pool\` / \`udid\`.
|
|
715
|
+
// autoDiscover: true,
|
|
716
|
+
},
|
|
717
|
+
// Spawn \`npx appium\` automatically when nothing is listening on
|
|
718
|
+
// the configured host:port. Set \`autoStart: false\` to manage
|
|
719
|
+
// Appium yourself (e.g. an Appium server you start by hand).
|
|
720
|
+
appium: {
|
|
721
|
+
autoStart: true,
|
|
722
|
+
// Boot an offline Android emulator automatically. Needs a
|
|
723
|
+
// string device.name equal to the AVD id (e.g. 'Pixel_7_API_34',
|
|
724
|
+
// see 'emulator -list-avds'); a RegExp name is rejected at
|
|
725
|
+
// config load. iOS simulators boot via XCUITest regardless.
|
|
726
|
+
${autoStartDeviceLine}
|
|
727
|
+
host: 'localhost',
|
|
728
|
+
port: 4723, // Appium 3 default
|
|
729
|
+
path: '/', // Appium 3 default (Appium 1.x used '/wd/hub')
|
|
730
|
+
// newCommandTimeout: 240,
|
|
731
|
+
// logLevel: 'warn',
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
${resetBlock}
|
|
735
|
+
|
|
736
|
+
// βββ Extra capabilities (escape hatch) ββββββββββββββββββββββ
|
|
737
|
+
// Anything Appium accepts; merged on top of the auto-built caps.
|
|
738
|
+
// capabilities: {
|
|
739
|
+
// 'appium:autoGrantPermissions': true,
|
|
740
|
+
// 'appium:autoAcceptAlerts': true,
|
|
741
|
+
// },
|
|
742
|
+
|
|
743
|
+
// βββ Per-project locator-action timeout (ms) ββββββββββββββββ
|
|
744
|
+
// Overrides the top-level \`expectTimeout\` for this project only.
|
|
745
|
+
// expectTimeout: 30_000,
|
|
746
|
+
|
|
747
|
+
// βββ Trace artifact βββββββββββββββββββββββββββββββββββββββββ
|
|
748
|
+
// Captures a per-action screenshot + page-source timeline as a
|
|
749
|
+
// self-contained \`trace.html\` under the test's output dir, also
|
|
750
|
+
// attached to the Playwright HTML report. Adds one screenshot +
|
|
751
|
+
// page-source round-trip per action (~100β300ms local, more
|
|
752
|
+
// over USB) β recommended for CI: 'on-failure'.
|
|
753
|
+
// 'off' β no overhead (default)
|
|
754
|
+
// 'on' β every test
|
|
755
|
+
// 'on-failure' β only failed tests
|
|
756
|
+
// 'retain-on-failure' β alias of 'on-failure' on mobile
|
|
757
|
+
// trace: 'on-failure',
|
|
758
|
+
|
|
759
|
+
// βββ Screen recording (video) βββββββββββββββββββββββββββββββ
|
|
760
|
+
// Records the device screen via Appium for the whole run and
|
|
761
|
+
// attaches a screen.mp4 to the Playwright HTML report (as
|
|
762
|
+
// 'taqwright-video'). No per-action cost like trace, but every
|
|
763
|
+
// run pays the device recorder + an mp4 transfer at teardown β
|
|
764
|
+
// recommended for CI: 'on-failure'. iOS-simulator support varies.
|
|
765
|
+
// 'off' β no recording (default)
|
|
766
|
+
// 'on' β every test
|
|
767
|
+
// 'on-failure' β only failed tests
|
|
768
|
+
// 'retain-on-failure' β alias of 'on-failure' on mobile
|
|
769
|
+
// video: 'on-failure',
|
|
770
|
+
|
|
771
|
+
// βββ Network capture (HAR) ββββββββββββββββββββββββββββββββββ
|
|
772
|
+
// Routes app traffic through a local MITM proxy and attaches a
|
|
773
|
+
// HAR 1.2 file to the Playwright HTML report (as 'taqwright-har').
|
|
774
|
+
// Zero-touch on userdebug Android emulators and iOS Simulators β
|
|
775
|
+
// taqwright generates its own CA, installs it on the device, sets
|
|
776
|
+
// the device/host proxy, and tears everything down on teardown
|
|
777
|
+
// (including crash paths). Cloud projects skip this (the hub
|
|
778
|
+
// captures HAR server-side); real devices and Google Play AVDs
|
|
779
|
+
// are skipped with a note in the artifact.
|
|
780
|
+
// 'off' β no capture (default)
|
|
781
|
+
// 'on' β every test
|
|
782
|
+
// 'on-failure' β only failed tests
|
|
783
|
+
// 'retain-on-failure' β alias of 'on-failure' on mobile
|
|
784
|
+
// network: 'on-failure',
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
// βββ Per-project test-runner overrides ββββββββββββββββββββββββ
|
|
788
|
+
// timeout: 90_000,
|
|
789
|
+
// retries: 2,
|
|
790
|
+
// grep: /smoke/,
|
|
791
|
+
// grepInvert: /flaky/,
|
|
792
|
+
// dependencies: ['setup'],
|
|
793
|
+
${testMatchLine}
|
|
794
|
+
}`;
|
|
795
|
+
}
|
|
796
|
+
export function exampleTestTemplate(demoApp) {
|
|
797
|
+
if (demoApp) {
|
|
798
|
+
return `import { test, expect } from '@taqwright/taqwright';
|
|
799
|
+
|
|
800
|
+
// βββ Example tests (demo app) ββββββββββββββββββββββββββββββββββββ
|
|
801
|
+
// Run against the bundled demo app (app/${DEMO_APK_FILENAME}). The
|
|
802
|
+
// config sets resetBetweenTests:true, so taqwright reinstalls +
|
|
803
|
+
// relaunches it fresh before each test β every test starts at the
|
|
804
|
+
// login screen. \`npx taqwright test\` should pass once a device /
|
|
805
|
+
// emulator is up. (Android selectors β the demo app is an APK.)
|
|
806
|
+
test('user can log in to the demo app', async ({ mobile }) => {
|
|
807
|
+
await mobile.getByXpath("//*[@hint='Username']").fill('emma@demoapp.com');
|
|
808
|
+
await mobile.getByXpath("//*[@hint='Password']").fill('10203040');
|
|
809
|
+
await mobile.getByUiSelector('new UiSelector().description("Login")').click();
|
|
810
|
+
await expect(mobile.getByUiSelector('new UiSelector().description("View All")')).toBeVisible();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('login fails with invalid username & password', async ({ mobile }) => {
|
|
814
|
+
await mobile.getByXpath("//*[@hint='Username']").fill('invalidusername');
|
|
815
|
+
await mobile.getByXpath("//*[@hint='Password']").fill('invalidpassword');
|
|
816
|
+
await mobile.getByUiSelector('new UiSelector().description("Login")').click();
|
|
817
|
+
await expect(mobile.getByXpath("//*[contains(@content-desc, 'Invalid username or password.')]")).toBeVisible();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('login is blocked without username & password', async ({ mobile }) => {
|
|
821
|
+
await mobile.getByUiSelector('new UiSelector().description("Login")').click();
|
|
822
|
+
await expect(mobile.getByUiSelector('new UiSelector().description("Please enter your username")')).toBeVisible();
|
|
823
|
+
await expect(mobile.getByUiSelector('new UiSelector().description("Please enter your password")')).toBeVisible();
|
|
824
|
+
});
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
return `import { test, expect } from '@taqwright/taqwright';
|
|
828
|
+
|
|
829
|
+
// βββ First test ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
830
|
+
// Runs without an app installed β just confirms the device + Appium
|
|
831
|
+
// stack is wired up. Fill in your own \`buildPath\` + \`appBundleId\`
|
|
832
|
+
// in taqwright.config.ts then write a real test below.
|
|
833
|
+
test('screen has positive dimensions', async ({ mobile }) => {
|
|
834
|
+
const size = await mobile.getScreenSize();
|
|
835
|
+
expect(size.width).toBeGreaterThan(0);
|
|
836
|
+
expect(size.height).toBeGreaterThan(0);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// βββ Realistic-shape example (commented) βββββββββββββββββββββββββ
|
|
840
|
+
// Uncomment after pointing the config at your app. Showcases the
|
|
841
|
+
// idiomatic taqwright surface:
|
|
842
|
+
//
|
|
843
|
+
// * Locator entry points: getById / getByText / getByLabel / getByRole / ...
|
|
844
|
+
// * Chain methods: .first() / .nth(i) / .filter({ hasText }) / .locator(child) / .all()
|
|
845
|
+
// * Auto-retrying matchers (Playwright-style) on a Locator:
|
|
846
|
+
// await expect(loc).toBeVisible() / .toHaveText() / .toBeChecked() / .toHaveCount(n) / ...
|
|
847
|
+
// * Plain \`expect(value)\` (no await) for numbers, strings, arrays.
|
|
848
|
+
//
|
|
849
|
+
// test('login flow', async ({ mobile }) => {
|
|
850
|
+
// await mobile.getById('Username').fill('demo@example.com');
|
|
851
|
+
// await mobile.getById('Password').fill('hunter2');
|
|
852
|
+
// await mobile.getByRole('button', { name: 'Sign in' }).click();
|
|
853
|
+
//
|
|
854
|
+
// // Auto-waits up to expectTimeout for the heading to appear.
|
|
855
|
+
// await expect(mobile.getByText('Welcome')).toBeVisible();
|
|
856
|
+
//
|
|
857
|
+
// // Chain β disambiguate the 3rd row in a repeating list.
|
|
858
|
+
// await mobile.getByType('XCUIElementTypeCell').nth(2).click();
|
|
859
|
+
//
|
|
860
|
+
// // Filter β pick the Wi-Fi row by its label, then tap its switch.
|
|
861
|
+
// await mobile.getByType('android.widget.LinearLayout')
|
|
862
|
+
// .filter({ hasText: 'Wi-Fi' })
|
|
863
|
+
// .locator(mobile.getByType('android.widget.Switch'))
|
|
864
|
+
// .check();
|
|
865
|
+
//
|
|
866
|
+
// // Plain-value expect β for non-Locator data.
|
|
867
|
+
// const items = await mobile.getByType('CartItem').all();
|
|
868
|
+
// expect(items).toHaveLength(3);
|
|
869
|
+
// });
|
|
870
|
+
|
|
871
|
+
// βββ Pause for interactive debugging (commented) βββββββββββββββββ
|
|
872
|
+
// Drop this anywhere in a test to hand off to the inspector. The
|
|
873
|
+
// in-flight WebDriver session is attached (no new Appium boot), the
|
|
874
|
+
// inspector opens in your browser, and the test resumes when you
|
|
875
|
+
// click "Resume" in the UI. Set \`PWDEBUG=0\` in CI to make it a
|
|
876
|
+
// no-op without removing the call.
|
|
877
|
+
//
|
|
878
|
+
// test('paused for inspection', async ({ mobile }) => {
|
|
879
|
+
// await mobile.getById('Login').click();
|
|
880
|
+
// await mobile.pause(); // β browser opens; click around; click Resume
|
|
881
|
+
// await expect(mobile.getByText('Dashboard')).toBeVisible();
|
|
882
|
+
// });
|
|
883
|
+
`;
|
|
884
|
+
}
|
|
885
|
+
function gitignoreTemplate() {
|
|
886
|
+
return `node_modules
|
|
887
|
+
dist
|
|
888
|
+
test-results
|
|
889
|
+
playwright-report
|
|
890
|
+
.DS_Store
|
|
891
|
+
*.log
|
|
892
|
+
`;
|
|
893
|
+
}
|
|
894
|
+
function npmrcTemplate() {
|
|
895
|
+
return `# Active path today: @taqwright/taqwright installs over git+ssh β no token needed.
|
|
896
|
+
# Future (once published to GitHub Packages), uncomment and supply a token:
|
|
897
|
+
# @taqwright:registry=https://npm.pkg.github.com
|
|
898
|
+
# Auth needs a Personal Access Token with read:packages. Don't commit it β
|
|
899
|
+
# put it in your user ~/.npmrc or a CI env var, e.g.:
|
|
900
|
+
# //npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}
|
|
901
|
+
`;
|
|
902
|
+
}
|