@unotest/mobile 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/.claude/skills/write-e2e-test.md +294 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/mcp.js +66 -0
- package/dist/mcp/server.js +6026 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/runner/cli.js +5598 -0
- package/dist/runner/cli.js.map +1 -0
- package/dist/runner/doctor.js +158 -0
- package/dist/runner/doctor.js.map +1 -0
- package/dist/runner/init.js +453 -0
- package/dist/runner/init.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/runner/init/environment-check.ts
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
var ExecError = class extends Error {
|
|
7
|
+
constructor(cmd, args) {
|
|
8
|
+
super(`${cmd} ${args.join(" ")} failed`);
|
|
9
|
+
this.cmd = cmd;
|
|
10
|
+
this.args = args;
|
|
11
|
+
}
|
|
12
|
+
cmd;
|
|
13
|
+
args;
|
|
14
|
+
static {
|
|
15
|
+
__name(this, "ExecError");
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function defaultExec(cmd, args) {
|
|
19
|
+
try {
|
|
20
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
21
|
+
} catch {
|
|
22
|
+
throw new ExecError(cmd, args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
__name(defaultExec, "defaultExec");
|
|
26
|
+
function runEnvironmentChecks(opts = {}) {
|
|
27
|
+
const results = [];
|
|
28
|
+
const platform = opts.platform ?? process.platform;
|
|
29
|
+
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
30
|
+
const exec = opts.exec ?? defaultExec;
|
|
31
|
+
if (platform !== "darwin") {
|
|
32
|
+
results.push({
|
|
33
|
+
name: "platform",
|
|
34
|
+
severity: opts.allowNonMacos ? "warning" : "error",
|
|
35
|
+
message: `macOS required (got "${platform}"). iOS simulators only run on macOS (Apple licensing).`,
|
|
36
|
+
detail: opts.allowNonMacos ? "Continuing with --allow-non-macos. Tests cannot run without a macOS host." : "Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner)."
|
|
37
|
+
});
|
|
38
|
+
if (!opts.allowNonMacos) return results;
|
|
39
|
+
} else {
|
|
40
|
+
results.push({ name: "platform", severity: "ok", message: "macOS detected" });
|
|
41
|
+
}
|
|
42
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
43
|
+
if (major < 20) {
|
|
44
|
+
results.push({
|
|
45
|
+
name: "node",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `Node 20+ required (got ${nodeVersion}).`,
|
|
48
|
+
detail: "Upgrade via nvm/fnm/volta."
|
|
49
|
+
});
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
results.push({ name: "node", severity: "ok", message: `Node ${nodeVersion}` });
|
|
53
|
+
if (platform !== "darwin") return results;
|
|
54
|
+
try {
|
|
55
|
+
const xcodePath = exec("xcode-select", ["-p"]).trim();
|
|
56
|
+
results.push({ name: "xcode-cli", severity: "ok", message: `Xcode CLI tools at ${xcodePath}` });
|
|
57
|
+
} catch {
|
|
58
|
+
results.push({
|
|
59
|
+
name: "xcode-cli",
|
|
60
|
+
severity: "error",
|
|
61
|
+
message: "Xcode Command Line Tools not found.",
|
|
62
|
+
detail: "Install: `xcode-select --install`"
|
|
63
|
+
});
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
exec("xcrun", ["simctl", "help"]);
|
|
68
|
+
results.push({ name: "simctl", severity: "ok", message: "xcrun simctl available" });
|
|
69
|
+
} catch {
|
|
70
|
+
results.push({
|
|
71
|
+
name: "simctl",
|
|
72
|
+
severity: "error",
|
|
73
|
+
message: "xcrun simctl not available.",
|
|
74
|
+
detail: "Install full Xcode (from Mac App Store), not just Command Line Tools."
|
|
75
|
+
});
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const json = exec("xcrun", ["simctl", "list", "devices", "available", "--json"]);
|
|
80
|
+
const parsed = JSON.parse(json);
|
|
81
|
+
const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);
|
|
82
|
+
if (total === 0) {
|
|
83
|
+
results.push({
|
|
84
|
+
name: "simulators",
|
|
85
|
+
severity: "warning",
|
|
86
|
+
message: "No iOS simulators found.",
|
|
87
|
+
detail: "Create one in Xcode \u2192 Window \u2192 Devices and Simulators \u2192 '+'."
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
results.push({ name: "simulators", severity: "ok", message: `${total} simulators available` });
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
results.push({
|
|
94
|
+
name: "simulators",
|
|
95
|
+
severity: "warning",
|
|
96
|
+
message: "Could not enumerate simulators."
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const pkgRaw = exec("cat", ["package.json"]);
|
|
101
|
+
const pkg = JSON.parse(pkgRaw);
|
|
102
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
103
|
+
const isRn = "react-native" in deps || "expo" in deps;
|
|
104
|
+
if (!isRn) {
|
|
105
|
+
results.push({
|
|
106
|
+
name: "rn-project",
|
|
107
|
+
severity: "warning",
|
|
108
|
+
message: "package.json doesn't declare react-native or expo.",
|
|
109
|
+
detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
results.push({
|
|
116
|
+
name: "rn-project",
|
|
117
|
+
severity: "warning",
|
|
118
|
+
message: "No readable package.json in current directory.",
|
|
119
|
+
detail: "Run init from your project's root."
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
__name(runEnvironmentChecks, "runEnvironmentChecks");
|
|
125
|
+
|
|
126
|
+
// src/runner/doctor.ts
|
|
127
|
+
function symbol(severity) {
|
|
128
|
+
return severity === "ok" ? "\u2713" : severity === "warning" ? "\u26A0" : "\u2717";
|
|
129
|
+
}
|
|
130
|
+
__name(symbol, "symbol");
|
|
131
|
+
function main() {
|
|
132
|
+
const argv = process.argv.slice(2);
|
|
133
|
+
const allowNonMacos = argv.includes("--allow-non-macos");
|
|
134
|
+
console.log("unotest-mobile doctor\n");
|
|
135
|
+
const checks = runEnvironmentChecks({ allowNonMacos });
|
|
136
|
+
let hardFailed = false;
|
|
137
|
+
let warnings = 0;
|
|
138
|
+
for (const c of checks) {
|
|
139
|
+
console.log(`${symbol(c.severity)} ${c.name}: ${c.message}`);
|
|
140
|
+
if (c.detail) console.log(` ${c.detail}`);
|
|
141
|
+
if (c.severity === "error") hardFailed = true;
|
|
142
|
+
if (c.severity === "warning") warnings++;
|
|
143
|
+
}
|
|
144
|
+
if (hardFailed) {
|
|
145
|
+
console.error("\nEnvironment not ready.");
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
if (warnings > 0) {
|
|
149
|
+
console.log(`
|
|
150
|
+
${warnings} warning(s). Tests may still work, but review them.`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
console.log("\nEnvironment OK.");
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
__name(main, "main");
|
|
157
|
+
process.exit(main());
|
|
158
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/runner/init/environment-check.ts","../../src/runner/doctor.ts"],"sourcesContent":["// Environment validation for `unotest-mobile init` and `doctor`. Each check\n// is small + injectable (exec function) for unit testing without touching\n// the host environment.\n\nimport { execFileSync } from \"node:child_process\";\n\nexport type CheckSeverity = \"ok\" | \"warning\" | \"error\";\n\nexport interface CheckResult {\n name: string;\n severity: CheckSeverity;\n message: string;\n detail?: string;\n}\n\nexport interface EnvironmentCheckOptions {\n allowNonMacos?: boolean;\n // Injectable for tests:\n platform?: NodeJS.Platform;\n nodeVersion?: string;\n exec?: (cmd: string, args: string[]) => string;\n}\n\nclass ExecError extends Error {\n constructor(public readonly cmd: string, public readonly args: string[]) {\n super(`${cmd} ${args.join(\" \")} failed`);\n }\n}\n\nfunction defaultExec(cmd: string, args: string[]): string {\n try {\n return execFileSync(cmd, args, { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n } catch {\n throw new ExecError(cmd, args);\n }\n}\n\nexport function runEnvironmentChecks(opts: EnvironmentCheckOptions = {}): CheckResult[] {\n const results: CheckResult[] = [];\n const platform = opts.platform ?? process.platform;\n const nodeVersion = opts.nodeVersion ?? process.versions.node;\n const exec = opts.exec ?? defaultExec;\n\n // 1. Platform — hard requirement for iOS Simulator (Apple-only).\n if (platform !== \"darwin\") {\n results.push({\n name: \"platform\",\n severity: opts.allowNonMacos ? \"warning\" : \"error\",\n message: `macOS required (got \"${platform}\"). iOS simulators only run on macOS (Apple licensing).`,\n detail: opts.allowNonMacos\n ? \"Continuing with --allow-non-macos. Tests cannot run without a macOS host.\"\n : \"Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner).\",\n });\n if (!opts.allowNonMacos) return results;\n } else {\n results.push({ name: \"platform\", severity: \"ok\", message: \"macOS detected\" });\n }\n\n // 2. Node version.\n const major = Number.parseInt(nodeVersion.split(\".\")[0] ?? \"0\", 10);\n if (major < 20) {\n results.push({\n name: \"node\",\n severity: \"error\",\n message: `Node 20+ required (got ${nodeVersion}).`,\n detail: \"Upgrade via nvm/fnm/volta.\",\n });\n return results;\n }\n results.push({ name: \"node\", severity: \"ok\", message: `Node ${nodeVersion}` });\n\n // Mac-only checks below.\n if (platform !== \"darwin\") return results;\n\n // 3. Xcode CLI tools.\n try {\n const xcodePath = exec(\"xcode-select\", [\"-p\"]).trim();\n results.push({ name: \"xcode-cli\", severity: \"ok\", message: `Xcode CLI tools at ${xcodePath}` });\n } catch {\n results.push({\n name: \"xcode-cli\",\n severity: \"error\",\n message: \"Xcode Command Line Tools not found.\",\n detail: \"Install: `xcode-select --install`\",\n });\n return results;\n }\n\n // 4. xcrun simctl.\n try {\n exec(\"xcrun\", [\"simctl\", \"help\"]);\n results.push({ name: \"simctl\", severity: \"ok\", message: \"xcrun simctl available\" });\n } catch {\n results.push({\n name: \"simctl\",\n severity: \"error\",\n message: \"xcrun simctl not available.\",\n detail: \"Install full Xcode (from Mac App Store), not just Command Line Tools.\",\n });\n return results;\n }\n\n // 5. iOS simulators present.\n try {\n const json = exec(\"xcrun\", [\"simctl\", \"list\", \"devices\", \"available\", \"--json\"]);\n const parsed = JSON.parse(json) as { devices: Record<string, unknown[]> };\n const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);\n if (total === 0) {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"No iOS simulators found.\",\n detail: \"Create one in Xcode → Window → Devices and Simulators → '+'.\",\n });\n } else {\n results.push({ name: \"simulators\", severity: \"ok\", message: `${total} simulators available` });\n }\n } catch {\n results.push({\n name: \"simulators\",\n severity: \"warning\",\n message: \"Could not enumerate simulators.\",\n });\n }\n\n // 6. Project type — soft warning if not RN/Expo.\n try {\n const pkgRaw = exec(\"cat\", [\"package.json\"]);\n const pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n const isRn = \"react-native\" in deps || \"expo\" in deps;\n if (!isRn) {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"package.json doesn't declare react-native or expo.\",\n detail: \"unotest-mobile is iOS RN/Expo-focused. Continuing — but make sure this is intentional.\",\n });\n } else {\n results.push({ name: \"rn-project\", severity: \"ok\", message: \"React Native / Expo detected\" });\n }\n } catch {\n results.push({\n name: \"rn-project\",\n severity: \"warning\",\n message: \"No readable package.json in current directory.\",\n detail: \"Run init from your project's root.\",\n });\n }\n\n return results;\n}\n","// `unotest-mobile doctor` — re-run environment checks without touching files.\n// Useful when init was run earlier but something changed (Xcode updated,\n// sims deleted, etc.).\n\nimport { runEnvironmentChecks } from \"./init/environment-check.ts\";\n\nfunction symbol(severity: \"ok\" | \"warning\" | \"error\"): string {\n return severity === \"ok\" ? \"✓\" : severity === \"warning\" ? \"⚠\" : \"✗\";\n}\n\nfunction main(): number {\n const argv = process.argv.slice(2);\n const allowNonMacos = argv.includes(\"--allow-non-macos\");\n\n console.log(\"unotest-mobile doctor\\n\");\n const checks = runEnvironmentChecks({ allowNonMacos });\n\n let hardFailed = false;\n let warnings = 0;\n for (const c of checks) {\n console.log(`${symbol(c.severity)} ${c.name}: ${c.message}`);\n if (c.detail) console.log(` ${c.detail}`);\n if (c.severity === \"error\") hardFailed = true;\n if (c.severity === \"warning\") warnings++;\n }\n\n if (hardFailed) {\n console.error(\"\\nEnvironment not ready.\");\n return 1;\n }\n if (warnings > 0) {\n console.log(`\\n${warnings} warning(s). Tests may still work, but review them.`);\n return 0;\n }\n console.log(\"\\nEnvironment OK.\");\n return 0;\n}\n\nprocess.exit(main());\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/runner/init.ts
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, join, relative, resolve } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/runner/init/environment-check.ts
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
var ExecError = class extends Error {
|
|
12
|
+
constructor(cmd, args) {
|
|
13
|
+
super(`${cmd} ${args.join(" ")} failed`);
|
|
14
|
+
this.cmd = cmd;
|
|
15
|
+
this.args = args;
|
|
16
|
+
}
|
|
17
|
+
cmd;
|
|
18
|
+
args;
|
|
19
|
+
static {
|
|
20
|
+
__name(this, "ExecError");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function defaultExec(cmd, args) {
|
|
24
|
+
try {
|
|
25
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ExecError(cmd, args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
__name(defaultExec, "defaultExec");
|
|
31
|
+
function runEnvironmentChecks(opts = {}) {
|
|
32
|
+
const results = [];
|
|
33
|
+
const platform = opts.platform ?? process.platform;
|
|
34
|
+
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
35
|
+
const exec = opts.exec ?? defaultExec;
|
|
36
|
+
if (platform !== "darwin") {
|
|
37
|
+
results.push({
|
|
38
|
+
name: "platform",
|
|
39
|
+
severity: opts.allowNonMacos ? "warning" : "error",
|
|
40
|
+
message: `macOS required (got "${platform}"). iOS simulators only run on macOS (Apple licensing).`,
|
|
41
|
+
detail: opts.allowNonMacos ? "Continuing with --allow-non-macos. Tests cannot run without a macOS host." : "Re-run with --allow-non-macos if you only need to scaffold files (e.g. preparing a macOS CI runner)."
|
|
42
|
+
});
|
|
43
|
+
if (!opts.allowNonMacos) return results;
|
|
44
|
+
} else {
|
|
45
|
+
results.push({ name: "platform", severity: "ok", message: "macOS detected" });
|
|
46
|
+
}
|
|
47
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
48
|
+
if (major < 20) {
|
|
49
|
+
results.push({
|
|
50
|
+
name: "node",
|
|
51
|
+
severity: "error",
|
|
52
|
+
message: `Node 20+ required (got ${nodeVersion}).`,
|
|
53
|
+
detail: "Upgrade via nvm/fnm/volta."
|
|
54
|
+
});
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
results.push({ name: "node", severity: "ok", message: `Node ${nodeVersion}` });
|
|
58
|
+
if (platform !== "darwin") return results;
|
|
59
|
+
try {
|
|
60
|
+
const xcodePath = exec("xcode-select", ["-p"]).trim();
|
|
61
|
+
results.push({ name: "xcode-cli", severity: "ok", message: `Xcode CLI tools at ${xcodePath}` });
|
|
62
|
+
} catch {
|
|
63
|
+
results.push({
|
|
64
|
+
name: "xcode-cli",
|
|
65
|
+
severity: "error",
|
|
66
|
+
message: "Xcode Command Line Tools not found.",
|
|
67
|
+
detail: "Install: `xcode-select --install`"
|
|
68
|
+
});
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
exec("xcrun", ["simctl", "help"]);
|
|
73
|
+
results.push({ name: "simctl", severity: "ok", message: "xcrun simctl available" });
|
|
74
|
+
} catch {
|
|
75
|
+
results.push({
|
|
76
|
+
name: "simctl",
|
|
77
|
+
severity: "error",
|
|
78
|
+
message: "xcrun simctl not available.",
|
|
79
|
+
detail: "Install full Xcode (from Mac App Store), not just Command Line Tools."
|
|
80
|
+
});
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const json = exec("xcrun", ["simctl", "list", "devices", "available", "--json"]);
|
|
85
|
+
const parsed = JSON.parse(json);
|
|
86
|
+
const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);
|
|
87
|
+
if (total === 0) {
|
|
88
|
+
results.push({
|
|
89
|
+
name: "simulators",
|
|
90
|
+
severity: "warning",
|
|
91
|
+
message: "No iOS simulators found.",
|
|
92
|
+
detail: "Create one in Xcode \u2192 Window \u2192 Devices and Simulators \u2192 '+'."
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
results.push({ name: "simulators", severity: "ok", message: `${total} simulators available` });
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
results.push({
|
|
99
|
+
name: "simulators",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
message: "Could not enumerate simulators."
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const pkgRaw = exec("cat", ["package.json"]);
|
|
106
|
+
const pkg = JSON.parse(pkgRaw);
|
|
107
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
108
|
+
const isRn = "react-native" in deps || "expo" in deps;
|
|
109
|
+
if (!isRn) {
|
|
110
|
+
results.push({
|
|
111
|
+
name: "rn-project",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
message: "package.json doesn't declare react-native or expo.",
|
|
114
|
+
detail: "unotest-mobile is iOS RN/Expo-focused. Continuing \u2014 but make sure this is intentional."
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
results.push({ name: "rn-project", severity: "ok", message: "React Native / Expo detected" });
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
results.push({
|
|
121
|
+
name: "rn-project",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message: "No readable package.json in current directory.",
|
|
124
|
+
detail: "Run init from your project's root."
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
__name(runEnvironmentChecks, "runEnvironmentChecks");
|
|
130
|
+
|
|
131
|
+
// src/runner/init/file-templates.ts
|
|
132
|
+
var templates = {
|
|
133
|
+
smokeWelcome: `// id-smoke-welcome
|
|
134
|
+
// First-run sanity: harness reaches sim, WDA, and app launch handshake
|
|
135
|
+
// #00aa00
|
|
136
|
+
function test_smoke_welcome() {
|
|
137
|
+
setDevice("A");
|
|
138
|
+
appLaunch(true);
|
|
139
|
+
}
|
|
140
|
+
`,
|
|
141
|
+
scenarioTemplate: `// id-<your-scenario-id>
|
|
142
|
+
// <one-line description>
|
|
143
|
+
// #888888
|
|
144
|
+
function test_<your_name>() {
|
|
145
|
+
// 1. SETUP \u2014 DB / API fixtures (helpers from _helpers/)
|
|
146
|
+
// wipe_e2e_users();
|
|
147
|
+
// user_id = seed_user("e2e@example.com", "e2e-pass");
|
|
148
|
+
|
|
149
|
+
// 2. ENTER \u2014 bring app to initial UI state
|
|
150
|
+
setDevice("A");
|
|
151
|
+
appLaunch(true);
|
|
152
|
+
waitFor(getByTestId("screen-welcome"), 15000);
|
|
153
|
+
|
|
154
|
+
// 3. ACT \u2014 actions you're testing
|
|
155
|
+
// signin("e2e@example.com", "e2e-pass");
|
|
156
|
+
// tap(getByTestId("btn-something"));
|
|
157
|
+
|
|
158
|
+
// 4. ASSERT \u2014 UI + DB / API checks
|
|
159
|
+
// assertVisible(getByTestId("screen-target"));
|
|
160
|
+
// count = dbQuery("SELECT count(*)::text FROM x WHERE ...");
|
|
161
|
+
// assertEqual(count, "1");
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
agentsMd: `# AI Agents: how to write e2e tests for this project
|
|
165
|
+
|
|
166
|
+
This project uses [\`@unotest/mobile\`](https://www.npmjs.com/package/@unotest/mobile)
|
|
167
|
+
for end-to-end testing of iOS React Native flows.
|
|
168
|
+
|
|
169
|
+
## Layout
|
|
170
|
+
|
|
171
|
+
- \`unotest/e2e/\` \u2014 scenarios (one \`test_*\` entry function per file)
|
|
172
|
+
- \`unotest/e2e/_helpers/\` \u2014 project-specific helpers, visible to all scenarios
|
|
173
|
+
- \`unotest/e2e/<feature>/\` \u2014 feature-scoped subfolders (recommended)
|
|
174
|
+
|
|
175
|
+
## How to write a test
|
|
176
|
+
|
|
177
|
+
The full guide \u2014 DSL functions, scenario shape, helper rules, linter codes,
|
|
178
|
+
debugging \u2014 is the \`write-e2e-test\` Claude Code skill at
|
|
179
|
+
\`.claude/skills/write-e2e-test.md\`. It is the single source of truth for
|
|
180
|
+
both Claude Code and other AI agents. Read it before writing tests.
|
|
181
|
+
|
|
182
|
+
## Running
|
|
183
|
+
|
|
184
|
+
- \`npx @unotest/mobile e2e <name>\` \u2014 run \`unotest/e2e/<name>.js\`
|
|
185
|
+
- \`npx @unotest/mobile lint\` \u2014 static check of all scenarios
|
|
186
|
+
- \`npx @unotest/mobile doctor\` \u2014 re-check environment
|
|
187
|
+
|
|
188
|
+
Inside Claude Code, prefer the MCP \`run_test\` tool with
|
|
189
|
+
\`pauseOnFailure: true\` \u2014 it pauses on the failed step so you can
|
|
190
|
+
\`inspect_runtime\`, fix the scenario, and \`resume\`.
|
|
191
|
+
`,
|
|
192
|
+
envExample: `# unotest-mobile \u2014 copied to unotest/.env by \`init\`. Fill in below.
|
|
193
|
+
|
|
194
|
+
# --- App under test --------------------------------------------------------
|
|
195
|
+
APP_BUNDLE_ID=com.example.myapp
|
|
196
|
+
APP_URL_SCHEME=myapp
|
|
197
|
+
INVITE_DEEPLINK_PREFIX=myapp://invite/
|
|
198
|
+
|
|
199
|
+
# --- Backend ---------------------------------------------------------------
|
|
200
|
+
API_BASE_URL=http://localhost:3000/api
|
|
201
|
+
# Optional. Absolute path used as default cwd for the \`shell(...)\` DSL
|
|
202
|
+
# primitive. Set if your scenarios shell out to project-local CLIs
|
|
203
|
+
# (pnpm cli, rails runner, etc.) that must run from the project root.
|
|
204
|
+
PROJECT_ROOT=
|
|
205
|
+
|
|
206
|
+
# --- Database --------------------------------------------------------------
|
|
207
|
+
# Pick the driver matching your backend. Install peer-deps only as needed:
|
|
208
|
+
# npm i -D pg @types/pg # for postgresql://
|
|
209
|
+
# npm i -D mysql2 # for mysql://
|
|
210
|
+
# (SQLite via better-sqlite3 is bundled \u2014 no separate install.)
|
|
211
|
+
#
|
|
212
|
+
# Docker-compose Postgres/MySQL \u2014 ensure host port is exposed
|
|
213
|
+
# (\`ports: ["5432:5432"]\`). Native clients connect from the host, not from
|
|
214
|
+
# inside the compose network.
|
|
215
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp_test
|
|
216
|
+
|
|
217
|
+
# --- Simulators ------------------------------------------------------------
|
|
218
|
+
# Names must match \`xcrun simctl list devices\`.
|
|
219
|
+
SIM_A_NAME=iPhone 15
|
|
220
|
+
SIM_B_NAME=iPhone 15 Plus
|
|
221
|
+
SIM_POOL=A,B
|
|
222
|
+
|
|
223
|
+
# --- Dev tooling -----------------------------------------------------------
|
|
224
|
+
METRO_URL=http://localhost:8081
|
|
225
|
+
# Expo dev-client: auto-open dev-client deep link after a clean launch.
|
|
226
|
+
EXPO_DEV_CLIENT=false
|
|
227
|
+
SESSION_LOG_PATH=sessions/current.jsonl
|
|
228
|
+
ARTIFACTS_DIR=artifacts
|
|
229
|
+
|
|
230
|
+
# --- WebDriverAgent --------------------------------------------------------
|
|
231
|
+
# Per-slot WDA ports. Each slot in SIM_POOL needs one. Format: "slot=port".
|
|
232
|
+
WDA_PORTS=A=8100,B=8101
|
|
233
|
+
WDA_DEFAULT_ACTION_WAIT_MS=2000
|
|
234
|
+
WDA_DEFAULT_WAITFOR_TIMEOUT_MS=10000
|
|
235
|
+
|
|
236
|
+
# --- D-17 paused-runtime TTL ----------------------------------------------
|
|
237
|
+
# How long a paused-on-failure runtime stays alive before auto-abort (ms).
|
|
238
|
+
# Default 30 minutes. Inspecting the runtime resets the timer.
|
|
239
|
+
PAUSED_RUNTIME_TTL_MS=1800000
|
|
240
|
+
`,
|
|
241
|
+
gitignoreLines: [
|
|
242
|
+
"",
|
|
243
|
+
"# unotest-mobile",
|
|
244
|
+
"unotest/.env",
|
|
245
|
+
"unotest/artifacts/",
|
|
246
|
+
"unotest/sessions/"
|
|
247
|
+
],
|
|
248
|
+
mcpServerEntry: {
|
|
249
|
+
command: "npx",
|
|
250
|
+
args: ["-y", "@unotest/mobile"]
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/runner/init/mcp-config-merger.ts
|
|
255
|
+
var McpJsonParseError = class extends Error {
|
|
256
|
+
static {
|
|
257
|
+
__name(this, "McpJsonParseError");
|
|
258
|
+
}
|
|
259
|
+
constructor(message) {
|
|
260
|
+
super(message);
|
|
261
|
+
this.name = "McpJsonParseError";
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function mergeMcpConfig(existingSource, serverName, serverConfig, options = {}) {
|
|
265
|
+
if (existingSource === null) {
|
|
266
|
+
const content2 = JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2) + "\n";
|
|
267
|
+
return { action: "created", newContent: content2 };
|
|
268
|
+
}
|
|
269
|
+
let parsed;
|
|
270
|
+
try {
|
|
271
|
+
parsed = JSON.parse(existingSource);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
throw new McpJsonParseError(
|
|
274
|
+
`Failed to parse .mcp.json as JSON: ${e.message}. Fix or remove the file and re-run init.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") {
|
|
278
|
+
parsed.mcpServers = {};
|
|
279
|
+
}
|
|
280
|
+
const alreadyPresent = serverName in parsed.mcpServers;
|
|
281
|
+
if (alreadyPresent && !options.force) {
|
|
282
|
+
return { action: "already-present", newContent: existingSource };
|
|
283
|
+
}
|
|
284
|
+
parsed.mcpServers[serverName] = serverConfig;
|
|
285
|
+
const content = JSON.stringify(parsed, null, 2) + "\n";
|
|
286
|
+
return {
|
|
287
|
+
action: alreadyPresent ? "force-overwrote" : "added",
|
|
288
|
+
newContent: content
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
__name(mergeMcpConfig, "mergeMcpConfig");
|
|
292
|
+
|
|
293
|
+
// src/runner/init/gitignore-updater.ts
|
|
294
|
+
function appendUniqueLines(existingContent, linesToAdd) {
|
|
295
|
+
const existing = existingContent ?? "";
|
|
296
|
+
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
297
|
+
const added = [];
|
|
298
|
+
const alreadyPresent = [];
|
|
299
|
+
for (const line of linesToAdd) {
|
|
300
|
+
const trimmed = line.trim();
|
|
301
|
+
if (trimmed === "" || existingLines.has(trimmed)) {
|
|
302
|
+
if (trimmed !== "") alreadyPresent.push(line);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
added.push(line);
|
|
306
|
+
existingLines.add(trimmed);
|
|
307
|
+
}
|
|
308
|
+
if (added.length === 0) {
|
|
309
|
+
return { added, alreadyPresent, newContent: existing };
|
|
310
|
+
}
|
|
311
|
+
const sep = existing === "" ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
312
|
+
const newContent = existing + sep + added.join("\n") + "\n";
|
|
313
|
+
return { added, alreadyPresent, newContent };
|
|
314
|
+
}
|
|
315
|
+
__name(appendUniqueLines, "appendUniqueLines");
|
|
316
|
+
|
|
317
|
+
// src/runner/init.ts
|
|
318
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
319
|
+
var packageRoot = resolve(here, "..", "..");
|
|
320
|
+
function parseArgs(argv) {
|
|
321
|
+
return {
|
|
322
|
+
force: argv.includes("--force"),
|
|
323
|
+
allowNonMacos: argv.includes("--allow-non-macos")
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
__name(parseArgs, "parseArgs");
|
|
327
|
+
function symbol(severity) {
|
|
328
|
+
return severity === "ok" ? "\u2713" : severity === "warning" ? "\u26A0" : "\u2717";
|
|
329
|
+
}
|
|
330
|
+
__name(symbol, "symbol");
|
|
331
|
+
function ensureDir(p) {
|
|
332
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
__name(ensureDir, "ensureDir");
|
|
335
|
+
function writeIfNeeded(filePath, content, force) {
|
|
336
|
+
const exists = existsSync(filePath);
|
|
337
|
+
if (exists && !force) return "skipped";
|
|
338
|
+
ensureDir(dirname(filePath));
|
|
339
|
+
writeFileSync(filePath, content);
|
|
340
|
+
return exists ? "overwrote" : "created";
|
|
341
|
+
}
|
|
342
|
+
__name(writeIfNeeded, "writeIfNeeded");
|
|
343
|
+
function readPackageFile(relativePath) {
|
|
344
|
+
const abs = resolve(packageRoot, relativePath);
|
|
345
|
+
if (!existsSync(abs)) return null;
|
|
346
|
+
return readFileSync(abs, "utf8");
|
|
347
|
+
}
|
|
348
|
+
__name(readPackageFile, "readPackageFile");
|
|
349
|
+
function main() {
|
|
350
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
351
|
+
const target = process.cwd();
|
|
352
|
+
console.log("unotest-mobile init \u2014 bootstrapping project\n");
|
|
353
|
+
console.log("Environment:");
|
|
354
|
+
const checks = runEnvironmentChecks({ allowNonMacos: opts.allowNonMacos });
|
|
355
|
+
let hardFailed = false;
|
|
356
|
+
for (const c of checks) {
|
|
357
|
+
console.log(` ${symbol(c.severity)} ${c.name}: ${c.message}`);
|
|
358
|
+
if (c.detail) console.log(` ${c.detail}`);
|
|
359
|
+
if (c.severity === "error") hardFailed = true;
|
|
360
|
+
}
|
|
361
|
+
if (hardFailed) {
|
|
362
|
+
console.error("\nFix the errors above and re-run.");
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
console.log("\nFiles:");
|
|
366
|
+
const summary = [];
|
|
367
|
+
ensureDir(join(target, "unotest/e2e/_helpers"));
|
|
368
|
+
summary.push({
|
|
369
|
+
path: "unotest/e2e/smoke-welcome.js",
|
|
370
|
+
status: writeIfNeeded(join(target, "unotest/e2e/smoke-welcome.js"), templates.smokeWelcome, opts.force)
|
|
371
|
+
});
|
|
372
|
+
summary.push({
|
|
373
|
+
path: "unotest/e2e/_template.js",
|
|
374
|
+
status: writeIfNeeded(join(target, "unotest/e2e/_template.js"), templates.scenarioTemplate, opts.force)
|
|
375
|
+
});
|
|
376
|
+
summary.push({
|
|
377
|
+
path: "unotest/AGENTS.md",
|
|
378
|
+
status: writeIfNeeded(join(target, "unotest/AGENTS.md"), templates.agentsMd, opts.force)
|
|
379
|
+
});
|
|
380
|
+
const skillSrc = readPackageFile(".claude/skills/write-e2e-test.md");
|
|
381
|
+
if (skillSrc !== null) {
|
|
382
|
+
summary.push({
|
|
383
|
+
path: ".claude/skills/write-e2e-test.md",
|
|
384
|
+
status: writeIfNeeded(join(target, ".claude/skills/write-e2e-test.md"), skillSrc, opts.force)
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
summary.push({ path: ".claude/skills/write-e2e-test.md", status: "missing-in-package" });
|
|
388
|
+
}
|
|
389
|
+
const mcpPath = join(target, ".mcp.json");
|
|
390
|
+
try {
|
|
391
|
+
const existing = existsSync(mcpPath) ? readFileSync(mcpPath, "utf8") : null;
|
|
392
|
+
const merge = mergeMcpConfig(
|
|
393
|
+
existing,
|
|
394
|
+
"unotest-mobile",
|
|
395
|
+
{
|
|
396
|
+
command: templates.mcpServerEntry.command,
|
|
397
|
+
args: [...templates.mcpServerEntry.args]
|
|
398
|
+
},
|
|
399
|
+
{ force: opts.force }
|
|
400
|
+
);
|
|
401
|
+
if (merge.action !== "already-present") {
|
|
402
|
+
writeFileSync(mcpPath, merge.newContent);
|
|
403
|
+
}
|
|
404
|
+
summary.push({ path: ".mcp.json", status: merge.action });
|
|
405
|
+
} catch (e) {
|
|
406
|
+
if (e instanceof McpJsonParseError) {
|
|
407
|
+
console.error(`
|
|
408
|
+
\u2717 .mcp.json: ${e.message}`);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
throw e;
|
|
412
|
+
}
|
|
413
|
+
const envExamplePath = join(target, "unotest/.env.example");
|
|
414
|
+
summary.push({
|
|
415
|
+
path: "unotest/.env.example",
|
|
416
|
+
status: writeIfNeeded(envExamplePath, templates.envExample, true)
|
|
417
|
+
});
|
|
418
|
+
const envDst = join(target, "unotest/.env");
|
|
419
|
+
if (!existsSync(envDst)) {
|
|
420
|
+
writeFileSync(envDst, templates.envExample);
|
|
421
|
+
summary.push({ path: "unotest/.env", status: "created" });
|
|
422
|
+
} else {
|
|
423
|
+
summary.push({ path: "unotest/.env", status: "skipped (exists \u2014 fill in manually)" });
|
|
424
|
+
}
|
|
425
|
+
const gitignorePath = join(target, ".gitignore");
|
|
426
|
+
const gitignoreExisting = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : null;
|
|
427
|
+
const giUpdate = appendUniqueLines(gitignoreExisting, [...templates.gitignoreLines]);
|
|
428
|
+
if (giUpdate.added.length > 0) {
|
|
429
|
+
writeFileSync(gitignorePath, giUpdate.newContent);
|
|
430
|
+
summary.push({ path: ".gitignore", status: `added ${giUpdate.added.length} line(s)` });
|
|
431
|
+
} else {
|
|
432
|
+
summary.push({ path: ".gitignore", status: "already up-to-date" });
|
|
433
|
+
}
|
|
434
|
+
for (const item of summary) {
|
|
435
|
+
const symbolForStatus = item.status === "created" || item.status.startsWith("added") ? "\u2713" : item.status === "skipped" || item.status.startsWith("already") ? "\xB7" : item.status === "overwrote" || item.status === "force-overwrote" ? "\u21BB" : "?";
|
|
436
|
+
console.log(` ${symbolForStatus} ${item.path} \u2014 ${item.status}`);
|
|
437
|
+
}
|
|
438
|
+
console.log(`
|
|
439
|
+
Next:
|
|
440
|
+
1. Edit ${relative(target, envDst) || "unotest/.env"} with your project's values
|
|
441
|
+
(DATABASE_URL, SIM_A_NAME, APP_BUNDLE_ID, ...)
|
|
442
|
+
2. Boot iOS simulators matching SIM_A_NAME / SIM_B_NAME (Xcode \u2192 Devices)
|
|
443
|
+
3. Open Claude Code in this directory \u2014 MCP server auto-registers via
|
|
444
|
+
.mcp.json. Ask the agent to write a test.
|
|
445
|
+
4. CLI: \`npx @unotest/mobile e2e smoke-welcome\` to run the starter.
|
|
446
|
+
|
|
447
|
+
Re-check environment anytime: \`npx @unotest/mobile doctor\`
|
|
448
|
+
`);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
__name(main, "main");
|
|
452
|
+
process.exit(main());
|
|
453
|
+
//# sourceMappingURL=init.js.map
|