@unotest/web 0.5.0 → 0.6.2
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/hooks/log-agent.sh +24 -0
- package/.claude/skills/{write-e2e-test.md → write-e2e-test/SKILL.md} +25 -18
- package/CHANGELOG.md +69 -0
- package/README.md +59 -84
- package/bin/unotest-web.js +10 -32
- package/dist/config/schema.d.ts +9 -9
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +25 -20
- package/dist/dsl/index.js +1 -1
- package/dist/linter-DzAzJpab.d.ts +21 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +10 -1
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init.js +1 -1
- package/dist/runner/install-chromium.js +1 -1
- package/dist/runner/prepare-fix.js +1 -1
- package/dist/runner/serve-fixture.js +1 -1
- package/dist/runner/web-runner-adapter.js +1 -1
- package/guides/agent-integration.md +297 -0
- package/guides/dsl-reference.md +670 -0
- package/package.json +30 -39
- package/src/mcp/prompts/agent-test-author.md +71 -1
package/package.json
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "AI-native E2E testing for web applications. MCP server (run_test / step / resume / inspect_runtime / agent_fix) + CLI runner + JavaScript DSL scenarios on a sandboxed AST engine + semantic DOM snapshots.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"author": "Ivan Volkov <ivan@volkov.io>",
|
|
10
|
-
"
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/unotest/web.git"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/unotest/web#readme",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/unotest/web/issues"
|
|
17
|
-
},
|
|
10
|
+
"homepage": "https://www.npmjs.com/package/@unotest/web",
|
|
18
11
|
"type": "module",
|
|
19
|
-
"packageManager": "pnpm@10.12.4",
|
|
20
12
|
"engines": {
|
|
21
13
|
"node": ">=20"
|
|
22
14
|
},
|
|
@@ -51,20 +43,41 @@
|
|
|
51
43
|
"files": [
|
|
52
44
|
"bin",
|
|
53
45
|
"dist",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
46
|
+
"guides",
|
|
47
|
+
"examples/fixtures/break-fix-app/index.html",
|
|
48
|
+
"examples/fixtures/break-fix-app/README.md",
|
|
49
|
+
"examples/fixtures/break-fix-app/serve.mjs",
|
|
56
50
|
"src/mcp/prompts",
|
|
57
51
|
".claude/skills",
|
|
52
|
+
".claude/hooks",
|
|
58
53
|
"CHANGELOG.md",
|
|
59
54
|
"LICENSE",
|
|
60
55
|
"README.md"
|
|
61
56
|
],
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
59
|
+
"chokidar": "^4.0.3",
|
|
60
|
+
"dotenv": "^16.4.5",
|
|
61
|
+
"playwright": "^1.49.0",
|
|
62
|
+
"tsx": "^4.19.2",
|
|
63
|
+
"yaml": "^2.6.0",
|
|
64
|
+
"zod": "^3.23.8",
|
|
65
|
+
"@unotest/dsl": "^0.6.2",
|
|
66
|
+
"@unotest/protocol": "^0.6.2",
|
|
67
|
+
"@unotest/viewer": "^0.6.2"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^22.10.0",
|
|
71
|
+
"javascript-obfuscator": "^4.1.1",
|
|
72
|
+
"tsup": "^8.5.1",
|
|
73
|
+
"typescript": "^5.7.2"
|
|
74
|
+
},
|
|
62
75
|
"scripts": {
|
|
63
76
|
"typecheck": "tsc --noEmit",
|
|
64
77
|
"design": "npx serve -l 3111 design",
|
|
65
78
|
"test": "node --import tsx --test --test-reporter=spec 'src/**/*.test.ts' 'scripts/**/*.test.mjs' 'evals/**/*.test.ts'",
|
|
66
79
|
"compile": "tsup",
|
|
67
|
-
"compile:all": "pnpm -C ../unotest-dsl compile && pnpm -C ../unotest-
|
|
80
|
+
"compile:all": "pnpm -C ../unotest-dsl compile && pnpm -C ../unotest-web compile && pnpm -C ../unotest-viewer compile",
|
|
68
81
|
"lint:scenarios": "tsx src/runner/cli.ts lint",
|
|
69
82
|
"smoke:bundle": "node scripts/smoke-bundle.mjs",
|
|
70
83
|
"check:docs-sync": "node scripts/check-docs-sync.mjs",
|
|
@@ -74,6 +87,7 @@
|
|
|
74
87
|
"check:skill-prompt-sync": "node scripts/check-skill-prompt-sync.mjs",
|
|
75
88
|
"check:tarball": "node scripts/check-tarball.mjs",
|
|
76
89
|
"check:sibling-versions": "node scripts/check-sibling-versions.mjs",
|
|
90
|
+
"build:guides": "node scripts/build-guides.mjs",
|
|
77
91
|
"prepublish:check": "node scripts/prepublish-check.mjs",
|
|
78
92
|
"evals:run": "tsx evals/run.ts",
|
|
79
93
|
"evals:prepare": "tsx evals/prepare.ts",
|
|
@@ -83,33 +97,10 @@
|
|
|
83
97
|
"perf:baseline": "node scripts/perf-baseline.mjs",
|
|
84
98
|
"verify": "pnpm typecheck && pnpm lint:scenarios && pnpm check:surface && pnpm test",
|
|
85
99
|
"obfuscate": "node scripts/obfuscate.mjs",
|
|
86
|
-
"build": "pnpm verify && pnpm compile && pnpm obfuscate && pnpm smoke:bundle && pnpm check:tarball",
|
|
100
|
+
"build": "pnpm verify && pnpm compile && pnpm obfuscate && pnpm smoke:bundle && pnpm build:guides && pnpm check:tarball",
|
|
87
101
|
"e2e": "tsx src/runner/cli.ts e2e",
|
|
88
102
|
"m1:cross-browser": "node scripts/m1-cross-browser.mjs",
|
|
89
103
|
"mcp": "tsx src/mcp/server.ts",
|
|
90
|
-
"serve-fixture": "node examples/fixtures/break-fix-app/serve.mjs"
|
|
91
|
-
"prepublishOnly": "pnpm build"
|
|
92
|
-
},
|
|
93
|
-
"dependencies": {
|
|
94
|
-
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
95
|
-
"@unotest/dsl": "^0.5.0",
|
|
96
|
-
"@unotest/protocol": "^0.5.0",
|
|
97
|
-
"@unotest/viewer": "^0.5.0",
|
|
98
|
-
"dotenv": "^16.4.5",
|
|
99
|
-
"playwright": "^1.49.0",
|
|
100
|
-
"tsx": "^4.19.2",
|
|
101
|
-
"yaml": "^2.6.0",
|
|
102
|
-
"zod": "^3.23.8"
|
|
103
|
-
},
|
|
104
|
-
"pnpm": {
|
|
105
|
-
"onlyBuiltDependencies": [
|
|
106
|
-
"esbuild"
|
|
107
|
-
]
|
|
108
|
-
},
|
|
109
|
-
"devDependencies": {
|
|
110
|
-
"@types/node": "^22.10.0",
|
|
111
|
-
"javascript-obfuscator": "^4.1.1",
|
|
112
|
-
"tsup": "^8.5.1",
|
|
113
|
-
"typescript": "^5.7.2"
|
|
104
|
+
"serve-fixture": "node examples/fixtures/break-fix-app/serve.mjs"
|
|
114
105
|
}
|
|
115
|
-
}
|
|
106
|
+
}
|
|
@@ -19,7 +19,7 @@ Anthropic-, OpenAI-, or Google-specific prompt syntax.
|
|
|
19
19
|
You are writing and debugging end-to-end tests for a web application,
|
|
20
20
|
using the `@unotest/web` MCP tools. Your job is to:
|
|
21
21
|
|
|
22
|
-
1. Author `.js` scenarios under `unotest/e2e
|
|
22
|
+
1. Author `.js` scenarios under `unotest/e2e/<feature>/` that follow this
|
|
23
23
|
project's DSL — Playwright-vocabulary primitives (`goto`, `click`,
|
|
24
24
|
`fill`, `getByRole`, `assertText`, …).
|
|
25
25
|
2. Run them through the debugger tools (`run_test`, `step`, `resume`,
|
|
@@ -35,6 +35,20 @@ is at
|
|
|
35
35
|
[dsl-reference.md](https://github.com/unotest/web/blob/main/docs/dsl-reference.md).
|
|
36
36
|
Read that before writing more than a one-line scenario.
|
|
37
37
|
|
|
38
|
+
## Layout — scenarios live in feature subfolders (hard rule)
|
|
39
|
+
|
|
40
|
+
Never place a scenario directly in `unotest/e2e/`. Every test file goes in a
|
|
41
|
+
feature/page subfolder that mirrors the site structure:
|
|
42
|
+
|
|
43
|
+
- `unotest/e2e/auth/login.js`
|
|
44
|
+
- `unotest/e2e/checkout/apply-coupon.js`
|
|
45
|
+
- `unotest/e2e/catalog/open-first-car-card.js`
|
|
46
|
+
|
|
47
|
+
The linter rejects a test in the e2e root (`lint:scenario-in-root`), and
|
|
48
|
+
`save_exploration_as_test` requires a `<feature>/<name>` path. Pick the folder
|
|
49
|
+
from the area of the app under test. Shared helpers live in
|
|
50
|
+
`unotest/e2e/_helpers/`; `_template.js` is a starting point to copy.
|
|
51
|
+
|
|
38
52
|
## Locator hierarchy (D-22, hard rule)
|
|
39
53
|
|
|
40
54
|
When choosing a selector, prefer in this exact order:
|
|
@@ -169,6 +183,62 @@ If a primitive throws with "sandbox.X is not configured", tell the
|
|
|
169
183
|
user to add it to `unotest.config.*`. Do not infer secrets from the
|
|
170
184
|
environment yourself.
|
|
171
185
|
|
|
186
|
+
## External variables & secrets
|
|
187
|
+
|
|
188
|
+
Don't hard-code logins, passwords, base URLs, or tokens in a scenario.
|
|
189
|
+
Reference them by a bare **UPPER_SNAKE** identifier — the runtime resolves it
|
|
190
|
+
from the project's variable store:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
goto(APP_BASE_URL);
|
|
194
|
+
fill(getByLabel('Email'), TEST_USER_EMAIL);
|
|
195
|
+
fill(getByLabel('Password'), TEST_USER_PASSWORD);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Non-secret values live in `unotest/.env`; secret values in
|
|
199
|
+
`unotest/.secrets` (both gitignored). No `env()` / `secret()` wrapper and no
|
|
200
|
+
`${...}` — just the bare name.
|
|
201
|
+
- A lowercase / mixed-case identifier is an in-scenario binding (a variable you
|
|
202
|
+
assigned earlier), not an external one.
|
|
203
|
+
- An UPPER_SNAKE name that isn't in either file fails the run with
|
|
204
|
+
"external variable X not found — add it to unotest/.env". When you need a
|
|
205
|
+
value the project hasn't defined, tell the user to add it; don't inline a
|
|
206
|
+
literal.
|
|
207
|
+
- Secret values are auto-masked (`‹secret:NAME›`) in the debug log, so a
|
|
208
|
+
password passed to `fill` never lands on disk verbatim.
|
|
209
|
+
|
|
210
|
+
**While recording (`explore_*`):** `explore_start` returns
|
|
211
|
+
`availableVariables` (the project's declared names). When a step's value is one
|
|
212
|
+
of them, pass it as **`{{NAME}}`** — `explore_step({action:"fill", …,
|
|
213
|
+
value:"{{PASSWORD}}"})`. (A bare `NAME` also works when it's a declared
|
|
214
|
+
variable, but `{{NAME}}` is unambiguous in a plain string value, so prefer
|
|
215
|
+
it.) The runner substitutes the real value LIVE (so the login actually works)
|
|
216
|
+
and records a bare reference, so the saved test reads `fill(loc, PASSWORD)` and
|
|
217
|
+
the secret never enters the file or logs. The same applies to any
|
|
218
|
+
value/locator-text that is a variable (`goto` URL via `{{APP_BASE_URL}}`, a row
|
|
219
|
+
found by `getByText({{PARK}})`). If you pass a name that isn't declared, the
|
|
220
|
+
step returns a `variableHint` — define it in `.env`/`.secrets` or use a literal.
|
|
221
|
+
|
|
222
|
+
## Reusable flows (auth/setup you record once)
|
|
223
|
+
|
|
224
|
+
A login (or any setup block) that many tests share should be a reusable
|
|
225
|
+
`flow_<name>()` helper, not re-recorded per test. `explore_start` returns
|
|
226
|
+
`availableFlows` — the flows already on disk.
|
|
227
|
+
|
|
228
|
+
- **Flow already exists** (it's in `availableFlows`): call
|
|
229
|
+
`explore_run_flow({ explorationId, name })`. It replays the flow LIVE (you end
|
|
230
|
+
up logged in) and records a single `flow_<name>()` call — don't re-record the
|
|
231
|
+
steps.
|
|
232
|
+
- **First time** (no flow yet): record the block normally but put
|
|
233
|
+
`flow:"signin"` on every step of it (`explore_step({…, flow:"signin"})`).
|
|
234
|
+
On `save_exploration_as_test`, those steps are extracted into
|
|
235
|
+
`unotest/e2e/_helpers/signin.js` as `flow_signin()`, and the test calls
|
|
236
|
+
`flow_signin()` in their place. One save writes both files.
|
|
237
|
+
|
|
238
|
+
Use the SAME flow name on every step of the block. Variables inside a flow
|
|
239
|
+
(`{{LOGIN}}` etc.) resolve at replay/run time, so the secret never lives in the
|
|
240
|
+
flow file either.
|
|
241
|
+
|
|
172
242
|
## When a locator matches `N elements`
|
|
173
243
|
|
|
174
244
|
Every locator must resolve to exactly one element. When you see
|