@unotest/mobile 0.8.3 → 0.9.1
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 +23 -14
- package/CHANGELOG.md +25 -0
- package/README.md +4 -3
- package/bin/{mcp.js → unotest-mobile.js} +9 -0
- package/dist/dsl/mobile-dsl-language-service.d.ts +12 -0
- package/dist/dsl/mobile-dsl-language-service.js +1056 -0
- package/dist/mcp/server.js +1095 -2886
- package/dist/runner/cli.js +4247 -4991
- package/dist/runner/doctor.js +4 -3
- package/dist/runner/init.js +67 -73
- package/dist/runner/install.js +103 -89
- package/dist/runner/mobile-runner-adapter.d.ts +6 -0
- package/dist/runner/mobile-runner-adapter.js +227 -0
- package/package.json +39 -33
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/runner/mobile-runner-adapter.ts
|
|
5
|
+
import {
|
|
6
|
+
UnsupportedRunKindError
|
|
7
|
+
} from "@unotest/protocol";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
|
|
12
|
+
// src/runner/init/environment-check.ts
|
|
13
|
+
import { execFileSync } from "child_process";
|
|
14
|
+
var ExecError = class extends Error {
|
|
15
|
+
constructor(cmd, args) {
|
|
16
|
+
super(`${cmd} ${args.join(" ")} failed`);
|
|
17
|
+
this.cmd = cmd;
|
|
18
|
+
this.args = args;
|
|
19
|
+
}
|
|
20
|
+
cmd;
|
|
21
|
+
args;
|
|
22
|
+
static {
|
|
23
|
+
__name(this, "ExecError");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function defaultExec(cmd, args, cwd) {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], cwd });
|
|
29
|
+
} catch {
|
|
30
|
+
throw new ExecError(cmd, args);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
__name(defaultExec, "defaultExec");
|
|
34
|
+
function runEnvironmentChecks(opts = {}) {
|
|
35
|
+
const results = [];
|
|
36
|
+
const platform = opts.platform ?? process.platform;
|
|
37
|
+
const nodeVersion = opts.nodeVersion ?? process.versions.node;
|
|
38
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
39
|
+
const exec = opts.exec ?? ((cmd, args) => defaultExec(cmd, args, cwd));
|
|
40
|
+
if (platform !== "darwin") {
|
|
41
|
+
results.push({
|
|
42
|
+
name: "platform",
|
|
43
|
+
severity: opts.allowNonMacos ? "warning" : "error",
|
|
44
|
+
message: `macOS required (got "${platform}"). iOS simulators only run on macOS (Apple licensing).`,
|
|
45
|
+
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)."
|
|
46
|
+
});
|
|
47
|
+
if (!opts.allowNonMacos) return results;
|
|
48
|
+
} else {
|
|
49
|
+
results.push({ name: "platform", severity: "ok", message: "macOS detected" });
|
|
50
|
+
}
|
|
51
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
52
|
+
if (major < 20) {
|
|
53
|
+
results.push({
|
|
54
|
+
name: "node",
|
|
55
|
+
severity: "error",
|
|
56
|
+
message: `Node 20+ required (got ${nodeVersion}).`,
|
|
57
|
+
detail: "Upgrade via nvm/fnm/volta."
|
|
58
|
+
});
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
results.push({ name: "node", severity: "ok", message: `Node ${nodeVersion}` });
|
|
62
|
+
if (platform !== "darwin") return results;
|
|
63
|
+
try {
|
|
64
|
+
const xcodePath = exec("xcode-select", ["-p"]).trim();
|
|
65
|
+
results.push({ name: "xcode-cli", severity: "ok", message: `Xcode CLI tools at ${xcodePath}` });
|
|
66
|
+
} catch {
|
|
67
|
+
results.push({
|
|
68
|
+
name: "xcode-cli",
|
|
69
|
+
severity: "error",
|
|
70
|
+
message: "Xcode Command Line Tools not found.",
|
|
71
|
+
detail: "Install: `xcode-select --install`"
|
|
72
|
+
});
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
exec("xcrun", ["simctl", "help"]);
|
|
77
|
+
results.push({ name: "simctl", severity: "ok", message: "xcrun simctl available" });
|
|
78
|
+
} catch {
|
|
79
|
+
results.push({
|
|
80
|
+
name: "simctl",
|
|
81
|
+
severity: "error",
|
|
82
|
+
message: "xcrun simctl not available.",
|
|
83
|
+
detail: "Install full Xcode (from Mac App Store), not just Command Line Tools."
|
|
84
|
+
});
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const json = exec("xcrun", ["simctl", "list", "devices", "available", "--json"]);
|
|
89
|
+
const parsed = JSON.parse(json);
|
|
90
|
+
const total = Object.values(parsed.devices).reduce((sum, list) => sum + list.length, 0);
|
|
91
|
+
if (total === 0) {
|
|
92
|
+
results.push({
|
|
93
|
+
name: "simulators",
|
|
94
|
+
severity: "warning",
|
|
95
|
+
message: "No iOS simulators found.",
|
|
96
|
+
detail: "Create one in Xcode \u2192 Window \u2192 Devices and Simulators \u2192 '+'."
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
results.push({ name: "simulators", severity: "ok", message: `${total} simulators available` });
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
results.push({
|
|
103
|
+
name: "simulators",
|
|
104
|
+
severity: "warning",
|
|
105
|
+
message: "Could not enumerate simulators."
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const pkgRaw = exec("cat", ["package.json"]);
|
|
110
|
+
const pkg = JSON.parse(pkgRaw);
|
|
111
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
112
|
+
const isRn = "react-native" in deps || "expo" in deps;
|
|
113
|
+
if (isRn) {
|
|
114
|
+
results.push({ name: "project-type", severity: "ok", message: "React Native / Expo detected" });
|
|
115
|
+
} else {
|
|
116
|
+
results.push({
|
|
117
|
+
name: "project-type",
|
|
118
|
+
severity: "ok",
|
|
119
|
+
message: "no React Native / Expo dependency in package.json",
|
|
120
|
+
detail: "Assuming a native iOS app (Swift/SwiftUI/Obj-C) or a non-Node project. Selectors resolve via the iOS accessibility tree \u2014 set accessibilityIdentifier on the views you want to target."
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
results.push({
|
|
125
|
+
name: "project-type",
|
|
126
|
+
severity: "ok",
|
|
127
|
+
message: "no package.json in current directory",
|
|
128
|
+
detail: "That's fine for native iOS projects. If this is a Node-based project, run from its root so unotest-mobile can detect React Native / Expo."
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
__name(runEnvironmentChecks, "runEnvironmentChecks");
|
|
134
|
+
|
|
135
|
+
// src/runner/scenarios-dir.ts
|
|
136
|
+
import { runsDirFor, testDirFor, UNOTEST_DIR } from "@unotest/protocol";
|
|
137
|
+
var MOBILE_SUFFIX = "-mobile";
|
|
138
|
+
var MOBILE_TEST_DIR = testDirFor(MOBILE_SUFFIX);
|
|
139
|
+
var MOBILE_SCENARIOS_DIR = `${UNOTEST_DIR}/${MOBILE_TEST_DIR}`;
|
|
140
|
+
var MOBILE_RUNS_DIR = `${UNOTEST_DIR}/${runsDirFor(MOBILE_SUFFIX)}`;
|
|
141
|
+
|
|
142
|
+
// src/runner/mobile-runner-adapter.ts
|
|
143
|
+
var PACKAGE_NAME = "@unotest/mobile";
|
|
144
|
+
var MobileRunnerAdapter = class {
|
|
145
|
+
static {
|
|
146
|
+
__name(this, "MobileRunnerAdapter");
|
|
147
|
+
}
|
|
148
|
+
name = PACKAGE_NAME;
|
|
149
|
+
suffix = MOBILE_SUFFIX;
|
|
150
|
+
/** Mobile drives a device via WDA — no clickable DOM, so the in-page locator
|
|
151
|
+
* picker doesn't apply (hidden). But the device IS shared: the agent can
|
|
152
|
+
* ATTACH and co-drive (wheel), so `attach` is true. A failure-pause is
|
|
153
|
+
* resumable as a RETRY (D-17 `resumeRetriesFailedStep`): the user fixes the
|
|
154
|
+
* device state, Continue re-executes the failed step. */
|
|
155
|
+
debug = { picker: false, attach: true, resumeFromFailure: true };
|
|
156
|
+
supportedKinds = ["scenario"];
|
|
157
|
+
/** iOS Simulator is Apple-licensed — this runner only executes on macOS.
|
|
158
|
+
* The viewer compares this against the host `process.platform` and blocks
|
|
159
|
+
* switching with `reason` on Windows/Linux (the runner package still
|
|
160
|
+
* installs everywhere; it just can't RUN off-Mac). */
|
|
161
|
+
host = {
|
|
162
|
+
platforms: ["darwin"],
|
|
163
|
+
reason: "iOS testing runs only on macOS \u2014 Xcode and the iOS Simulator are Apple-only."
|
|
164
|
+
};
|
|
165
|
+
resolveBin() {
|
|
166
|
+
const req = createRequire(import.meta.url);
|
|
167
|
+
const pkgPath = req.resolve(`${PACKAGE_NAME}/package.json`);
|
|
168
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
169
|
+
const binRel = pickBin(pkg);
|
|
170
|
+
if (!binRel) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${PACKAGE_NAME} package.json declares no 'bin' field \u2014 broken install?`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return join(dirname(pkgPath), binRel);
|
|
176
|
+
}
|
|
177
|
+
buildArgs(req) {
|
|
178
|
+
if (req.kind === "scenario") {
|
|
179
|
+
const args = ["e2e", req.scenario];
|
|
180
|
+
if (req.debug) args.push("--debug");
|
|
181
|
+
if (req.breakpoints && req.breakpoints.length > 0) {
|
|
182
|
+
args.push("--break", req.breakpoints.join(","));
|
|
183
|
+
}
|
|
184
|
+
return args;
|
|
185
|
+
}
|
|
186
|
+
throw new UnsupportedRunKindError(req.kind, this.name);
|
|
187
|
+
}
|
|
188
|
+
buildEnv(_req) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
/** Device holder (M-16): `unotest-mobile app-server` boots the
|
|
192
|
+
* SIM_POOL slots and installs APP_PATH, reusing the `install` CLI
|
|
193
|
+
* machinery — the viewer never touches simctl itself. */
|
|
194
|
+
appServerArgs() {
|
|
195
|
+
return ["app-server"];
|
|
196
|
+
}
|
|
197
|
+
/** App-path inputs for the viewer's App panel (M-17). iOS today; an
|
|
198
|
+
* Android phase appends an `.apk` spec here (M-18: platform is a RUN
|
|
199
|
+
* configuration, the test suite stays one). */
|
|
200
|
+
appServerPickers() {
|
|
201
|
+
return [
|
|
202
|
+
{ envKey: "APP_PATH", extensions: [".app"], label: "iOS app bundle (.app)" }
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
/** Environment preflight (platform → Node → Xcode CLI → simctl →
|
|
206
|
+
* simulators → project type) — the viewer's "Check" button calls this.
|
|
207
|
+
* Delegates to the SAME `runEnvironmentChecks` that `unotest-mobile
|
|
208
|
+
* doctor` / `init` use (one source of truth), scoped to the workspace
|
|
209
|
+
* root so the project-type hint reads the user's `package.json`. */
|
|
210
|
+
async doctor(ctx) {
|
|
211
|
+
return runEnvironmentChecks({ cwd: ctx.projectRoot });
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var mobileRunnerAdapter = new MobileRunnerAdapter();
|
|
215
|
+
var mobile_runner_adapter_default = mobileRunnerAdapter;
|
|
216
|
+
function pickBin(pkg) {
|
|
217
|
+
if (typeof pkg.bin === "string") return pkg.bin;
|
|
218
|
+
if (pkg.bin && typeof pkg.bin === "object") {
|
|
219
|
+
return pkg.bin["unotest-mobile"] ?? Object.values(pkg.bin)[0] ?? null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
__name(pickBin, "pickBin");
|
|
224
|
+
export {
|
|
225
|
+
mobile_runner_adapter_default as default,
|
|
226
|
+
mobileRunnerAdapter
|
|
227
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "AI-native E2E testing for iOS React Native apps. MCP server + CLI runner + JS-DSL scenarios.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"packageManager": "pnpm@10.12.4",
|
|
8
7
|
"engines": {
|
|
9
8
|
"node": ">=20"
|
|
10
9
|
},
|
|
11
10
|
"bin": {
|
|
12
|
-
"unotest-mobile": "./bin/
|
|
11
|
+
"unotest-mobile": "./bin/unotest-mobile.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./adapter": {
|
|
15
|
+
"types": "./dist/runner/mobile-runner-adapter.d.ts",
|
|
16
|
+
"import": "./dist/runner/mobile-runner-adapter.js"
|
|
17
|
+
},
|
|
18
|
+
"./dsl": {
|
|
19
|
+
"types": "./dist/dsl/mobile-dsl-language-service.d.ts",
|
|
20
|
+
"import": "./dist/dsl/mobile-dsl-language-service.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
13
23
|
},
|
|
14
24
|
"files": [
|
|
15
25
|
"bin",
|
|
@@ -19,27 +29,6 @@
|
|
|
19
29
|
"LICENSE",
|
|
20
30
|
"README.md"
|
|
21
31
|
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"typecheck": "tsc --noEmit",
|
|
24
|
-
"test": "node --import tsx --test --test-reporter=spec 'src/**/*.test.ts' 'evals/**/*.test.ts'",
|
|
25
|
-
"compile": "tsup",
|
|
26
|
-
"smoke:bundle": "node scripts/smoke-bundle.mjs",
|
|
27
|
-
"check:no-cyrillic": "node scripts/check-no-cyrillic.mjs",
|
|
28
|
-
"check:no-internal-refs": "node scripts/check-no-internal-refs.mjs",
|
|
29
|
-
"verify": "pnpm typecheck && pnpm lint:scenarios && pnpm test",
|
|
30
|
-
"build": "pnpm verify && pnpm compile && pnpm smoke:bundle && pnpm check:no-cyrillic && pnpm check:no-internal-refs",
|
|
31
|
-
"e2e": "tsx src/runner/cli.ts",
|
|
32
|
-
"lint:scenarios": "tsx src/runner/cli.ts lint",
|
|
33
|
-
"mcp": "tsx src/mcp/server.ts",
|
|
34
|
-
"evals:prepare": "tsx evals/prepare.ts",
|
|
35
|
-
"evals:check": "tsx evals/check.ts",
|
|
36
|
-
"evals:run": "tsx evals/run.ts",
|
|
37
|
-
"evals:diff": "tsx evals/diff.ts",
|
|
38
|
-
"evals:bless": "tsx evals/bless.ts",
|
|
39
|
-
"wda:warmup": "tsx scripts/wda-warmup.ts",
|
|
40
|
-
"prepublish:check": "node scripts/prepublish-check.mjs",
|
|
41
|
-
"prepublishOnly": "pnpm build"
|
|
42
|
-
},
|
|
43
32
|
"dependencies": {
|
|
44
33
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
45
34
|
"appium-webdriveragent": "^12.2.2",
|
|
@@ -47,7 +36,11 @@
|
|
|
47
36
|
"dotenv": "^16.4.5",
|
|
48
37
|
"sharp": "^0.34.5",
|
|
49
38
|
"zod": "^3.23.8",
|
|
50
|
-
"zod-to-json-schema": "^3.23.5"
|
|
39
|
+
"zod-to-json-schema": "^3.23.5",
|
|
40
|
+
"@unotest/core": "^0.9.1",
|
|
41
|
+
"@unotest/protocol": "^0.9.1",
|
|
42
|
+
"@unotest/dsl": "^0.9.1",
|
|
43
|
+
"@unotest/viewer": "^0.9.1"
|
|
51
44
|
},
|
|
52
45
|
"peerDependencies": {
|
|
53
46
|
"mysql2": "^3.0.0",
|
|
@@ -61,13 +54,6 @@
|
|
|
61
54
|
"optional": true
|
|
62
55
|
}
|
|
63
56
|
},
|
|
64
|
-
"pnpm": {
|
|
65
|
-
"onlyBuiltDependencies": [
|
|
66
|
-
"better-sqlite3",
|
|
67
|
-
"esbuild",
|
|
68
|
-
"sharp"
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
57
|
"devDependencies": {
|
|
72
58
|
"@types/better-sqlite3": "^7.6.13",
|
|
73
59
|
"@types/node": "^22.10.0",
|
|
@@ -78,5 +64,25 @@
|
|
|
78
64
|
"tsup": "^8.5.1",
|
|
79
65
|
"tsx": "^4.19.2",
|
|
80
66
|
"typescript": "^5.7.2"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "node --import tsx --test --test-reporter=spec 'src/**/*.test.ts' 'evals/**/*.test.ts'",
|
|
71
|
+
"compile": "tsup",
|
|
72
|
+
"smoke:bundle": "node scripts/smoke-bundle.mjs",
|
|
73
|
+
"check:no-cyrillic": "node scripts/check-no-cyrillic.mjs",
|
|
74
|
+
"check:no-internal-refs": "node scripts/check-no-internal-refs.mjs",
|
|
75
|
+
"verify": "pnpm typecheck && pnpm lint:scenarios && pnpm test",
|
|
76
|
+
"build": "pnpm verify && pnpm compile && pnpm smoke:bundle && pnpm check:no-cyrillic && pnpm check:no-internal-refs",
|
|
77
|
+
"e2e": "tsx src/runner/cli.ts",
|
|
78
|
+
"lint:scenarios": "tsx src/runner/cli.ts lint",
|
|
79
|
+
"mcp": "tsx src/mcp/server.ts",
|
|
80
|
+
"evals:prepare": "tsx evals/prepare.ts",
|
|
81
|
+
"evals:check": "tsx evals/check.ts",
|
|
82
|
+
"evals:run": "tsx evals/run.ts",
|
|
83
|
+
"evals:diff": "tsx evals/diff.ts",
|
|
84
|
+
"evals:bless": "tsx evals/bless.ts",
|
|
85
|
+
"wda:warmup": "tsx scripts/wda-warmup.ts",
|
|
86
|
+
"prepublish:check": "node scripts/prepublish-check.mjs"
|
|
81
87
|
}
|
|
82
|
-
}
|
|
88
|
+
}
|