@irsprs/mobwright 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/doctor.ts
4
+ import { exec as execCb } from "child_process";
5
+ import { promisify } from "util";
6
+ import { existsSync } from "fs";
7
+
8
+ // src/cli/colors.ts
9
+ var enabled = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
10
+ function paint(code, text) {
11
+ return enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
12
+ }
13
+ var green = (s) => paint(32, s);
14
+ var yellow = (s) => paint(33, s);
15
+ var red = (s) => paint(31, s);
16
+ var dim = (s) => paint(2, s);
17
+ var bold = (s) => paint(1, s);
18
+
19
+ // src/cli/doctor.ts
20
+ var exec = promisify(execCb);
21
+ async function tryExec(cmd, args = []) {
22
+ try {
23
+ const { stdout } = await exec([cmd, ...args].join(" "), { timeout: 5e3 });
24
+ return stdout.split("\n")[0]?.trim() ?? "";
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ async function httpProbe(url, timeoutMs = 1500) {
30
+ try {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
33
+ const res = await fetch(url, { signal: controller.signal });
34
+ clearTimeout(timer);
35
+ return res.ok || res.status === 404;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function checkSystem() {
41
+ const results = [];
42
+ const nodeVersion = process.versions.node;
43
+ const major = parseInt(nodeVersion.split(".")[0], 10);
44
+ results.push({
45
+ status: major >= 18 ? "ok" : "fail",
46
+ label: `Node ${nodeVersion}`,
47
+ detail: major >= 18 ? void 0 : "Node 18.18 or higher required"
48
+ });
49
+ const pnpmVersion = await tryExec("pnpm", ["--version"]);
50
+ if (pnpmVersion) {
51
+ results.push({ status: "ok", label: `pnpm ${pnpmVersion}` });
52
+ }
53
+ return { title: "System", results };
54
+ }
55
+ async function checkAndroid() {
56
+ const results = [];
57
+ const androidHome = process.env.ANDROID_HOME;
58
+ if (!androidHome) {
59
+ results.push({
60
+ status: "warn",
61
+ label: "ANDROID_HOME not set",
62
+ detail: "Required for Android testing. Set to your Android SDK path."
63
+ });
64
+ return { title: "Android", results };
65
+ }
66
+ results.push({
67
+ status: "ok",
68
+ label: `ANDROID_HOME=${androidHome}`
69
+ });
70
+ if (!existsSync(androidHome)) {
71
+ results.push({
72
+ status: "fail",
73
+ label: `Directory does not exist: ${androidHome}`
74
+ });
75
+ return { title: "Android", results };
76
+ }
77
+ const adb = await tryExec("adb", ["--version"]);
78
+ results.push({
79
+ status: adb ? "ok" : "warn",
80
+ label: "adb",
81
+ detail: adb ?? "Not in PATH. Add $ANDROID_HOME/platform-tools."
82
+ });
83
+ const emulatorHelp = await tryExec("emulator", ["-help-version"]);
84
+ results.push({
85
+ status: emulatorHelp !== null ? "ok" : "warn",
86
+ label: "emulator",
87
+ detail: emulatorHelp !== null ? void 0 : "Not in PATH. Add $ANDROID_HOME/emulator."
88
+ });
89
+ if (adb) {
90
+ const devices = await tryExec("adb", ["devices"]);
91
+ const lines = (devices ?? "").split("\n").slice(1).filter((l) => l.includes(" device"));
92
+ if (lines.length > 0) {
93
+ results.push({
94
+ status: "info",
95
+ label: `${lines.length} emulator${lines.length > 1 ? "s" : ""} booted`
96
+ });
97
+ }
98
+ }
99
+ return { title: "Android", results };
100
+ }
101
+ async function checkIOS() {
102
+ const results = [];
103
+ if (process.platform !== "darwin") {
104
+ results.push({
105
+ status: "info",
106
+ label: "iOS checks skipped (not macOS)"
107
+ });
108
+ return { title: "iOS", results };
109
+ }
110
+ const xcodeVersion = await tryExec("xcodebuild", ["-version"]);
111
+ results.push({
112
+ status: xcodeVersion ? "ok" : "warn",
113
+ label: xcodeVersion ?? "Xcode not found"
114
+ });
115
+ const xcrun = await tryExec("xcrun", ["--version"]);
116
+ results.push({
117
+ status: xcrun ? "ok" : "warn",
118
+ label: xcrun ? "xcrun" : "xcrun not in PATH"
119
+ });
120
+ const sims = await tryExec("xcrun", ["simctl", "list", "devices", "booted"]);
121
+ if (sims) {
122
+ const lines = sims.split("\n").filter((l) => l.includes("(Booted)"));
123
+ if (lines.length > 0) {
124
+ results.push({
125
+ status: "info",
126
+ label: `${lines.length} simulator${lines.length > 1 ? "s" : ""} booted`
127
+ });
128
+ }
129
+ }
130
+ return { title: "iOS", results };
131
+ }
132
+ async function checkAppium() {
133
+ const results = [];
134
+ const appiumVersion = await tryExec("appium", ["--version"]);
135
+ if (!appiumVersion) {
136
+ results.push({
137
+ status: "fail",
138
+ label: "Appium not installed",
139
+ detail: "npm install -g appium"
140
+ });
141
+ return { title: "Appium", results };
142
+ }
143
+ results.push({ status: "ok", label: `Appium ${appiumVersion}` });
144
+ const driverList = await tryExec("appium", ["driver", "list", "--installed"]);
145
+ if (driverList) {
146
+ const hasUiAutomator2 = driverList.includes("uiautomator2");
147
+ const hasXCUITest = driverList.includes("xcuitest");
148
+ results.push({
149
+ status: hasUiAutomator2 ? "ok" : "warn",
150
+ label: "uiautomator2 driver",
151
+ detail: hasUiAutomator2 ? void 0 : "appium driver install uiautomator2"
152
+ });
153
+ if (process.platform === "darwin") {
154
+ results.push({
155
+ status: hasXCUITest ? "ok" : "warn",
156
+ label: "xcuitest driver",
157
+ detail: hasXCUITest ? void 0 : "appium driver install xcuitest"
158
+ });
159
+ }
160
+ }
161
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
162
+ const reachable = await httpProbe(`http://localhost:${port}/status`);
163
+ results.push({
164
+ status: reachable ? "ok" : "warn",
165
+ label: `Server reachable at localhost:${port}`,
166
+ detail: reachable ? void 0 : "Start with `appium` in another terminal."
167
+ });
168
+ return { title: "Appium", results };
169
+ }
170
+ function checkEnvVars() {
171
+ const results = [];
172
+ const required = ["MOBWRIGHT_APP_PATH", "MOBWRIGHT_ANDROID_AVD"];
173
+ for (const key of required) {
174
+ const value = process.env[key];
175
+ results.push({
176
+ status: value ? "ok" : "warn",
177
+ label: `${key}${value ? " set" : " not set"}`,
178
+ detail: value ? void 0 : "Required for Android testing."
179
+ });
180
+ }
181
+ const aiProvider = process.env.MOBWRIGHT_AI_PROVIDER;
182
+ const aiKey = process.env.MOBWRIGHT_AI_API_KEY;
183
+ if (!aiProvider || !aiKey) {
184
+ results.push({
185
+ status: "warn",
186
+ label: "MOBWRIGHT_AI_API_KEY not set",
187
+ detail: "AI features (device.ai) unavailable until set."
188
+ });
189
+ } else {
190
+ results.push({
191
+ status: "ok",
192
+ label: `AI provider: ${aiProvider}`
193
+ });
194
+ }
195
+ return { title: "Environment variables", results };
196
+ }
197
+ function symbol(status) {
198
+ switch (status) {
199
+ case "ok":
200
+ return green("\u2714");
201
+ case "info":
202
+ return green("\u2713");
203
+ case "warn":
204
+ return yellow("\u2717");
205
+ case "fail":
206
+ return red("\u2717");
207
+ }
208
+ }
209
+ function renderSection(section) {
210
+ console.log("\n" + bold(section.title));
211
+ for (const r of section.results) {
212
+ const indent = " ";
213
+ console.log(`${indent}${symbol(r.status)} ${r.label}`);
214
+ if (r.detail) {
215
+ console.log(`${indent} ${dim(r.detail)}`);
216
+ }
217
+ }
218
+ }
219
+ async function runDoctor() {
220
+ console.log(bold("\u{1F50D} Mobwright environment check"));
221
+ const sections = await Promise.all([
222
+ checkSystem(),
223
+ checkAndroid(),
224
+ checkIOS(),
225
+ checkAppium()
226
+ ]);
227
+ sections.push(checkEnvVars());
228
+ for (const section of sections) {
229
+ renderSection(section);
230
+ }
231
+ const all = sections.flatMap((s) => s.results);
232
+ const fails = all.filter((r) => r.status === "fail").length;
233
+ const warns = all.filter((r) => r.status === "warn").length;
234
+ console.log("");
235
+ if (fails > 0) {
236
+ console.log(red(bold(`Result: ${fails} error${fails > 1 ? "s" : ""}, ${warns} warning${warns !== 1 ? "s" : ""}.`)));
237
+ console.log(dim("Fix errors above before running tests."));
238
+ return 1;
239
+ }
240
+ if (warns > 0) {
241
+ console.log(yellow(bold(`Result: ${warns} warning${warns > 1 ? "s" : ""}.`)));
242
+ console.log(dim("Tests should run, but some features may be limited."));
243
+ return 0;
244
+ }
245
+ console.log(green(bold("Result: All checks passed. \u2728")));
246
+ return 0;
247
+ }
248
+
249
+ // src/cli/init.ts
250
+ import { mkdir, writeFile, readdir } from "fs/promises";
251
+ import { existsSync as existsSync2 } from "fs";
252
+ import path from "path";
253
+ import { exec as execCb2 } from "child_process";
254
+ import { promisify as promisify2 } from "util";
255
+
256
+ // src/cli/prompt.ts
257
+ import { createInterface } from "readline/promises";
258
+ var rl = null;
259
+ function getInterface() {
260
+ if (!rl) {
261
+ rl = createInterface({ input: process.stdin, output: process.stdout });
262
+ }
263
+ return rl;
264
+ }
265
+ function closePrompt() {
266
+ if (rl) {
267
+ rl.close();
268
+ rl = null;
269
+ }
270
+ }
271
+ async function ask(question, defaultValue) {
272
+ const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
273
+ const answer = (await getInterface().question(`${question}${suffix}: `)).trim();
274
+ return answer || defaultValue || "";
275
+ }
276
+ async function confirm(question, defaultYes = true) {
277
+ const hint = defaultYes ? "Y/n" : "y/N";
278
+ const answer = (await getInterface().question(`${question} (${hint}): `)).trim().toLowerCase();
279
+ if (!answer) return defaultYes;
280
+ return answer === "y" || answer === "yes";
281
+ }
282
+ async function pickOne(question, options, defaultIndex = 0) {
283
+ console.log(question);
284
+ options.forEach((opt, idx) => {
285
+ const marker = idx === defaultIndex ? "\u203A" : " ";
286
+ console.log(` ${marker} ${idx + 1}. ${opt}`);
287
+ });
288
+ const raw = (await getInterface().question(`Choose (1-${options.length}, default ${defaultIndex + 1}): `)).trim();
289
+ const choice = raw ? parseInt(raw, 10) - 1 : defaultIndex;
290
+ if (Number.isNaN(choice) || choice < 0 || choice >= options.length) {
291
+ return options[defaultIndex];
292
+ }
293
+ return options[choice];
294
+ }
295
+
296
+ // src/cli/templates.ts
297
+ var PACKAGE_JSON = (opts) => JSON.stringify(
298
+ {
299
+ name: opts.name,
300
+ version: "0.1.0",
301
+ private: true,
302
+ type: "module",
303
+ scripts: {
304
+ test: "mobwright test",
305
+ "test:android": "mobwright test --project=android",
306
+ "test:ios": "mobwright test --project=ios",
307
+ doctor: "mobwright doctor"
308
+ },
309
+ devDependencies: {
310
+ "@playwright/test": "^1.48.0",
311
+ mobwright: "^0.1.0",
312
+ dotenv: "^16.4.0",
313
+ typescript: "^5.4.0"
314
+ }
315
+ },
316
+ null,
317
+ 2
318
+ ) + "\n";
319
+ var TSCONFIG = () => JSON.stringify(
320
+ {
321
+ compilerOptions: {
322
+ target: "ES2022",
323
+ module: "NodeNext",
324
+ moduleResolution: "NodeNext",
325
+ strict: true,
326
+ esModuleInterop: true,
327
+ skipLibCheck: true
328
+ },
329
+ include: ["tests/**/*", "playwright.config.ts"]
330
+ },
331
+ null,
332
+ 2
333
+ ) + "\n";
334
+ var PLAYWRIGHT_CONFIG = (opts) => {
335
+ const projects = opts.platforms.map(
336
+ (p) => ` {
337
+ name: '${p}',
338
+ testMatch: ['**/shared/**', '**/${p}/**'],
339
+ },`
340
+ ).join("\n");
341
+ return `import { defineConfig } from '@playwright/test';
342
+ import dotenv from 'dotenv';
343
+ import path from 'node:path';
344
+ import { fileURLToPath } from 'node:url';
345
+
346
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
347
+ dotenv.config({ path: path.join(__dirname, '.env') });
348
+
349
+ export default defineConfig({
350
+ testDir: './tests',
351
+ fullyParallel: false,
352
+ retries: 0,
353
+ workers: 1,
354
+ reporter: 'list',
355
+ timeout: 120_000,
356
+ projects: [
357
+ ${projects}
358
+ ],
359
+ });
360
+ `;
361
+ };
362
+ var ENV_EXAMPLE = (opts) => {
363
+ const lines = ["# Mobwright local config \u2014 copy to .env and fill in.", ""];
364
+ if (opts.platforms.includes("android")) {
365
+ lines.push(
366
+ "# Android",
367
+ "MOBWRIGHT_APP_PATH=/absolute/path/to/your/app.apk",
368
+ "MOBWRIGHT_ANDROID_AVD=Pixel_6_API_34",
369
+ "# MOBWRIGHT_ANDROID_APP_PACKAGE=com.example",
370
+ "# MOBWRIGHT_ANDROID_APP_ACTIVITY=com.example.MainActivity",
371
+ ""
372
+ );
373
+ }
374
+ if (opts.platforms.includes("ios")) {
375
+ lines.push(
376
+ "# iOS",
377
+ "MOBWRIGHT_IOS_DEVICE=iPhone 15",
378
+ "MOBWRIGHT_IOS_BUNDLE_ID=com.example.app",
379
+ "# MOBWRIGHT_IOS_APP_PATH=/absolute/path/to/My.app",
380
+ "# MOBWRIGHT_IOS_UDID=",
381
+ ""
382
+ );
383
+ }
384
+ if (opts.aiProvider !== "none") {
385
+ lines.push(
386
+ "# AI provider",
387
+ `MOBWRIGHT_AI_PROVIDER=${opts.aiProvider}`,
388
+ "MOBWRIGHT_AI_API_KEY=sk-...",
389
+ ""
390
+ );
391
+ } else {
392
+ lines.push(
393
+ "# AI provider (optional)",
394
+ "# MOBWRIGHT_AI_PROVIDER=deepseek",
395
+ "# MOBWRIGHT_AI_API_KEY=sk-...",
396
+ ""
397
+ );
398
+ }
399
+ return lines.join("\n");
400
+ };
401
+ var GITIGNORE = () => `node_modules/
402
+ .env
403
+ .env.local
404
+ test-results/
405
+ playwright-report/
406
+ *.log
407
+ .DS_Store
408
+ `;
409
+ var SANITY_SPEC = () => `import { test, expect } from 'mobwright';
410
+
411
+ test('session boots cleanly', async ({ device }) => {
412
+ expect(device.browser.sessionId).toBeTruthy();
413
+ console.log(\`\u2713 \${device.project} session\`);
414
+ });
415
+
416
+ test('can take a screenshot', async ({ device }) => {
417
+ const png = await device.screenshot();
418
+ expect(png.length).toBeGreaterThan(1000);
419
+ });
420
+ `;
421
+ var PLATFORM_EXAMPLE = (platform) => `import { test, expect } from 'mobwright';
422
+
423
+ test('${platform}: example test', async ({ device }) => {
424
+ // Replace with a real selector from your app
425
+ const greeting = device.locator('~welcome');
426
+
427
+ // Auto-waits up to 5 seconds for the element to appear
428
+ await expect(greeting).toBeVisible();
429
+ });
430
+ `;
431
+ var README = (opts) => `# ${opts.name}
432
+
433
+ Mobile e2e tests powered by [mobwright](https://www.npmjs.com/package/mobwright).
434
+
435
+ ## Setup
436
+
437
+ 1. Copy \`.env.example\` to \`.env\` and fill in your device info:
438
+ \`\`\`bash
439
+ cp .env.example .env
440
+ \`\`\`
441
+
442
+ 2. Verify your environment:
443
+ \`\`\`bash
444
+ npx mobwright doctor
445
+ \`\`\`
446
+
447
+ 3. Boot a device (Android emulator or iOS simulator).
448
+
449
+ 4. Start the Appium server in a separate terminal:
450
+ \`\`\`bash
451
+ appium
452
+ \`\`\`
453
+
454
+ 5. Run the tests:
455
+ \`\`\`bash
456
+ npm test # both platforms
457
+ npm run test:android # Android only${opts.platforms.includes("ios") ? `
458
+ npm run test:ios # iOS only` : ""}
459
+ \`\`\`
460
+
461
+ ## Project structure
462
+
463
+ \`\`\`
464
+ tests/
465
+ \u251C\u2500\u2500 shared/ # Tests that run on both platforms
466
+ ${opts.platforms.includes("android") ? "\u251C\u2500\u2500 android/ # Android-only tests\n" : ""}${opts.platforms.includes("ios") ? "\u2514\u2500\u2500 ios/ # iOS-only tests\n" : ""}\`\`\`
467
+
468
+ ## Learn more
469
+
470
+ - [Mobwright docs](https://github.com/yourname/mobwright)
471
+ - [Playwright test runner](https://playwright.dev/docs/test-intro)
472
+ `;
473
+
474
+ // src/cli/init.ts
475
+ var exec2 = promisify2(execCb2);
476
+ async function runInit(flags = {}) {
477
+ console.log(bold("\u{1FA84} Mobwright project setup\n"));
478
+ try {
479
+ const dirInput = flags.dir ?? (flags.yes ? "./my-mobile-tests" : await ask("Project directory", "./my-mobile-tests"));
480
+ const targetDir = path.resolve(dirInput);
481
+ if (existsSync2(targetDir)) {
482
+ const contents = await readdir(targetDir);
483
+ const nonHidden = contents.filter((f) => !f.startsWith("."));
484
+ if (nonHidden.length > 0 && !flags.force) {
485
+ console.error(red(`\u2717 Directory ${targetDir} is not empty.`));
486
+ console.error(dim(" Pass --force to overwrite, or pick another directory."));
487
+ return 1;
488
+ }
489
+ }
490
+ let platforms;
491
+ if (flags.platforms) {
492
+ platforms = parsePlatformsFlag(flags.platforms);
493
+ } else if (flags.yes) {
494
+ platforms = ["android", "ios"];
495
+ } else {
496
+ const choice = await pickOne(
497
+ "Which platforms?",
498
+ ["Both Android and iOS", "Android only", "iOS only"]
499
+ );
500
+ platforms = choice === "Android only" ? ["android"] : choice === "iOS only" ? ["ios"] : ["android", "ios"];
501
+ }
502
+ const aiProvider = flags.ai ? parseAIFlag(flags.ai) : flags.yes ? "none" : await pickOne(
503
+ "AI provider (optional)?",
504
+ ["none", "deepseek", "openai", "anthropic"]
505
+ );
506
+ const shouldInstall = flags.skipInstall ? false : flags.yes ? true : await confirm("Install dependencies now?", true);
507
+ const projectName = path.basename(targetDir).toLowerCase().replace(/[^a-z0-9-]/g, "-");
508
+ const opts = {
509
+ name: projectName,
510
+ platforms,
511
+ aiProvider
512
+ };
513
+ console.log("");
514
+ console.log(`Creating project at ${dim(targetDir)}...`);
515
+ await mkdir(targetDir, { recursive: true });
516
+ await writeFiles(targetDir, opts);
517
+ if (shouldInstall) {
518
+ console.log(" " + dim("Installing dependencies (this may take a minute)..."));
519
+ try {
520
+ await exec2("npm install", { cwd: targetDir });
521
+ console.log(` ${green("\u2714")} Dependencies installed`);
522
+ } catch (err) {
523
+ console.log(` ${yellow("\u2717")} Install failed. Run \`npm install\` manually in ${projectName}/.`);
524
+ }
525
+ }
526
+ printNextSteps(projectName, shouldInstall);
527
+ return 0;
528
+ } catch (err) {
529
+ if (err.code === "ERR_USE_AFTER_CLOSE") {
530
+ console.log("\n" + yellow("Cancelled."));
531
+ return 130;
532
+ }
533
+ console.error(red(`\u2717 ${err.message}`));
534
+ return 1;
535
+ } finally {
536
+ closePrompt();
537
+ }
538
+ }
539
+ async function writeFiles(targetDir, opts) {
540
+ const files = [
541
+ ["package.json", PACKAGE_JSON(opts)],
542
+ ["tsconfig.json", TSCONFIG()],
543
+ ["playwright.config.ts", PLAYWRIGHT_CONFIG(opts)],
544
+ [".env.example", ENV_EXAMPLE(opts)],
545
+ [".gitignore", GITIGNORE()],
546
+ ["README.md", README(opts)],
547
+ ["tests/shared/sanity.spec.ts", SANITY_SPEC()]
548
+ ];
549
+ for (const platform of opts.platforms) {
550
+ files.push([`tests/${platform}/example.spec.ts`, PLATFORM_EXAMPLE(platform)]);
551
+ }
552
+ for (const [relPath, content] of files) {
553
+ const fullPath = path.join(targetDir, relPath);
554
+ await mkdir(path.dirname(fullPath), { recursive: true });
555
+ await writeFile(fullPath, content, "utf8");
556
+ console.log(` ${green("\u2714")} ${relPath}`);
557
+ }
558
+ }
559
+ function parsePlatformsFlag(value) {
560
+ const parts = value.split(",").map((p) => p.trim().toLowerCase());
561
+ const result = [];
562
+ for (const p of parts) {
563
+ if (p === "android" || p === "ios") {
564
+ result.push(p);
565
+ } else {
566
+ throw new Error(`Unknown platform: ${p} (use 'android' or 'ios')`);
567
+ }
568
+ }
569
+ if (result.length === 0) {
570
+ throw new Error("At least one platform required");
571
+ }
572
+ return result;
573
+ }
574
+ function parseAIFlag(value) {
575
+ const v = value.trim().toLowerCase();
576
+ if (v === "none" || v === "anthropic" || v === "openai" || v === "deepseek") {
577
+ return v;
578
+ }
579
+ throw new Error(`Unknown AI provider: ${value} (use 'none', 'anthropic', 'openai', 'deepseek')`);
580
+ }
581
+ function printNextSteps(projectName, installed) {
582
+ console.log("");
583
+ console.log(green(bold("\u2728 Done!")) + " Next steps:\n");
584
+ console.log(` ${dim("# enter your new project")}`);
585
+ console.log(` cd ${projectName}
586
+ `);
587
+ if (!installed) {
588
+ console.log(` ${dim("# install dependencies")}`);
589
+ console.log(` npm install
590
+ `);
591
+ }
592
+ console.log(` ${dim("# configure your devices")}`);
593
+ console.log(` cp .env.example .env
594
+ `);
595
+ console.log(` ${dim("# verify your environment")}`);
596
+ console.log(` npx mobwright doctor
597
+ `);
598
+ console.log(` ${dim("# boot a device + run Appium in another terminal, then:")}`);
599
+ console.log(` npx playwright test`);
600
+ console.log("");
601
+ }
602
+
603
+ // src/cli/test.ts
604
+ import { spawn } from "child_process";
605
+ async function runTest(flags) {
606
+ console.log(bold("\u{1F3AF} Mobwright \u2014 running tests") + "\n");
607
+ if (!flags.noPreflight) {
608
+ const ok = await checkAppium2();
609
+ if (!ok) {
610
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
611
+ console.error(red("\u2717") + ` Appium server not reachable at http://localhost:${port}.`);
612
+ console.error(dim(" Start it in another terminal: ") + bold("appium"));
613
+ console.error(dim(" Or skip this check: ") + bold("npx mobwright test --no-preflight"));
614
+ console.error("");
615
+ return 1;
616
+ }
617
+ console.log(green("\u2714") + " Appium reachable");
618
+ console.log("");
619
+ }
620
+ return await forwardToPlaywright(flags.rest);
621
+ }
622
+ async function checkAppium2() {
623
+ const port = process.env.MOBWRIGHT_APPIUM_PORT ?? "4723";
624
+ const url = `http://localhost:${port}/status`;
625
+ try {
626
+ const controller = new AbortController();
627
+ const timer = setTimeout(() => controller.abort(), 1500);
628
+ const res = await fetch(url, { signal: controller.signal });
629
+ clearTimeout(timer);
630
+ return res.ok;
631
+ } catch {
632
+ return false;
633
+ }
634
+ }
635
+ function forwardToPlaywright(args) {
636
+ return new Promise((resolve) => {
637
+ const child = spawn("npx", ["playwright", "test", ...args], {
638
+ stdio: "inherit",
639
+ shell: process.platform === "win32"
640
+ // Windows needs shell=true for npx
641
+ });
642
+ child.on("exit", (code, signal) => {
643
+ if (signal) {
644
+ resolve(128);
645
+ return;
646
+ }
647
+ resolve(code ?? 0);
648
+ });
649
+ child.on("error", (err) => {
650
+ console.error(red("\u2717") + ` Failed to spawn playwright: ${err.message}`);
651
+ console.error(dim(" Is @playwright/test installed?"));
652
+ resolve(1);
653
+ });
654
+ });
655
+ }
656
+
657
+ // src/cli/index.ts
658
+ async function main() {
659
+ const args = process.argv.slice(2);
660
+ const command = args[0];
661
+ switch (command) {
662
+ case "doctor": {
663
+ const exitCode = await runDoctor();
664
+ process.exit(exitCode);
665
+ }
666
+ case "init": {
667
+ const flags = parseInitFlags(args.slice(1));
668
+ const exitCode = await runInit(flags);
669
+ process.exit(exitCode);
670
+ }
671
+ case "test": {
672
+ const flags = parseTestFlags(args.slice(1));
673
+ const exitCode = await runTest(flags);
674
+ process.exit(exitCode);
675
+ }
676
+ case void 0:
677
+ case "-h":
678
+ case "--help":
679
+ case "help": {
680
+ printHelp();
681
+ process.exit(0);
682
+ }
683
+ case "-v":
684
+ case "--version": {
685
+ console.log("mobwright 0.1.0");
686
+ process.exit(0);
687
+ }
688
+ default: {
689
+ console.error(`Unknown command: ${command}`);
690
+ console.error("");
691
+ printHelp();
692
+ process.exit(2);
693
+ }
694
+ }
695
+ }
696
+ function parseInitFlags(args) {
697
+ const flags = {};
698
+ for (let i = 0; i < args.length; i++) {
699
+ const arg = args[i];
700
+ if (arg === "-y" || arg === "--yes") {
701
+ flags.yes = true;
702
+ } else if (arg === "--force") {
703
+ flags.force = true;
704
+ } else if (arg === "--skip-install") {
705
+ flags.skipInstall = true;
706
+ } else if (arg === "--dir") {
707
+ flags.dir = args[++i];
708
+ } else if (arg.startsWith("--dir=")) {
709
+ flags.dir = arg.slice("--dir=".length);
710
+ } else if (arg === "--platforms") {
711
+ flags.platforms = args[++i];
712
+ } else if (arg.startsWith("--platforms=")) {
713
+ flags.platforms = arg.slice("--platforms=".length);
714
+ } else if (arg === "--ai") {
715
+ flags.ai = args[++i];
716
+ } else if (arg.startsWith("--ai=")) {
717
+ flags.ai = arg.slice("--ai=".length);
718
+ } else if (!arg.startsWith("-") && !flags.dir) {
719
+ flags.dir = arg;
720
+ }
721
+ }
722
+ return flags;
723
+ }
724
+ function parseTestFlags(args) {
725
+ const flags = { rest: [] };
726
+ for (let i = 0; i < args.length; i++) {
727
+ const arg = args[i];
728
+ if (arg === "--no-preflight") {
729
+ flags.noPreflight = true;
730
+ } else {
731
+ flags.rest.push(arg);
732
+ }
733
+ }
734
+ return flags;
735
+ }
736
+ function printHelp() {
737
+ console.log(`mobwright \u2014 mobile e2e testing with AI
738
+
739
+ Usage:
740
+ mobwright <command> [options]
741
+
742
+ Commands:
743
+ init [dir] Scaffold a new mobwright project
744
+ test Run your test suite
745
+ doctor Check your environment for required tools and config
746
+ help Show this help
747
+
748
+ init options:
749
+ --yes, -y Accept all defaults (no prompts)
750
+ --dir <path> Project directory (default: ./my-mobile-tests)
751
+ --platforms <list> Comma-separated: android,ios (default: both)
752
+ --ai <provider> none, deepseek, openai, anthropic (default: none)
753
+ --skip-install Don't run npm install
754
+ --force Overwrite non-empty target directory
755
+
756
+ test options:
757
+ --no-preflight Skip Appium reachability check
758
+ All other args are forwarded to \`playwright test\`.
759
+
760
+ Examples:
761
+ mobwright init
762
+ mobwright test --project=android
763
+ mobwright test --grep "login" --workers=2
764
+ mobwright doctor
765
+
766
+ Docs: https://github.com/yourname/mobwright
767
+ `);
768
+ }
769
+ main().catch((err) => {
770
+ console.error("Unexpected error:", err);
771
+ process.exit(1);
772
+ });
773
+ //# sourceMappingURL=index.js.map