@unotest/web 0.30.0 → 0.32.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/SKILL.md +12 -0
- package/.claude/skills/write-e2e-test-ground/SKILL.md +12 -0
- package/CHANGELOG.md +383 -0
- package/README.md +66 -2
- package/bin/unotest-web.js +3 -1
- package/dist/config/schema.d.ts +15 -15
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +11 -3
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +54 -4
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.d.ts +22 -2
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/{interfaces-BluheUKs.d.ts → interfaces-B34N4rfI.d.ts} +8 -0
- package/dist/legacy-layout-By62PkbP.d.ts +8 -0
- package/dist/linter-Yf4gPe5J.d.ts +36 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +14 -8
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.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/scaffold-workspace.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 +190 -14
- package/guides/dsl-reference.md +156 -9
- package/package.json +8 -8
- package/src/mcp/prompts/agent-test-author.md +11 -1
- package/types/unotest-dsl.d.ts +29 -5
- package/dist/linter-CM2CpIyW.d.ts +0 -21
package/guides/dsl-reference.md
CHANGED
|
@@ -37,6 +37,68 @@ function test_my_scenario() {
|
|
|
37
37
|
> one block per logical chunk; anything is allowed inside a block (nested
|
|
38
38
|
> blocks, control flow, comments).
|
|
39
39
|
|
|
40
|
+
### `step(...)` forms — tags and soft steps
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
step("label", () => { … });
|
|
44
|
+
step("label", {tag: <expr>}, () => { … });
|
|
45
|
+
step.soft("label", () => { … });
|
|
46
|
+
step.soft("label", {tag: <expr>}, () => { … });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **`label`** is a string literal — the intent a human reads.
|
|
50
|
+
- **`{tag: <expr>}`** names the case a data-driven step is on: any
|
|
51
|
+
expression (`q.id`, `textJoin(['q-', i])`), evaluated when the block
|
|
52
|
+
starts. It rides on every event of the block (`Question [q17]` in the
|
|
53
|
+
CLI line, a chip on the step in the viewer). `tag` is the only option.
|
|
54
|
+
- **`step.soft(...)`** — a failure inside the body is *recorded* instead
|
|
55
|
+
of stopping the run: the rest of that body is skipped (as after
|
|
56
|
+
`expect.soft` inside a Playwright `test.step`), execution continues
|
|
57
|
+
with the statement after the block, and the test still ends **failed**
|
|
58
|
+
with every soft failure listed:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
✗ test: test_control_questions (12.4s)
|
|
62
|
+
3 soft step(s) failed: Question [q17]: …; Question [q42]: …; Question [q58]: …
|
|
63
|
+
✗ soft step "Question" [q17]: judge said no: …
|
|
64
|
+
✗ soft step "Question" [q42]: …
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`return` / `break` / `continue` pass through a soft step untouched. A
|
|
68
|
+
hard failure after soft ones ends the run with *that* error; the soft
|
|
69
|
+
list is still attached. The failure bundle shows the page at the moment
|
|
70
|
+
of the **first** soft failure (`failure.json` carries `soft: true` and
|
|
71
|
+
`stepTag`); later ones are in the journal (`step-block:finished`) and in
|
|
72
|
+
`run_test`'s reply (`next.softFailures`).
|
|
73
|
+
- `step.soft` is allowed **inside `test_*` only** (validator
|
|
74
|
+
`step-shape`): a helper has no test to report a soft failure on.
|
|
75
|
+
Nesting is free — the usual shape is a plain outer step, a loop, and
|
|
76
|
+
tagged soft steps inside it. A soft failure does **not** change the
|
|
77
|
+
outer step's own outcome: the group is not an assertion.
|
|
78
|
+
- In the debugger, resuming past a failure inside a soft step continues
|
|
79
|
+
with the next statement and records nothing — same as resuming past
|
|
80
|
+
any failure on web.
|
|
81
|
+
|
|
82
|
+
The data-driven gate, in full (`readJsonLine` reads the cases; the outer
|
|
83
|
+
`step` satisfies step coverage):
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
function test_control_questions() {
|
|
87
|
+
step("Control questions", () => {
|
|
88
|
+
for (i = 1; i < 11; i = i + 1) {
|
|
89
|
+
q = readJsonLine('questions.jsonl', {slot: i});
|
|
90
|
+
step.soft("Question", {tag: q.id}, () => {
|
|
91
|
+
fill(getByRole('textbox', {name: 'Message'}), q.text);
|
|
92
|
+
press(getByRole('textbox', {name: 'Message'}), 'Enter');
|
|
93
|
+
answer = textContent(getByTestId('last-answer'));
|
|
94
|
+
screenshot(q.id);
|
|
95
|
+
assertJudge(answer, q.rubric);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
40
102
|
> **Locator quality matters (D-22).** Prefer `getByTestId` →
|
|
41
103
|
> `getByRole(name)` → `getByLabel` → `getByText` → `locator(css)`. The AST
|
|
42
104
|
> linter (`unotest-web lint`) flags brittle selectors — see
|
|
@@ -303,8 +365,14 @@ check(getByRole('checkbox', {name: 'Subscribe'}))
|
|
|
303
365
|
|
|
304
366
|
```js
|
|
305
367
|
hover(getByRole('button', {name: 'Help'}))
|
|
368
|
+
// A tall chat bubble whose menu appears only near its top edge: hover
|
|
369
|
+
// the centre and nothing shows — hover an offset inside the element.
|
|
370
|
+
hover(getByTestId('message-42'), {position: {x: 24, y: 8}})
|
|
306
371
|
```
|
|
307
372
|
|
|
373
|
+
**Options:** `force?`, `timeout?`, `position?` (no `modifiers` — that
|
|
374
|
+
is a `click` option).
|
|
375
|
+
|
|
308
376
|
### `selectOption(loc, value, options?)`
|
|
309
377
|
|
|
310
378
|
Native `<select>` only. `value` can be a string, an array of strings, or
|
|
@@ -453,6 +521,11 @@ Semantics:
|
|
|
453
521
|
a separate `preamble` field: the model saw the two concatenated, and
|
|
454
522
|
the split says which half came from the scenario and which from the
|
|
455
523
|
environment.
|
|
524
|
+
- **In the viewer** the verdict sits under the step (`judge ✓/✗ ·
|
|
525
|
+
rubric · text`, open by default when it said no), and the error card
|
|
526
|
+
of a failed judge step carries the whole verdict — rubric, judged
|
|
527
|
+
text, reasoning, model. See `note()` below for attaching the question
|
|
528
|
+
and the answer next to it.
|
|
456
529
|
- A judge is not deterministic: the judge itself re-asks on `fail`
|
|
457
530
|
(`UNOTEST_JUDGE_RETRIES`, first `pass` wins); temperature is pinned to 0
|
|
458
531
|
where the backend accepts it (vertex/gemini). The runner's retry layer is
|
|
@@ -881,6 +954,30 @@ step("A personal chat must not leak into the project feed", () => {
|
|
|
881
954
|
});
|
|
882
955
|
```
|
|
883
956
|
|
|
957
|
+
### `readJsonLine(path, filter)` → `object`
|
|
958
|
+
|
|
959
|
+
The no-wait form of `waitForJsonLine` for a file that **already exists**:
|
|
960
|
+
read it once and return the first line matching `filter`. Use it for
|
|
961
|
+
fixtures and finished exports — a data-driven test reads its cases from
|
|
962
|
+
a JSONL file this way. Same filter rules as `waitForJsonLine` (strict
|
|
963
|
+
equality per key, dot paths for nesting, unparsable lines skipped).
|
|
964
|
+
|
|
965
|
+
```js
|
|
966
|
+
step("Ask control question 3", () => {
|
|
967
|
+
q = readJsonLine('questions.jsonl', {slot: 3});
|
|
968
|
+
fill(getByRole('textbox', {name: 'Message'}), q.text);
|
|
969
|
+
screenshot(q.id);
|
|
970
|
+
});
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
- No options and no poll: a missing file fails at once with
|
|
974
|
+
`file not found: <path>`, a missing line with
|
|
975
|
+
`no line matches {…} in <path>` plus the same parse stats and
|
|
976
|
+
closest-line hint `waitForJsonLine` reports on timeout.
|
|
977
|
+
- Pick `waitForJsonLine` when the file is still being written to (a
|
|
978
|
+
background listener's output); its timeout is what makes that safe,
|
|
979
|
+
and what would only hide a wrong path here.
|
|
980
|
+
|
|
884
981
|
### `waitForFileCount(path, pattern, count, options?)` → `number`
|
|
885
982
|
|
|
886
983
|
Wait until **at least** `count` lines match, then return how many there
|
|
@@ -1152,8 +1249,18 @@ screenshot('bot-reply'); // 002-bot-reply.png
|
|
|
1152
1249
|
screenshot('landing', {fullPage: true}); // full scroll height
|
|
1153
1250
|
```
|
|
1154
1251
|
|
|
1155
|
-
- `name` is
|
|
1156
|
-
|
|
1252
|
+
- `name` is any string value, not only a literal — a data-driven test
|
|
1253
|
+
names its evidence after the case it is on:
|
|
1254
|
+
|
|
1255
|
+
```js
|
|
1256
|
+
q = waitForJsonLine('questions.jsonl', {slot: 3});
|
|
1257
|
+
screenshot(q.id); // 003-q-17.png
|
|
1258
|
+
screenshot(textJoin(['q-', q.id, '-answer']));
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
- `name` is slugified to `[a-z0-9._-]` (`'Bot Reply!'` → `bot-reply`,
|
|
1262
|
+
`'Q 17/a'` → `q-17-a`); omitted → `shot`. Files are numbered in
|
|
1263
|
+
capture order.
|
|
1157
1264
|
- **Options:** `fullPage?` (default `false` — viewport only).
|
|
1158
1265
|
- Before capturing, the call waits for the network to go quiet and for a
|
|
1159
1266
|
painted frame (up to 2s). Without it a `screenshot()` right after
|
|
@@ -1174,17 +1281,50 @@ screenshot('landing', {fullPage: true}); // full scroll height
|
|
|
1174
1281
|
|
|
1175
1282
|
---
|
|
1176
1283
|
|
|
1177
|
-
## Logging
|
|
1284
|
+
## Logging and notes
|
|
1178
1285
|
|
|
1179
1286
|
### `log(...args)`
|
|
1180
1287
|
|
|
1181
|
-
|
|
1182
|
-
`
|
|
1288
|
+
Writes one line to the runner's output (the terminal, the live System
|
|
1289
|
+
pane in the viewer) **and** to the run journal (`steps.jsonl`, as a
|
|
1290
|
+
`log` event under the current step), so a finished run still shows it
|
|
1291
|
+
under the step that wrote it and in the System pane. Non-string args
|
|
1292
|
+
are `JSON.stringify`'d.
|
|
1183
1293
|
|
|
1184
1294
|
```js
|
|
1185
1295
|
log('user id:', userId);
|
|
1186
1296
|
```
|
|
1187
1297
|
|
|
1298
|
+
### `note(label, value)`
|
|
1299
|
+
|
|
1300
|
+
Attach a labelled value to the **current step**: the question a
|
|
1301
|
+
data-driven case asked, the answer it got, an id worth seeing next to
|
|
1302
|
+
the verdict. Lives in the journal, not in stdout — the viewer shows it
|
|
1303
|
+
under the step (an `ⓘ N` chip on the row; the list opens by default
|
|
1304
|
+
when the step failed or a judge said no) during and after the run.
|
|
1305
|
+
|
|
1306
|
+
```js
|
|
1307
|
+
step.soft("Question", {tag: q.id}, () => {
|
|
1308
|
+
note('question', q.text);
|
|
1309
|
+
fill(getByRole('textbox', {name: 'Message'}), q.text);
|
|
1310
|
+
press(getByRole('textbox', {name: 'Message'}), 'Enter');
|
|
1311
|
+
answer = textContent(getByTestId('last-answer'));
|
|
1312
|
+
note('answer', answer);
|
|
1313
|
+
assertJudge(getByTestId('last-answer'), q.rubric);
|
|
1314
|
+
});
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
- `label` is a string; `value` is anything — a string as it is, a
|
|
1318
|
+
number, an object or array serialized as JSON.
|
|
1319
|
+
- Long values are cut at 4 KB (marked `truncated`); secrets are masked
|
|
1320
|
+
like everywhere in the run directory.
|
|
1321
|
+
- Each `note` / `log` event records where it was called (`file`, `line`,
|
|
1322
|
+
`col`) and the nearest statement of the entry file (`entryLine`,
|
|
1323
|
+
`entryCol`) — a note from inside a helper lands under the entry step
|
|
1324
|
+
that called the helper.
|
|
1325
|
+
- Without a run journal (exploration, an ad-hoc runtime) `note` goes to
|
|
1326
|
+
the logger instead; nothing fails.
|
|
1327
|
+
|
|
1188
1328
|
---
|
|
1189
1329
|
|
|
1190
1330
|
## Variables, control flow, types
|
|
@@ -1192,8 +1332,13 @@ log('user id:', userId);
|
|
|
1192
1332
|
The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
1193
1333
|
(`vendor/dsl/`), not V8. What's supported in scenario files:
|
|
1194
1334
|
|
|
1195
|
-
- **`function test_*(): { … }`** — scenario entry
|
|
1196
|
-
|
|
1335
|
+
- **`function test_*(): { … }`** — the scenario entry, **exactly one per
|
|
1336
|
+
file**: a collection runs files, and the viewer projects a run onto
|
|
1337
|
+
the first `test_*` it finds. A second `test_*` is reported by
|
|
1338
|
+
`lint:one-test-per-file` (a warning in 0.31, an error in a later
|
|
1339
|
+
release) — fold the cases into one test with tagged steps
|
|
1340
|
+
(`step("…", {tag: id}, …)`), or move shared journeys to
|
|
1341
|
+
`unotest/e2e/_helpers/`.
|
|
1197
1342
|
- **Assignments** — `name = expression;` (no `let`/`const`/`var`).
|
|
1198
1343
|
- **`if` / `else`** and **`for` in the canonical shape**
|
|
1199
1344
|
`for (i = 0; i < N; i = i + 1) { … }` (increment must be an
|
|
@@ -1249,8 +1394,10 @@ exact grammar deltas from the upstream parser.
|
|
|
1249
1394
|
## Conventions
|
|
1250
1395
|
|
|
1251
1396
|
- **Snake-case test names** — `function test_login_happy_path()`.
|
|
1252
|
-
- **One scenario file = one
|
|
1253
|
-
|
|
1397
|
+
- **One scenario file = one test.** The file is the unit a collection
|
|
1398
|
+
runs and the viewer shows; several cases of one feature are one
|
|
1399
|
+
`test_*` with a loop and `step("…", {tag: id}, …)` per case, not
|
|
1400
|
+
several `test_*`.
|
|
1254
1401
|
- **Helpers stay in `unotest/e2e/_helpers/`** — these are project-owned,
|
|
1255
1402
|
not part of the core surface. Use sandbox primitives there.
|
|
1256
1403
|
- **Two kinds of helper.** A `flow_*` helper is a composite journey —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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": {
|
|
@@ -83,18 +83,18 @@
|
|
|
83
83
|
"tsx": "^4.19.2",
|
|
84
84
|
"yaml": "^2.6.0",
|
|
85
85
|
"zod": "^3.23.8",
|
|
86
|
-
"@unotest/core": "^0.
|
|
87
|
-
"@unotest/
|
|
88
|
-
"@unotest/grounder-client": "^0.
|
|
89
|
-
"@unotest/dsl": "^0.
|
|
90
|
-
"@unotest/
|
|
86
|
+
"@unotest/core": "^0.32.0",
|
|
87
|
+
"@unotest/protocol": "^0.32.0",
|
|
88
|
+
"@unotest/grounder-client": "^0.32.0",
|
|
89
|
+
"@unotest/dsl": "^0.32.0",
|
|
90
|
+
"@unotest/viewer": "^0.32.0"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@types/node": "^22.10.0",
|
|
94
94
|
"javascript-obfuscator": "^4.1.1",
|
|
95
95
|
"tsup": "^8.5.1",
|
|
96
96
|
"typescript": "^5.7.2",
|
|
97
|
-
"@unotest/
|
|
98
|
-
"@unotest/
|
|
97
|
+
"@unotest/grounder": "^0.32.0",
|
|
98
|
+
"@unotest/judge": "^0.32.0"
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -49,6 +49,14 @@ The linter rejects a test in the e2e root (`lint:scenario-in-root`), and
|
|
|
49
49
|
from the area of the app under test. Shared helpers live in
|
|
50
50
|
`unotest/e2e/_helpers/`; `_template.js` is a starting point to copy.
|
|
51
51
|
|
|
52
|
+
One `test_*` per file. The file is what a collection runs and what the viewer
|
|
53
|
+
shows (its Steps tree projects the run onto the first `test_*`; any other
|
|
54
|
+
runs unseen). Several cases of one feature are ONE test with a loop and
|
|
55
|
+
`step("…", {tag: id}, () => { … })` per case — `step.soft` when a failed case
|
|
56
|
+
must not stop the run; shared journeys are `flow_*` helpers in
|
|
57
|
+
`unotest/e2e/_helpers/`. A second `test_*` is flagged by
|
|
58
|
+
`lint:one-test-per-file` (a warning today, an error in a later release).
|
|
59
|
+
|
|
52
60
|
## Locator hierarchy (D-22, hard rule)
|
|
53
61
|
|
|
54
62
|
When choosing a selector, prefer in this exact order:
|
|
@@ -310,7 +318,9 @@ A login (or any setup block) that many tests share should be a reusable
|
|
|
310
318
|
- **Flow already exists** (it's in `availableFlows`): call
|
|
311
319
|
`explore_run_flow({ explorationId, name })`. It replays the flow LIVE (you end
|
|
312
320
|
up logged in) and records a single `flow_<name>()` call — don't re-record the
|
|
313
|
-
steps.
|
|
321
|
+
steps. A flow with parameters (`params` in `availableFlows`) takes them as
|
|
322
|
+
`args`, positional — `args: ["{{LOGIN}}", "{{PASSWORD}}"]` replays on the
|
|
323
|
+
values and records `flow_login(LOGIN, PASSWORD)`; the count must match.
|
|
314
324
|
- **First time** (no flow yet): record the block normally but put
|
|
315
325
|
`flow:"signin"` on every step of it (`explore_step({…, flow:"signin"})`).
|
|
316
326
|
On `save_exploration_as_test`, those steps are extracted into
|
package/types/unotest-dsl.d.ts
CHANGED
|
@@ -120,9 +120,9 @@ declare function check(locator: Locator, options?: any): void;
|
|
|
120
120
|
declare function uncheck(locator: Locator, options?: any): void;
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* hover(locator, { force?, timeout? })
|
|
123
|
+
* hover(locator, { force?, position?, timeout? })
|
|
124
124
|
*
|
|
125
|
-
* Hover over an element.
|
|
125
|
+
* Hover over an element. `position` is an {x, y} offset inside it — for a menu that only appears at one edge of a tall element.
|
|
126
126
|
*/
|
|
127
127
|
declare function hover(locator: Locator, options?: any): void;
|
|
128
128
|
|
|
@@ -280,6 +280,13 @@ declare function randomNth(receiver: Locator): Locator;
|
|
|
280
280
|
*/
|
|
281
281
|
declare function screenshot(name?: string, options?: any): string;
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* note(label, value)
|
|
285
|
+
*
|
|
286
|
+
* Attach a labelled value to the current step: shown in the viewer under the step (also after the run) and kept in the run journal. Any value — a string, a number, an object (recorded as JSON). Secrets are masked; long values are cut at 4 KB. Use it for the question a data-driven case asked and the answer it got.
|
|
287
|
+
*/
|
|
288
|
+
declare function note(label: string, value: any): void;
|
|
289
|
+
|
|
283
290
|
/**
|
|
284
291
|
* evaluate(js, arg?) → any
|
|
285
292
|
*
|
|
@@ -511,6 +518,13 @@ declare function waitForJsonLine(path: string, filter: any, options?: any): any;
|
|
|
511
518
|
*/
|
|
512
519
|
declare function assertNoJsonLine(path: string, filter: any, options?: any): void;
|
|
513
520
|
|
|
521
|
+
/**
|
|
522
|
+
* readJsonLine(path, filter) → object
|
|
523
|
+
*
|
|
524
|
+
* First matching line of a JSONL file that already exists, no wait: one read, same key filter as waitForJsonLine. FAILS at once when the file is missing or no line matches — use it for fixtures and finished exports, waitForJsonLine for a file still being written.
|
|
525
|
+
*/
|
|
526
|
+
declare function readJsonLine(path: string, filter: any): any;
|
|
527
|
+
|
|
514
528
|
/**
|
|
515
529
|
* waitForFileCount(path, pattern, count, options?) → number
|
|
516
530
|
*
|
|
@@ -654,7 +668,7 @@ declare function getUrl(): string;
|
|
|
654
668
|
/**
|
|
655
669
|
* log(...args)
|
|
656
670
|
*
|
|
657
|
-
* Write to the run
|
|
671
|
+
* Write a line to the runner's output AND to the run journal, under the current step — visible in the viewer's System pane after the run too.
|
|
658
672
|
*/
|
|
659
673
|
declare function log(message: any, ...args: unknown[]): void;
|
|
660
674
|
|
|
@@ -714,8 +728,18 @@ declare function waitForCount(locator: Locator, count: number, options?: any): v
|
|
|
714
728
|
*/
|
|
715
729
|
declare function pause(ms: number): void;
|
|
716
730
|
|
|
717
|
-
/** step(label, body)
|
|
731
|
+
/** step(label, [options,] body)
|
|
718
732
|
*
|
|
719
733
|
* Groups statements under a labelled block. Every direct child of a
|
|
720
|
-
* test_* function must sit inside a step(...).
|
|
734
|
+
* test_* function must sit inside a step(...). `{tag}` names the case a
|
|
735
|
+
* data-driven step is on. */
|
|
721
736
|
declare function step(label: string, body: () => void): void;
|
|
737
|
+
declare function step(label: string, options: { tag: unknown }, body: () => void): void;
|
|
738
|
+
declare namespace step {
|
|
739
|
+
/** step.soft(label, [options,] body)
|
|
740
|
+
*
|
|
741
|
+
* A failure inside is recorded and the run continues after the block;
|
|
742
|
+
* the test still ends failed. Allowed inside test_* only. */
|
|
743
|
+
function soft(label: string, body: () => void): void;
|
|
744
|
+
function soft(label: string, options: { tag: unknown }, body: () => void): void;
|
|
745
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { BlockStatement } from '@unotest/dsl';
|
|
2
|
-
|
|
3
|
-
type LintSeverity = 'error' | 'warning' | 'info';
|
|
4
|
-
interface LintDiagnostic {
|
|
5
|
-
rule: string;
|
|
6
|
-
severity: LintSeverity;
|
|
7
|
-
message: string;
|
|
8
|
-
line: number;
|
|
9
|
-
col: number;
|
|
10
|
-
}
|
|
11
|
-
interface LintOptions {
|
|
12
|
-
/** Per-rule severity override. Default severities live with each rule. */
|
|
13
|
-
ruleSeverity?: Partial<Record<string, LintSeverity>>;
|
|
14
|
-
/** Pass through if the agent provided documented justification — e.g. a
|
|
15
|
-
* `// reason:` comment immediately above the line. The lexer doesn't
|
|
16
|
-
* preserve comments today; for v1 we accept a flag from the caller. */
|
|
17
|
-
allowDisambigByIndex?: boolean;
|
|
18
|
-
}
|
|
19
|
-
declare function lintAst(ast: BlockStatement, opts?: LintOptions): LintDiagnostic[];
|
|
20
|
-
|
|
21
|
-
export { type LintDiagnostic as L, type LintSeverity as a, type LintOptions as b, lintAst as l };
|