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