@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @unotest/web — agent-action trace hook.
|
|
3
|
+
#
|
|
4
|
+
# Appends each Claude Code hook event (delivered as JSON on stdin) to a
|
|
5
|
+
# JSONL file so a debug session captures EVERYTHING the agent did: every
|
|
6
|
+
# tool call (name + input + output), prompts, session lifecycle, and which
|
|
7
|
+
# files it read (e.g. the write-e2e-test skill). Layer above UNOTEST_DEBUG's
|
|
8
|
+
# app-under-test artifacts — this one sees the agent's host-side actions
|
|
9
|
+
# that the MCP server never observes.
|
|
10
|
+
#
|
|
11
|
+
# Opt-in: no-op unless UNOTEST_DEBUG is set. Never blocks the agent — always
|
|
12
|
+
# exits 0 (only exit 2 would block). Reasoning/thinking is NOT capturable by
|
|
13
|
+
# hooks; this records tool I/O + prompts + lifecycle only.
|
|
14
|
+
#
|
|
15
|
+
# Arg $1: absolute path to the JSONL log file.
|
|
16
|
+
|
|
17
|
+
[ "$UNOTEST_DEBUG" = "1" ] || [ "$UNOTEST_DEBUG" = "full" ] || [ "$UNOTEST_DEBUG" = "summary" ] || exit 0
|
|
18
|
+
|
|
19
|
+
out="$1"
|
|
20
|
+
[ -n "$out" ] || exit 0
|
|
21
|
+
mkdir -p "$(dirname "$out")" 2>/dev/null
|
|
22
|
+
cat >> "$out"
|
|
23
|
+
printf '\n' >> "$out"
|
|
24
|
+
exit 0
|
|
@@ -6,8 +6,10 @@ description: Write a new E2E test for the web application under test using @unot
|
|
|
6
6
|
# Skill: write-e2e-test
|
|
7
7
|
|
|
8
8
|
You are writing an end-to-end test using `@unotest/web` against a web
|
|
9
|
-
app. The test lives in `unotest/e2e/<name>.js` in the project
|
|
10
|
-
test (NOT in the `@unotest/web` package itself).
|
|
9
|
+
app. The test lives in `unotest/e2e/<feature>/<name>.js` in the project
|
|
10
|
+
under test (NOT in the `@unotest/web` package itself). Scenarios are
|
|
11
|
+
grouped by feature/page in subfolders — never placed directly in the
|
|
12
|
+
`unotest/e2e/` root (the linter rejects that with `lint:scenario-in-root`).
|
|
11
13
|
|
|
12
14
|
**Default mode is verify-against-live-app.** A test that hasn't been
|
|
13
15
|
run is unverified — selectors may be invented, transitions may race,
|
|
@@ -53,7 +55,7 @@ Then move to Phase 1. Three files in, exploration done.
|
|
|
53
55
|
|
|
54
56
|
```
|
|
55
57
|
new_context { } → { ok, url: "about:blank" }
|
|
56
|
-
explore_start { scenario_name: "<
|
|
58
|
+
explore_start { scenario_name: "<feature>/<name>" } → { explorationId }
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
Keep the `explorationId` — every recorded step needs it.
|
|
@@ -81,30 +83,33 @@ For each page state you traverse:
|
|
|
81
83
|
1. **Discover** — `get_page_snapshot { }`. Default outline format:
|
|
82
84
|
|
|
83
85
|
```
|
|
84
|
-
|
|
86
|
+
regions:
|
|
85
87
|
navigation:
|
|
86
88
|
- link "Sets 21,414 items" [e3]
|
|
87
89
|
- link "Themes" [e4]
|
|
88
90
|
main:
|
|
89
91
|
- heading "Browse the catalog" [e5]
|
|
90
92
|
- button "Sign in" [e6]
|
|
91
|
-
|
|
92
|
-
bottom:
|
|
93
|
+
contentinfo (below):
|
|
93
94
|
- link "Help" [e21]
|
|
94
95
|
_meta:
|
|
96
|
+
totalActions: 7 totalRegions: 3
|
|
97
|
+
regions: 2 in, 0 above, 1 below, 0 partial
|
|
95
98
|
viewport: 1280x720
|
|
96
|
-
|
|
99
|
+
mode: outline
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
Line grammar: `-
|
|
100
|
-
is what you'll pass back — refs are **wire-only**, never
|
|
101
|
-
the saved test.
|
|
102
|
-
|
|
102
|
+
Line grammar: `- {role} {"name"} {#testId} {[clickable]} {[state]} [eN]`.
|
|
103
|
+
The `[eN]` ref is what you'll pass back — refs are **wire-only**, never
|
|
104
|
+
typed into the saved test. Top-level keys are landmark `regions`
|
|
105
|
+
(navigation / main / contentinfo / form / region / …) in reading order.
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
A region wholly or partly past the viewport carries an inline tag after
|
|
108
|
+
its heading — `(above)` / `(below)` / `(partial)`; a fully-in-view region
|
|
109
|
+
has none. There is NO separate off-screen block. To reach an action in a
|
|
110
|
+
`(below)` region, scroll first (`explore_step { action: "scroll_into_view",
|
|
111
|
+
locator: {kind:"locator", steps:[{kind:"ref", ref:"e21"}]} }`), then
|
|
112
|
+
re-snapshot.
|
|
108
113
|
|
|
109
114
|
2. **Act + record — pass the `[eN]` ref locator.**
|
|
110
115
|
|
|
@@ -196,10 +201,12 @@ Read the warnings before saving:
|
|
|
196
201
|
Then persist:
|
|
197
202
|
|
|
198
203
|
```
|
|
199
|
-
save_exploration_as_test { explorationId, scenarioName: "<name>" }
|
|
204
|
+
save_exploration_as_test { explorationId, scenarioName: "<feature>/<name>" }
|
|
200
205
|
```
|
|
201
206
|
|
|
202
|
-
Writes `unotest/e2e/<name>.js`.
|
|
207
|
+
Writes `unotest/e2e/<feature>/<name>.js`. `scenarioName` MUST include a
|
|
208
|
+
feature subfolder (e.g. `auth/login`) — a bare name is rejected. File
|
|
209
|
+
exists? Pass `overwrite: true`.
|
|
203
210
|
|
|
204
211
|
### Phase 5 — reset state, then run_test
|
|
205
212
|
|
|
@@ -513,7 +520,7 @@ npx @unotest/web lint
|
|
|
513
520
|
The linter runs two passes:
|
|
514
521
|
|
|
515
522
|
1. **Validator** (`validator:unknown-function`, `validator:dsl`) —
|
|
516
|
-
`error` severity
|
|
523
|
+
`error` severity (fails `npx @unotest/web lint`). Catches unknown function
|
|
517
524
|
names (`css`, typos), wrong arg kinds, malformed `for`/`if`
|
|
518
525
|
shapes. **If you see `validator:unknown-function`, you invented
|
|
519
526
|
a function name** — re-read the DSL cheat-sheet above and use the
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,75 @@ All notable changes to `@unotest/web` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.2] - 2026-06-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **MCP server `instructions`.** The server now ships start-of-session
|
|
13
|
+
`instructions` (injected by the MCP host into the agent's context the
|
|
14
|
+
moment the server is approved), pointing the agent at the
|
|
15
|
+
`write-e2e-test` skill and the canonical MCP tools before it
|
|
16
|
+
free-researches the repo.
|
|
17
|
+
- **`init` writes/merges Claude Code config.** `init` now copies the
|
|
18
|
+
`write-e2e-test` skill as a real skill **directory**
|
|
19
|
+
(`.claude/skills/write-e2e-test/SKILL.md`, so `/write-e2e-test` and
|
|
20
|
+
auto-invoke work) plus an agent-trace hook (`.claude/hooks/log-agent.sh`,
|
|
21
|
+
a no-op unless `UNOTEST_DEBUG` is set), and merges managed blocks into
|
|
22
|
+
the consumer's `CLAUDE.md` and `.claude/settings.json` (pre-approve MCP
|
|
23
|
+
tools, register the trace hook) without clobbering user content.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- CLI help uses `examples/01-smoke` as the `e2e` example (runnable example
|
|
28
|
+
scenarios live under `unotest/e2e/examples/`).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **External variables and secrets.** Scenarios reference external values
|
|
33
|
+
by bare `UPPER_SNAKE` identifiers (`APP_BASE_URL`, `TEST_USER_EMAIL`)
|
|
34
|
+
instead of hard-coding them. Non-secret values live in `unotest/.env`,
|
|
35
|
+
secrets in `unotest/.secrets` (gitignored); both are created by `init`
|
|
36
|
+
and loaded fresh on every run. Secret values are masked as
|
|
37
|
+
`‹secret:NAME›` in logs, debug output, and recorded artifacts, so they
|
|
38
|
+
never reach a saved file in plaintext.
|
|
39
|
+
- **Reusable flows.** Recorded exploration steps can be tagged with a
|
|
40
|
+
`flow` group and extracted on save into a `flow_<name>()` helper under
|
|
41
|
+
`unotest/e2e/_helpers/`; the generated test calls the helper instead of
|
|
42
|
+
inlining the steps. The new `explore_run_flow` MCP tool replays an
|
|
43
|
+
existing `flow_*` helper without re-recording it, and `explore_start`
|
|
44
|
+
now surfaces the available flows and the declared variable names.
|
|
45
|
+
- **Run source snapshot.** Each run writes a `sources.json` (the entry
|
|
46
|
+
scenario plus every loaded helper as they were when the run began), so a
|
|
47
|
+
run always renders against the code that actually executed even if the
|
|
48
|
+
file is edited afterward. (Consumes `@unotest/protocol`'s `RunSources`.)
|
|
49
|
+
- **New scenario linter rules:** `scenario-in-root` (scenarios must live in
|
|
50
|
+
a feature subfolder) and `mustache-in-dsl` (flags `{{NAME}}` template
|
|
51
|
+
syntax inside DSL string literals).
|
|
52
|
+
|
|
53
|
+
### Changed (BREAKING)
|
|
54
|
+
|
|
55
|
+
- **Scenarios must live in a feature subfolder.** A scenario placed
|
|
56
|
+
directly in `unotest/e2e/` is now rejected by the linter — use
|
|
57
|
+
`unotest/e2e/<feature>/<name>.js` (the `e2e/` root is reserved for
|
|
58
|
+
`_helpers/`). The MCP `scenarioName` argument (`explore_start` /
|
|
59
|
+
`save_exploration_as_test`) likewise requires `<feature>/<name>` form,
|
|
60
|
+
and `init` scaffolds the welcome smoke test at
|
|
61
|
+
`unotest/e2e/welcome/smoke.js`.
|
|
62
|
+
- **`{{NAME}}` inside a DSL string literal is now a lint error.** That form
|
|
63
|
+
is only for MCP recorded values; in scenario code, reference a variable
|
|
64
|
+
as a bare identifier (`fill(loc, PASSWORD)`).
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **Windows: path handling.** Run-artifact paths (entry, `sources.json`
|
|
69
|
+
keys, step `file` events) are normalized to project-relative posix, so
|
|
70
|
+
the viewer's asset allowlist and source mapping match on Windows — a
|
|
71
|
+
native `\` previously broke them.
|
|
72
|
+
- **Actionable error when no browser is installed.** Playwright launch now
|
|
73
|
+
preflights the browser binary and fails with a clear message hinting
|
|
74
|
+
`npx @unotest/web install-chromium`, instead of a raw stack trace; launch
|
|
75
|
+
failures are printed to the console instead of exiting silently.
|
|
76
|
+
|
|
8
77
|
## [0.5.0] - 2026-05-29
|
|
9
78
|
|
|
10
79
|
### Added
|
package/README.md
CHANGED
|
@@ -9,80 +9,80 @@ structured failure bundles.
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
## 1. Setup
|
|
13
|
-
|
|
14
|
-
Works in any project — Node, Django, Rails, Go, anything. No `package.json`
|
|
15
|
-
required; everything runs through `npx`.
|
|
12
|
+
## 1. Setup
|
|
16
13
|
|
|
17
14
|
```sh
|
|
18
|
-
npx @unotest/web@latest init
|
|
19
|
-
npx @unotest/web install-chromium # skip if you picked system Chrome during init
|
|
15
|
+
npx @unotest/web@latest init
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
Run once in your project — any stack (Node, Django, Rails, Go, …), no
|
|
19
|
+
`package.json` required. `init` writes the config and a starter scenario,
|
|
20
|
+
sets up the browser (bundled Chromium, or your system Chrome if you pick it
|
|
21
|
+
during the prompt), and wires the MCP config so Claude Code / Cursor / Codex
|
|
22
|
+
pick the server up automatically. Re-run anytime to update — it never
|
|
23
|
+
overwrites your edits.
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
it** so the MCP server never drifts. Most clients (Claude Code, Cursor, …)
|
|
28
|
-
pick it up automatically. The entry it writes looks like:
|
|
25
|
+
## 2. Open the viewer
|
|
29
26
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
"mcpServers": {
|
|
33
|
-
"unotest-web": { "command": "npx", "args": ["-y", "@unotest/web@0.5.0", "mcp"] }
|
|
34
|
-
}
|
|
35
|
-
}
|
|
27
|
+
```sh
|
|
28
|
+
npx @unotest/web viewer
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
## 3. Create a test — just ask
|
|
31
|
+
A local IDE-style UI for your tests — no cloud, no account. This is your
|
|
32
|
+
home base: browse scenarios, run them, and watch each step live. (Details
|
|
33
|
+
in §5.)
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
plain English:
|
|
35
|
+
## 3. Create a test — just ask your agent
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
In your AI editor (Claude Code, Codex, Cursor, …), open your project and
|
|
38
|
+
describe the flow in plain English:
|
|
48
39
|
|
|
49
|
-
|
|
40
|
+
> **"Open https://playground.unotest.com, go to the _Click vs Double-click_
|
|
41
|
+
> section, click the _Click me_ button, and check that a popup appears."**
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
3. **Runs it** (`run_test`) and, if it fails, **debugs itself** — reads
|
|
57
|
-
the paused browser state, the failure bundle, and console, then edits
|
|
58
|
-
and re-runs.
|
|
43
|
+
Through the MCP server the agent explores your live app — clicking,
|
|
44
|
+
filling, reading the real DOM via semantic snapshots — then writes a clean
|
|
45
|
+
scenario to `unotest/e2e/<name>.js` with stable selectors (`getByTestId` →
|
|
46
|
+
`getByRole` → … — never brittle CSS), runs it, and debugs itself if it
|
|
47
|
+
fails. You get a reviewable `.js` test: read it, tweak it, commit it.
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
> For best results, point the agent at its authoring guide, shipped with
|
|
50
|
+
> the package at `node_modules/@unotest/web/guides/agent-integration.md`.
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
> [agent-integration.md](https://github.com/unotest/web/blob/main/docs/manuals/agent-integration.md).
|
|
52
|
+
## 4. Run a test
|
|
65
53
|
|
|
66
|
-
|
|
54
|
+
Click any scenario in the viewer, or run from the CLI:
|
|
67
55
|
|
|
68
56
|
```sh
|
|
69
|
-
npx @unotest/web e2e <name>
|
|
70
|
-
npx @unotest/web e2e
|
|
57
|
+
npx @unotest/web e2e <name> # one scenario
|
|
58
|
+
npx @unotest/web e2e # list / usage
|
|
71
59
|
```
|
|
72
60
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
## 5. Watch it run — the viewer
|
|
62
|
+
|
|
63
|
+
`npx @unotest/web viewer` opens a local UI (no cloud, no account):
|
|
64
|
+
|
|
65
|
+
- **Scenario tree** — scenarios, collections, helpers, and run history.
|
|
66
|
+
- **Run from the UI** — results stream live over WebSocket as steps execute.
|
|
67
|
+
- **Block view** — each `//@collapse` group folds into a section with
|
|
68
|
+
per-step status; a failure pins an error card to the offending line.
|
|
69
|
+
- **Step debugger** — set gutter breakpoints, run paused, inspect the live
|
|
70
|
+
DOM at each stop.
|
|
71
|
+
- **Inspector** — screenshots and failure artifacts per run.
|
|
72
|
+
- **Integrated terminal** — a docked xterm (toggle `` ⌃` ``) to drive the
|
|
73
|
+
CLI without leaving the window.
|
|
74
|
+
|
|
75
|
+
### Writing by hand (optional)
|
|
76
|
+
|
|
77
|
+
Scenarios are plain `.js`. Every executable step sits inside a
|
|
78
|
+
`//@collapse("what this does")` block — plain-English intent the agent reads
|
|
79
|
+
back to repair a step when a selector drifts:
|
|
78
80
|
|
|
79
81
|
```js
|
|
80
82
|
function test_login() {
|
|
81
|
-
//@collapse("Log in as demo user")
|
|
83
|
+
//@collapse("Log in as the demo user")
|
|
82
84
|
goto('https://app.example.com');
|
|
83
|
-
click(getByRole('button', { name: /sign in/i }));
|
|
84
85
|
fill(getByLabel('Email'), 'demo@example.com');
|
|
85
|
-
fill(getByLabel('Password'), 'demo');
|
|
86
86
|
click(getByRole('button', { name: 'Continue' }));
|
|
87
87
|
//@endcollapse
|
|
88
88
|
|
|
@@ -92,32 +92,8 @@ function test_login() {
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
npx @unotest/web viewer
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
A local IDE-style UI for your test runs (no cloud, no account):
|
|
102
|
-
|
|
103
|
-
- **Scenario tree** — browse scenarios, collections, helpers, and run history.
|
|
104
|
-
- **Run from the UI** — launch any scenario or collection; results stream
|
|
105
|
-
live over WebSocket as steps execute.
|
|
106
|
-
- **Block view** — each `//@collapse` group rendered as a foldable section
|
|
107
|
-
with per-step status; failures pin an error card to the offending line.
|
|
108
|
-
- **Step debugger** — set breakpoints in the gutter, run paused, and
|
|
109
|
-
inspect the live DOM at each stop.
|
|
110
|
-
- **Inspector** — screenshots and failure artifacts per run.
|
|
111
|
-
- **Integrated terminal** — a docked xterm (toggle with `` ⌃` ``) to drive
|
|
112
|
-
the CLI without leaving the window.
|
|
113
|
-
- **Source ↔ block** — switch between the raw `.js` and the block view.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Writing by hand (optional)
|
|
118
|
-
|
|
119
|
-
Scenarios are plain `.js`. The DSL uses the familiar modern
|
|
120
|
-
browser-automation vocabulary, so it reads the way you already expect:
|
|
95
|
+
The DSL uses the familiar modern browser-automation vocabulary, so it reads
|
|
96
|
+
the way you already expect:
|
|
121
97
|
|
|
122
98
|
- **Navigation** — `goto`, `reload`, `waitForUrl`
|
|
123
99
|
- **Locators** — `getByRole`, `getByTestId`, `getByLabel`, `getByText`, `getByPlaceholder`, `locator`
|
|
@@ -125,21 +101,20 @@ browser-automation vocabulary, so it reads the way you already expect:
|
|
|
125
101
|
- **Assertions** — `assertText`, `assertUrl`, `assertVisible`, `assertChecked`, `expect`
|
|
126
102
|
- **Setup** — `dbQuery`, `dbExec`, `apiCall`, `shell`, `evaluate`
|
|
127
103
|
|
|
128
|
-
Full reference:
|
|
129
|
-
|
|
130
|
-
|
|
104
|
+
Full reference, shipped with the package:
|
|
105
|
+
`node_modules/@unotest/web/guides/dsl-reference.md` (plus operational guides
|
|
106
|
+
alongside it in `guides/`).
|
|
131
107
|
|
|
132
|
-
|
|
133
|
-
`npx @unotest/web install-chromium`.
|
|
108
|
+
---
|
|
134
109
|
|
|
135
110
|
## Ecosystem
|
|
136
111
|
|
|
137
112
|
| Package | Role |
|
|
138
113
|
|---|---|
|
|
139
114
|
| `@unotest/web` | this package — CLI / MCP server / runner |
|
|
140
|
-
| [`@unotest/viewer`](https://
|
|
141
|
-
| [`@unotest/protocol`](https://
|
|
142
|
-
| [`@unotest/dsl`](https://
|
|
115
|
+
| [`@unotest/viewer`](https://www.npmjs.com/package/@unotest/viewer) | local results browser |
|
|
116
|
+
| [`@unotest/protocol`](https://www.npmjs.com/package/@unotest/protocol) | shared types |
|
|
117
|
+
| [`@unotest/dsl`](https://www.npmjs.com/package/@unotest/dsl) | scenario parser + validator engine |
|
|
143
118
|
|
|
144
119
|
## License
|
|
145
120
|
|
package/bin/unotest-web.js
CHANGED
|
@@ -17,43 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
import { spawn } from "node:child_process";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
|
-
import { dirname, resolve
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
|
-
import { existsSync
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
23
|
|
|
24
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const root = resolve(here, "..");
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let raw;
|
|
36
|
-
try { raw = readFileSync(path, "utf8"); } catch { return; }
|
|
37
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
38
|
-
const trimmed = line.trim();
|
|
39
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
40
|
-
const eq = trimmed.indexOf("=");
|
|
41
|
-
if (eq <= 0) continue;
|
|
42
|
-
const key = trimmed.slice(0, eq).trim();
|
|
43
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
44
|
-
if (process.env[key] !== undefined) continue;
|
|
45
|
-
let value = trimmed.slice(eq + 1).trim();
|
|
46
|
-
if (value.length >= 2) {
|
|
47
|
-
const first = value[0];
|
|
48
|
-
const last = value[value.length - 1];
|
|
49
|
-
if ((first === '"' || first === "'") && first === last) {
|
|
50
|
-
value = value.slice(1, -1);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
process.env[key] = value;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
loadUnotestEnv();
|
|
27
|
+
// NOTE: `unotest/.env` is intentionally NOT loaded here. The dispatcher runs
|
|
28
|
+
// in every process — including the long-lived `viewer` launcher — and
|
|
29
|
+
// flattening `.env` into a launcher's process.env freezes a boot snapshot
|
|
30
|
+
// that spawned runs would inherit, so UI edits to `.env` wouldn't apply
|
|
31
|
+
// until the launcher restarted. Instead, `loadConfig()` flattens `.env` per
|
|
32
|
+
// run-process via `applyEnvFile`; the viewer never loads config, so each
|
|
33
|
+
// scenario run it spawns is a fresh process that reads the current file.
|
|
34
|
+
// See src/config/env-file.ts.
|
|
57
35
|
|
|
58
36
|
// Resolve tsx CLI binary from @unotest/web's own node_modules. tsx CLI
|
|
59
37
|
// (`tsx/cli` export → dist/cli.mjs) handles both legacy and modern Node
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -90,19 +90,19 @@ declare const FailureBundleConfigSchema: z.ZodObject<{
|
|
|
90
90
|
type FailureBundleConfig = z.infer<typeof FailureBundleConfigSchema>;
|
|
91
91
|
declare const DialogPolicySchema: z.ZodEnum<["accept", "dismiss", "manual"]>;
|
|
92
92
|
type DialogPolicy = z.infer<typeof DialogPolicySchema>;
|
|
93
|
-
declare const LinterRuleIdSchema: z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "validator:unknown-function", "validator:dsl"]>;
|
|
93
|
+
declare const LinterRuleIdSchema: z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "validator:unknown-function", "validator:dsl"]>;
|
|
94
94
|
type LinterRuleId = z.infer<typeof LinterRuleIdSchema>;
|
|
95
95
|
declare const LinterSeveritySchema: z.ZodEnum<["off", "warn", "error"]>;
|
|
96
96
|
type LinterSeverity = z.infer<typeof LinterSeveritySchema>;
|
|
97
97
|
declare const LinterConfigSchema: z.ZodObject<{
|
|
98
98
|
enabled: z.ZodBoolean;
|
|
99
|
-
rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "validator:unknown-function", "validator:dsl"]>, z.ZodEnum<["off", "warn", "error"]>>;
|
|
99
|
+
rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "validator:unknown-function", "validator:dsl"]>, z.ZodEnum<["off", "warn", "error"]>>;
|
|
100
100
|
}, "strip", z.ZodTypeAny, {
|
|
101
101
|
enabled: boolean;
|
|
102
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
102
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
103
103
|
}, {
|
|
104
104
|
enabled: boolean;
|
|
105
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
105
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
106
106
|
}>;
|
|
107
107
|
type LinterConfig = z.infer<typeof LinterConfigSchema>;
|
|
108
108
|
declare const McpConfigSchema: z.ZodObject<{
|
|
@@ -228,13 +228,13 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
228
228
|
defaultNavigationTimeoutMs: z.ZodNumber;
|
|
229
229
|
linter: z.ZodObject<{
|
|
230
230
|
enabled: z.ZodBoolean;
|
|
231
|
-
rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "validator:unknown-function", "validator:dsl"]>, z.ZodEnum<["off", "warn", "error"]>>;
|
|
231
|
+
rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "validator:unknown-function", "validator:dsl"]>, z.ZodEnum<["off", "warn", "error"]>>;
|
|
232
232
|
}, "strip", z.ZodTypeAny, {
|
|
233
233
|
enabled: boolean;
|
|
234
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
234
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
235
235
|
}, {
|
|
236
236
|
enabled: boolean;
|
|
237
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
237
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
238
238
|
}>;
|
|
239
239
|
mcp: z.ZodObject<{
|
|
240
240
|
transport: z.ZodLiteral<"stdio">;
|
|
@@ -292,7 +292,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
292
292
|
defaultNavigationTimeoutMs: number;
|
|
293
293
|
linter: {
|
|
294
294
|
enabled: boolean;
|
|
295
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
295
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
296
296
|
};
|
|
297
297
|
mcp: {
|
|
298
298
|
transport: "stdio";
|
|
@@ -335,7 +335,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
335
335
|
defaultNavigationTimeoutMs: number;
|
|
336
336
|
linter: {
|
|
337
337
|
enabled: boolean;
|
|
338
|
-
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
338
|
+
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "validator:unknown-function" | "validator:dsl", "off" | "warn" | "error">>;
|
|
339
339
|
};
|
|
340
340
|
mcp: {
|
|
341
341
|
transport: "stdio";
|
package/dist/config/schema.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
function _0x16c7(){var _0xb973b1=['Cg9ZAxrPDMu','CMvJB3jK','zw51Bq','C3rYAw5N','zxjYB3i','mtjMqLfpzNa','Aw50','BgL0zxjHBa','mZu4nK5xwLbkzq','mtuZmdm1yvLirMvm','mtuZA1vPqM9R','y2HYB21LlwjLDge','Dw5VDgvZDc9LmMuVx2HLBhbLCNm','B2zM','y2HYB21PDw0','zgvMyxvSDa','y3jHC2G','BNvSBa','y2HYB21L','BxnLzgDL','nZe5otCWwuL5r3HT','Dw5PB24','BNvTyMvY','D2vIA2L0','BwLU','B3b0Aw9UywW','DxjS','DMfSAwrHDg9YoNvUA25VD24TzNvUy3rPB24','mJu0nJbzBuHRqNe','D2fYBG','B2jQzwn0','zgLZBwLZCW','BgLUDdPZy2vUyxjPBY1PBI1YB290','BMv0D29YAW','mZaXC29WD2Tr','yM9VBgvHBG','BgLUDdPLDMfSDwf0zs1KAxnJB3vYywDLza','yxjYyxK','ywnJzxb0','DhjHBNnPzw50','Dw5VDgvZDc9LmMu','BwfUDwfS','mZCWodbWt3vmB1y','otC5u1f3zMH4','mJm1odGXmeXcDvPQDa','DMfSAwrHDg9YoMrZBa','BgLUDdPTDxn0ywnOzs1PBI1KC2W','otu2nhDrvKrmzW','zMLYzwzVEa'];_0x16c7=function(){return _0xb973b1;};return _0x16c7();}var _0x3893a2=_0x1f93;(function(_0x224983,_0x4ea4ba){var _0x4a0693={_0xb7d0c5:0xab,_0x5053dd:0xa8,_0x4ef3b5:0xc5,_0x33befc:0xcd},_0x80a53d=_0x1f93,_0x2a1e1b=_0x224983();while(!![]){try{var _0x5ceb66=parseInt(_0x80a53d(0xac))/0x1+-parseInt(_0x80a53d(_0x4a0693._0xb7d0c5))/0x2+parseInt(_0x80a53d(0xad))/0x3*(-parseInt(_0x80a53d(0xa1))/0x4)+parseInt(_0x80a53d(0xb7))/0x5*(parseInt(_0x80a53d(_0x4a0693._0x5053dd))/0x6)+-parseInt(_0x80a53d(_0x4a0693._0x4ef3b5))/0x7*(parseInt(_0x80a53d(_0x4a0693._0x33befc))/0x8)+parseInt(_0x80a53d(0xcf))/0x9+-parseInt(_0x80a53d(0xbf))/0xa*(parseInt(_0x80a53d(0xce))/0xb);if(_0x5ceb66===_0x4ea4ba)break;else _0x2a1e1b['push'](_0x2a1e1b['shift']());}catch(_0x5b69b7){_0x2a1e1b['push'](_0x2a1e1b['shift']());}}}(_0x16c7,0x25788));import{z}from'zod';function _0x1f93(_0x1ec1b0,_0x2c48d8){_0x1ec1b0=_0x1ec1b0-0x9f;var _0x16c79d=_0x16c7();var _0x1f9358=_0x16c79d[_0x1ec1b0];if(_0x1f93['iUYfIn']===undefined){var _0x1e2f01=function(_0x5ba337){var _0x379035='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x5ad0e0='',_0x477573='';for(var _0x2f68f7=0x0,_0x1cbf77,_0x5c6ba6,_0x10bb37=0x0;_0x5c6ba6=_0x5ba337['charAt'](_0x10bb37++);~_0x5c6ba6&&(_0x1cbf77=_0x2f68f7%0x4?_0x1cbf77*0x40+_0x5c6ba6:_0x5c6ba6,_0x2f68f7++%0x4)?_0x5ad0e0+=String['fromCharCode'](0xff&_0x1cbf77>>(-0x2*_0x2f68f7&0x6)):0x0){_0x5c6ba6=_0x379035['indexOf'](_0x5c6ba6);}for(var _0x5865de=0x0,_0x5b87a9=_0x5ad0e0['length'];_0x5865de<_0x5b87a9;_0x5865de++){_0x477573+='%'+('00'+_0x5ad0e0['charCodeAt'](_0x5865de)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x477573);};_0x1f93['UPRwyN']=_0x1e2f01,_0x1f93['fNATEj']={},_0x1f93['iUYfIn']=!![];}var _0xad7c19=_0x16c79d[0x0],_0x3792ff=_0x1ec1b0+_0xad7c19,_0x5080ef=_0x1f93['fNATEj'][_0x3792ff];return!_0x5080ef?(_0x1f9358=_0x1f93['UPRwyN'](_0x1f9358),_0x1f93['fNATEj'][_0x3792ff]=_0x1f9358):_0x1f9358=_0x5080ef,_0x1f9358;}var BrowserSchema=z[_0x3893a2(0xa5)](['chromium',_0x3893a2(0xa2),_0x3893a2(0xba)]),BrowserChannelSchema=z[_0x3893a2(0xb8)]([z['literal'](_0x3893a2(0xb5)),z[_0x3893a2(0xaa)](_0x3893a2(0xb6)),z[_0x3893a2(0xaa)](_0x3893a2(0xae)),z[_0x3893a2(0xb4)]()])[_0x3893a2(0xbc)](),ViewportSchema=z[_0x3893a2(0xc1)]({'width':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)](),'height':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)]()}),RetryReasonSchema=z[_0x3893a2(0xa5)]([_0x3893a2(0xca),_0x3893a2(0xc4),_0x3893a2(0xb3)]),RetryConfigSchema=z[_0x3893a2(0xc1)]({'count':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xbb)](0x0)['max'](0xa),'on':z['array'](RetryReasonSchema)}),FailureBundleConfigSchema=z[_0x3893a2(0xc1)]({'tier1':z['literal'](!![])[_0x3893a2(0xb2)](!![]),'tier2':z[_0x3893a2(0xc6)](),'tier3':z[_0x3893a2(0xc1)]({'network':z[_0x3893a2(0xc6)](),'video':z[_0x3893a2(0xc6)]()}),'retention':z[_0x3893a2(0xc1)]({'runs':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)](),'days':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)]()}),'storageDir':z[_0x3893a2(0xa6)]()['min'](0x1)}),DialogPolicySchema=z[_0x3893a2(0xa5)]([_0x3893a2(0xc9),_0x3893a2(0xc2),_0x3893a2(0xcc)]),LinterRuleIdSchema=z[_0x3893a2(0xa5)](['lint:deep-css','lint:xpath','lint:obfuscated-class','lint:pause-explicit','lint:disambig-by-index',_0x3893a2(0xc7),_0x3893a2(0xc3),_0x3893a2(0xa0),_0x3893a2(0xbe),_0x3893a2(0x9f)]),LinterSeveritySchema=z[_0x3893a2(0xa5)](['off',_0x3893a2(0xc0),'error']),LinterConfigSchema=z[_0x3893a2(0xc1)]({'enabled':z[_0x3893a2(0xc6)](),'rules':z[_0x3893a2(0xa4)](LinterRuleIdSchema,LinterSeveritySchema)}),McpConfigSchema=z['object']({'transport':z['literal']('stdio')}),SandboxConfigSchema=z[_0x3893a2(0xc1)]({'shellCwd':z[_0x3893a2(0xa6)]()['optional'](),'database':z[_0x3893a2(0xa6)]()['optional'](),'apiBaseUrl':z['string']()[_0x3893a2(0xbd)]()['optional']()}),UnotestConfigSchema=z['object']({'baseUrl':z[_0x3893a2(0xa6)]()[_0x3893a2(0xbd)]()[_0x3893a2(0xbc)](),'browsers':z[_0x3893a2(0xc8)](BrowserSchema)['min'](0x1),'channel':BrowserChannelSchema,'viewport':ViewportSchema,'retry':RetryConfigSchema,'failureBundle':FailureBundleConfigSchema,'dialogPolicy':DialogPolicySchema,'storageState':z[_0x3893a2(0xa6)]()[_0x3893a2(0xbc)](),'testDir':z[_0x3893a2(0xa6)]()['min'](0x1),'helpersDir':z[_0x3893a2(0xa6)]()[_0x3893a2(0xbb)](0x1),'defaultTimeoutMs':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)](),'defaultNavigationTimeoutMs':z[_0x3893a2(0xb9)]()[_0x3893a2(0xa9)]()[_0x3893a2(0xa3)](),'linter':LinterConfigSchema,'mcp':McpConfigSchema,'sandbox':SandboxConfigSchema}),DEFAULT_CONFIG={'browsers':[_0x3893a2(0xb1)],'viewport':{'width':0x500,'height':0x2d0},'retry':{'count':0x0,'on':['transient']},'failureBundle':{'tier1':!![],'tier2':!![],'tier3':{'network':![],'video':![]},'retention':{'runs':0x14,'days':0x7},'storageDir':'.unotest/failures'},'dialogPolicy':_0x3893a2(0xc9),'testDir':_0x3893a2(0xcb),'helpersDir':_0x3893a2(0xaf),'defaultTimeoutMs':0xbb8,'defaultNavigationTimeoutMs':0x3a98,'linter':{'enabled':!![],'rules':{'lint:deep-css':'warn','lint:xpath':_0x3893a2(0xc0),'lint:obfuscated-class':_0x3893a2(0xc0),'lint:pause-explicit':_0x3893a2(0xc0),'lint:disambig-by-index':_0x3893a2(0xb0),'lint:evaluate-discouraged':_0x3893a2(0xc0),'lint:scenario-in-root':_0x3893a2(0xa7),'lint:mustache-in-dsl':'error','validator:unknown-function':_0x3893a2(0xa7),'validator:dsl':_0x3893a2(0xa7)}},'mcp':{'transport':'stdio'},'sandbox':{}};export{BrowserChannelSchema,BrowserSchema,DEFAULT_CONFIG,DialogPolicySchema,FailureBundleConfigSchema,LinterConfigSchema,LinterRuleIdSchema,LinterSeveritySchema,McpConfigSchema,RetryConfigSchema,RetryReasonSchema,SandboxConfigSchema,UnotestConfigSchema,ViewportSchema};
|