@asrunjs/asrun 0.9.0 → 0.11.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/commands/explore-case.md +11 -6
- package/.claude/commands/gen-spec.md +10 -6
- package/.claude/commands/heal-spec.md +13 -9
- package/.claude/commands/make-case.md +1 -1
- package/.claude/skills/exploring-cases/SKILL.md +84 -63
- package/.claude/skills/exploring-cases/references/assertion-targets.md +8 -4
- package/.claude/skills/exploring-cases/references/case-rules.md +110 -0
- package/.claude/skills/exploring-cases/references/handoff-card.js +73 -11
- package/.claude/skills/exploring-cases/references/notes.md +87 -0
- package/.claude/skills/exploring-cases/references/open-questions.md +58 -6
- package/.claude/skills/exploring-cases/references/readiness.md +7 -5
- package/.claude/skills/exploring-cases/references/tools.md +151 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +47 -18
- package/.claude/skills/generating-specs/SKILL.md +99 -54
- package/.claude/skills/generating-specs/references/conventions.md +4 -0
- package/.claude/skills/making-cases/SKILL.md +14 -7
- package/.claude/skills/making-cases/references/gaps.md +7 -4
- package/.claude/skills/making-cases/references/normalizing.md +1 -1
- package/AGENTS.md +61 -20
- package/README.md +10 -8
- package/bin/asrun.mjs +2 -0
- package/dist/lib/case-doc.d.ts +41 -4
- package/dist/lib/case-doc.js +109 -17
- package/dist/lib/explore-notes.d.ts +74 -0
- package/dist/lib/explore-notes.js +242 -0
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +2 -0
- package/dist/scripts/check-cases.js +89 -1
- package/dist/scripts/cli.js +9 -2
- package/dist/scripts/doctor.js +50 -3
- package/dist/scripts/explore-notes.d.ts +1 -0
- package/dist/scripts/explore-notes.js +131 -0
- package/dist/scripts/markers-confirm.d.ts +1 -0
- package/dist/scripts/markers-confirm.js +139 -0
- package/dist/ui/server/actions.js +50 -16
- package/dist/ui/server/app.js +7 -2
- package/dist/ui/server/job-store.d.ts +60 -0
- package/dist/ui/server/job-store.js +198 -0
- package/dist/ui/server/jobs.d.ts +46 -4
- package/dist/ui/server/jobs.js +361 -59
- package/dist/ui/server/process-info.d.ts +17 -0
- package/dist/ui/server/process-info.js +126 -0
- package/dist/ui/server/result-state.d.ts +10 -0
- package/dist/ui/server/result-state.js +106 -0
- package/dist/ui/shared/ai-commands.d.ts +4 -1
- package/dist/ui/shared/ai-commands.js +14 -4
- package/dist/ui/shared/types.d.ts +17 -2
- package/docs/agents/case-format.md +40 -5
- package/docs/agents/framework-dev.md +4 -0
- package/docs/ai-workflow.md +56 -16
- package/docs/cases.md +32 -8
- package/docs/commands.md +17 -1
- package/docs/theme-guide.html +56 -0
- package/docs/troubleshooting.md +40 -0
- package/docs/web-ui.md +59 -15
- package/package.json +3 -1
- package/templates/.claude/commands/explore-case.md +11 -6
- package/templates/.claude/commands/gen-spec.md +10 -5
- package/templates/.claude/commands/heal-spec.md +11 -7
- package/templates/.claude/commands/make-case.md +1 -1
- package/templates/AGENTS.md +9 -6
- package/templates/cases/_template.md +7 -0
- package/templates/package.json +2 -0
- package/ui/dist/assets/{dist-WvvCZeeu.js → dist--BXJInTq.js} +1 -1
- package/ui/dist/assets/{dist-B7HeglDd.js → dist-BMmNFIUH.js} +1 -1
- package/ui/dist/assets/{dist-DevASegc.js → dist-BXNaYHeh.js} +1 -1
- package/ui/dist/assets/{dist-Bb36rNUw.js → dist-ByLbEOlF.js} +1 -1
- package/ui/dist/assets/{dist-Dqc_tfzC.js → dist-C9ra6_BD.js} +1 -1
- package/ui/dist/assets/{dist-DfBpjgZT.js → dist-CK8mow8u.js} +1 -1
- package/ui/dist/assets/{dist-G9KNeh6u.js → dist-CQS0So-i.js} +1 -1
- package/ui/dist/assets/{dist-GvRUDL-2.js → dist-CTrwRc3Q.js} +1 -1
- package/ui/dist/assets/{dist-B9_GXR2G.js → dist-DTYj2vUD.js} +1 -1
- package/ui/dist/assets/{dist-Dq4Vnw5N.js → dist-mV0_nQZe.js} +1 -1
- package/ui/dist/assets/index-BzMqkmQ9.js +348 -0
- package/ui/dist/assets/index-DcJR-8Mt.css +2 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BJ5D2mFK.js +0 -346
- package/ui/dist/assets/index-BXY-eKBn.css +0 -2
|
@@ -9,6 +9,52 @@ async (page) => {
|
|
|
9
9
|
const maxMs = cfg.maxMs || 600000;
|
|
10
10
|
await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
|
|
11
11
|
|
|
12
|
+
// Record what the person does. The card owns both ends of the recording, so every way out stops it and no handoff
|
|
13
|
+
// leaks a recording into the next one (see user-handoff.md, "Recording"). Never call browser_start_recording: the
|
|
14
|
+
// playwright-test MCP lists it but cannot run it (devtools-only; see tools.md in this folder).
|
|
15
|
+
const CARD_CONTROL = /AsRun handoff:/i;
|
|
16
|
+
const entries = [];
|
|
17
|
+
const entry = (action, code) => ({
|
|
18
|
+
code,
|
|
19
|
+
card: CARD_CONTROL.test(String((action && action.selector) || '')),
|
|
20
|
+
});
|
|
21
|
+
const sink = {
|
|
22
|
+
actionAdded: (p, action, code) => entries.push(entry(action, code)),
|
|
23
|
+
actionUpdated: (p, action, code) => {
|
|
24
|
+
const next = entry(action, code);
|
|
25
|
+
if (entries.length) entries[entries.length - 1] = next;
|
|
26
|
+
else entries.push(next);
|
|
27
|
+
},
|
|
28
|
+
signalAdded: (p, signal, code) => {
|
|
29
|
+
const last = entries[entries.length - 1];
|
|
30
|
+
if (last && code && !last.card) last.code = code;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
let recording = false;
|
|
34
|
+
let recordingError = null;
|
|
35
|
+
// Library-mode code (`page`, `page1`, ...) keeps the `newPage`/`close` lines of tabs the person opened by hand; the
|
|
36
|
+
// test-runner generator drops `openPage`/`closePage`.
|
|
37
|
+
const recordingOptions = {language: 'javascript'};
|
|
38
|
+
try {
|
|
39
|
+
if (typeof ctx._startRecording !== 'function' || typeof ctx._stopRecording !== 'function')
|
|
40
|
+
throw new Error('this Playwright has no browser-context recording');
|
|
41
|
+
await ctx._startRecording(recordingOptions, sink);
|
|
42
|
+
recording = true;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// A card that died before stopping left the context recording: close that one and try once more.
|
|
45
|
+
if (/already in progress/i.test(String(e && e.message))) {
|
|
46
|
+
await ctx._stopRecording().catch(() => {});
|
|
47
|
+
try {
|
|
48
|
+
await ctx._startRecording(recordingOptions, sink);
|
|
49
|
+
recording = true;
|
|
50
|
+
} catch (e2) {
|
|
51
|
+
recordingError = String((e2 && e2.message) || e2);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
recordingError = String((e && e.message) || e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
12
58
|
const paint = (p) => p.evaluate(({message, copy}) => {
|
|
13
59
|
if (document.getElementById('__ASRUN_panel')) return;
|
|
14
60
|
const KEY = '__ASRUN_panel_state';
|
|
@@ -41,9 +87,9 @@ async (page) => {
|
|
|
41
87
|
'.row{display:flex;gap:8px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
|
|
42
88
|
'</style>' +
|
|
43
89
|
'<div class="card" id="card"><div class="bar" id="bar" title="Drag to move"><span class="grip">' + ICON.move + '</span><b id="title">YOUR TURN</b>' +
|
|
44
|
-
'<button class="mini-done" data-a="done">Done</button><button id="toggle" title="Minimize or expand" aria-label="
|
|
90
|
+
'<button class="mini-done" data-a="done" aria-label="AsRun handoff: done">Done</button><button id="toggle" title="Minimize or expand" aria-label="AsRun handoff: minimize or expand"></button></div>' +
|
|
45
91
|
'<div class="body"><div id="msg"></div><div id="chips"></div><div class="row">' +
|
|
46
|
-
'<button class="primary" data-a="done">Done</button><button data-a="skip">Cannot do this</button></div></div></div>';
|
|
92
|
+
'<button class="primary" data-a="done" aria-label="AsRun handoff: done">Done</button><button data-a="skip" aria-label="AsRun handoff: cannot do this">Cannot do this</button></div></div></div>';
|
|
47
93
|
const card = root.getElementById('card'), bar = root.getElementById('bar'), toggle = root.getElementById('toggle');
|
|
48
94
|
root.getElementById('msg').textContent = message;
|
|
49
95
|
const chips = root.getElementById('chips');
|
|
@@ -51,7 +97,7 @@ async (page) => {
|
|
|
51
97
|
for (const text of copy) {
|
|
52
98
|
const chip = document.createElement('div');
|
|
53
99
|
chip.className = 'chip';
|
|
54
|
-
chip.innerHTML = '<span></span><button aria-label="
|
|
100
|
+
chip.innerHTML = '<span></span><button aria-label="AsRun handoff: copy to clipboard" title="Copy">' + ICON.copy + '</button>';
|
|
55
101
|
chip.firstChild.textContent = text;
|
|
56
102
|
const btn = chip.lastChild;
|
|
57
103
|
btn.addEventListener('click', async () => {
|
|
@@ -107,14 +153,18 @@ async (page) => {
|
|
|
107
153
|
for (const p of ctx.pages()) await p.evaluate(() => { window.__handoff = null; }).catch(() => {});
|
|
108
154
|
const started = Date.now();
|
|
109
155
|
let outcome = null;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
try {
|
|
157
|
+
while (!outcome && Date.now() - started < maxMs) {
|
|
158
|
+
for (const p of ctx.pages()) {
|
|
159
|
+
await paint(p);
|
|
160
|
+
const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
|
|
161
|
+
if (flag) outcome = flag;
|
|
162
|
+
else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
|
|
163
|
+
}
|
|
164
|
+
if (!outcome) await page.waitForTimeout(500);
|
|
116
165
|
}
|
|
117
|
-
|
|
166
|
+
} finally {
|
|
167
|
+
if (recording) await ctx._stopRecording().catch(() => {});
|
|
118
168
|
}
|
|
119
169
|
for (const p of ctx.pages()) {
|
|
120
170
|
await p.evaluate(() => {
|
|
@@ -124,5 +174,17 @@ async (page) => {
|
|
|
124
174
|
}
|
|
125
175
|
await page.waitForTimeout(2300);
|
|
126
176
|
for (const p of ctx.pages()) await p.evaluate(() => sessionStorage.removeItem('__ASRUN_panel_state')).catch(() => {});
|
|
127
|
-
|
|
177
|
+
const dedent = (text) => {
|
|
178
|
+
const lines = text.split('\n');
|
|
179
|
+
const indents = lines.filter((line) => line.trim()).map((line) => line.length - line.trimStart().length);
|
|
180
|
+
const indent = indents.length ? Math.min(...indents) : 0;
|
|
181
|
+
return lines.map((line) => line.substring(indent)).join('\n');
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
outcome: outcome || 'timeout',
|
|
185
|
+
waitedMs: Date.now() - started,
|
|
186
|
+
recording,
|
|
187
|
+
recorded: entries.filter((e) => e.code && e.code.trim() && !e.card).map((e) => dedent(e.code)),
|
|
188
|
+
...(recordingError ? {recordingError} : {}),
|
|
189
|
+
};
|
|
128
190
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# The exploration note: what a run saw, in a form `/gen-spec` can read
|
|
2
|
+
|
|
3
|
+
Local, git-ignored and optional. `exploring-cases` writes it; `generating-specs` (and healing) read it to **verify**
|
|
4
|
+
instead of rediscovering the page. It is not part of the case and never becomes part of it: expected values stay in
|
|
5
|
+
`## Values`, behaviour in `## Expected`, ready signals in `Ready when:`, and assertions never appear here.
|
|
6
|
+
|
|
7
|
+
## Where it lives
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.explore/notes/<case-id>/<profile-id>.json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
One file per profile. A `--profiles a,b` run writes only `a.json` and `b.json`; a later run with `c` adds `c.json`
|
|
14
|
+
next to them, so nothing is merged and an earlier observation is never overwritten. A fresh checkout has no notes, and
|
|
15
|
+
that is a normal state: generation falls back to replaying the case.
|
|
16
|
+
|
|
17
|
+
## The shape
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"version": 1,
|
|
22
|
+
"caseId": "conduit/header-navigation",
|
|
23
|
+
"profile": "example-staging",
|
|
24
|
+
"env": "staging",
|
|
25
|
+
"caseSha256": "<pnpm asrun explore:notes <case-id> --hash>",
|
|
26
|
+
"capturedAt": "2026-11-09T10:20:31Z",
|
|
27
|
+
"steps": {
|
|
28
|
+
"2": {
|
|
29
|
+
"url": "/",
|
|
30
|
+
"locators": [{"purpose": "the header navigation link", "locator": "page.getByRole('link', {name: 'Home'})"}],
|
|
31
|
+
"requests": ["/api/articles"]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"targets": {"T1": {"locator": "page.getByRole('img', {name: 'Example'})", "matches": 1}}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Keys of `steps` are the case's step numbers, keys of `targets` are its card ids (`T1`).
|
|
39
|
+
- `steps` may be `{}` and `targets` may be omitted; every other key is required (`env` and `updatedAt` are optional).
|
|
40
|
+
No other field is accepted.
|
|
41
|
+
- `caseSha256` is the 64-character hash `pnpm asrun explore:notes <case-id> --hash` prints (not the placeholder above).
|
|
42
|
+
- `pnpm asrun explore:notes <case-id> --check` validates the files: it fails on a malformed note and warns on a note
|
|
43
|
+
whose `caseSha256` no longer matches the case (the summary already prints it as `stale`; re-run the exploration, or
|
|
44
|
+
delete the note and let the case be replayed). A file that fails is ignored by `/gen-spec`.
|
|
45
|
+
|
|
46
|
+
## What goes in
|
|
47
|
+
|
|
48
|
+
`steps.<n>`
|
|
49
|
+
- `url`: the path the step runs on, relative to `baseURL` (`/documents/`), when it is not the current page.
|
|
50
|
+
- `locators`: the element the step acts on, not every element on the page. `purpose` says what it is in words; `locator`
|
|
51
|
+
is exactly what `browser_generate_locator` returned. Prefer the stable controls (`getByRole`, `getByTestId`,
|
|
52
|
+
`getByLabel`) and one entry per action.
|
|
53
|
+
- `requests`: the data-call paths the step's action started, taken from `browser_network_requests` above the marker
|
|
54
|
+
(`tools.md`, "Network requests without a clear"). Path only: `/api/articles`, never a host, a query string or a token.
|
|
55
|
+
Images, fonts and analytics are not data calls.
|
|
56
|
+
|
|
57
|
+
`targets.T1`
|
|
58
|
+
- `locator`: the card's `Identified by`, confirmed on the page.
|
|
59
|
+
- `matches`: how many elements it matched — `1` for a completed card. A card with open doubts gets no entry.
|
|
60
|
+
|
|
61
|
+
## What never goes in
|
|
62
|
+
|
|
63
|
+
- Expected values, counts, texts a check asserts: they belong in the case's `## Values` and `## Expected`.
|
|
64
|
+
- Ready signals: they belong in `Ready when:` (the note holds the request paths, not the promise).
|
|
65
|
+
- Assertions, test steps or a copy of the spec.
|
|
66
|
+
- Cookies, headers, storage state, tokens or a URL with a query string. `.explore/` is local, but a note is still not a
|
|
67
|
+
place for a credential (`F5`).
|
|
68
|
+
- A selector or an id that a future run would copy into `cases/*.md`. The case stays implementation-free; the note is
|
|
69
|
+
the only place a locator lives.
|
|
70
|
+
|
|
71
|
+
## Lists and changing data
|
|
72
|
+
|
|
73
|
+
A list, a row or a search result is recorded by its container and position (`page.getByRole('row').nth(0)`), never by
|
|
74
|
+
the name of the item, because the data changes between runs. The same rule as a recipe (`user-handoff.md`).
|
|
75
|
+
|
|
76
|
+
## Freshness and the write-back
|
|
77
|
+
|
|
78
|
+
- `caseSha256` is the sha256 of `cases/<case-id>.md` at the moment the note was written. If the case changes after that,
|
|
79
|
+
the note is **stale**: `/gen-spec` replays the case and does not use it.
|
|
80
|
+
- `/gen-spec` confirms an entry of a fresh note on the page and may write the verified locator or request back
|
|
81
|
+
(`updatedAt` gets the time). It never adds a step or a target, and it never touches a stale note.
|
|
82
|
+
- Deleting `.explore/notes/` is always safe: generation then re-derives everything from the page.
|
|
83
|
+
|
|
84
|
+
## Who reads it
|
|
85
|
+
|
|
86
|
+
Both skills read this file and `tools.md`, `readiness.md` and `assertion-targets.md`; the details of a browser call are
|
|
87
|
+
in `tools.md`, never repeated here.
|
|
@@ -5,7 +5,7 @@ without anybody present (a one-shot `claude -p` / `opencode run`). The case file
|
|
|
5
5
|
talk to each other, so nothing is lost between runs, tools or sessions.
|
|
6
6
|
|
|
7
7
|
## How a question is written in the case file
|
|
8
|
-
Every marker starts with its emoji, which is part of the marker: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, `🔍 to be confirmed
|
|
8
|
+
Every marker starts with its emoji, which is part of the marker: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, `🔍 to be confirmed` and `👁️ not observed`. The web UI colors the emoji with the marker and offers an X button that deletes it. `(seen on <profile>)` is not a marker: it is a plain note, shown as ordinary text.
|
|
9
9
|
Every question is a small block that hangs under a normal sentence. The sentence is the agent's suggestion, written
|
|
10
10
|
exactly as it should read in the final case. The block holds the question and an empty answer slot:
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ exactly as it should read in the final case. The block holds the question and an
|
|
|
15
15
|
- ✍️ Answer:
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The same shape is used under a step, under a bullet of "Expected" and
|
|
18
|
+
The same shape is used under a step, under a bullet of "Expected" and on the prose line above the `## Values` block:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
11. Delete the document with the "Delete" option of its "..." menu on the list page. [writes]
|
|
@@ -40,8 +40,52 @@ Rules for writing it:
|
|
|
40
40
|
QA should confirm it later but nothing waits for it.
|
|
41
41
|
- One question per block; narrow broad questions down instead of copying them ("which profile elements?" becomes
|
|
42
42
|
"is the logo the only per-profile element in the left side bar?").
|
|
43
|
-
- A
|
|
44
|
-
|
|
43
|
+
- A fact read from a page is not a question: either say it as a plain note `(seen on <profile>)`, or mark it
|
|
44
|
+
`(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)` when a wrong reading would still pass.
|
|
45
|
+
It needs no TODO block. See "Marker or question, never both" below.
|
|
46
|
+
|
|
47
|
+
## Where the block hangs
|
|
48
|
+
The parent line is the sentence the question is about: the panel shows it on its own, next to "The text above this
|
|
49
|
+
question", so it has to make sense without the rest of the document.
|
|
50
|
+
- **The immediate line, not an ancestor.** Hang the block under the one sentence that makes the claim. Under a
|
|
51
|
+
nested bullet, indent it under that bullet, not under the top-level one above it; when the claim only makes sense
|
|
52
|
+
with an umbrella sentence, write the claim itself as a top-level bullet and hang the block there.
|
|
53
|
+
- **One claim, one block.** Several bullets raising one question get one block, under the closest of them, with the
|
|
54
|
+
others' evidence in that block. Never end a long list with a single question that covers all of it.
|
|
55
|
+
- **A sentence that stands alone.** No "see above", no pronoun whose referent is in a sibling bullet.
|
|
56
|
+
- **Never inside the `## Values` yaml block.** A marker or a question there becomes part of the expected value: put
|
|
57
|
+
it on the prose line above the block.
|
|
58
|
+
|
|
59
|
+
## Marker or question, never both
|
|
60
|
+
A sentence carries at most one "not settled yet" signal, and the two marks are alternatives:
|
|
61
|
+
- `🚫/❓ TODO`: there **is** a question. The parent sentence is a suggestion, the block says what to decide.
|
|
62
|
+
- `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`: there is **no** question, only a check
|
|
63
|
+
somebody has to make later. The part after the colon is required: "to be confirmed by QA" alone tells a person
|
|
64
|
+
nothing, and the panel shows that part as "why it needs you".
|
|
65
|
+
- `(observed on <profile>, evidence: .explore/observations/<case-id>/<image>.png)`: the same, with a screenshot the
|
|
66
|
+
reviewer opens instead of going back to the page. Save it while exploring.
|
|
67
|
+
- `(seen on <profile>)`: a plain note. The run checks it, so nobody has to; it gets no colour and no panel row.
|
|
68
|
+
|
|
69
|
+
- Never put a marker inside a TODO block: not in the question, not in the evidence, not in a wrapped line. The words
|
|
70
|
+
"to be confirmed", "TBC", "TBD" and "not observed" are marker words even in prose there, and `pnpm case:check` fails
|
|
71
|
+
on them.
|
|
72
|
+
- Never put a marker on the sentence a block hangs under: keep the question and drop the marker.
|
|
73
|
+
- `👁️ not observed` is for an expectation that came from the notes and was never seen on a page. It is never the
|
|
74
|
+
evidence of a question.
|
|
75
|
+
|
|
76
|
+
## Which sentences carry a marker
|
|
77
|
+
A marker is for exactly one thing: a fact a person must settle because a wrong reading would still pass. Ask yourself:
|
|
78
|
+
**if this line were read wrong, would the generated spec fail?**
|
|
79
|
+
- **It would fail** → no marker. Locators, the flow of a step, navigation, request URLs, readiness signals, whether an
|
|
80
|
+
element exists. The run is the confirmation. Leave `(seen on <profile>)` when where it was seen matters.
|
|
81
|
+
- **It would pass anyway** → marker, with the reason. The literals a check asserts (a `## Values` entry, the wording of
|
|
82
|
+
an `## Expected` bullet) and the identity of a `[T<n>]` target. This is the class that hides a wrong expectation
|
|
83
|
+
forever; `assertion-targets.md` records two real incidents of it.
|
|
84
|
+
- A `## Rules` bullet the agent inferred from a page carries a marker only when the rule changes what is asserted;
|
|
85
|
+
otherwise it is `(seen on <profile>)`. A rule a person stated never carries one.
|
|
86
|
+
|
|
87
|
+
After a passing run, `pnpm markers:confirm --case <id> --env <env>` settles the markers whose line the spec checks
|
|
88
|
+
(`// unconfirmed: <the sentence>`), so a person does not have to open the page again to see whether it is still true.
|
|
45
89
|
|
|
46
90
|
## How the user answers
|
|
47
91
|
Two ways, and the agent must understand both on the next run:
|
|
@@ -69,7 +113,9 @@ answer, adds `(page shows <x> on <profile-id>)` and reports it as a finding. It
|
|
|
69
113
|
## Resolution order (no question goes to the user while a better step exists)
|
|
70
114
|
1. **The page answers it.** Facts such as exact labels, which controls exist, where the logo is, what a menu holds
|
|
71
115
|
are answered by exploration (`/explore-case`; needs an environment and a logged-in profile; the agent only reads, a person does the steps that write). Rewrite the parent sentence
|
|
72
|
-
with the fact
|
|
116
|
+
with the fact and remove the block. Then decide the mark by "Which sentences carry a marker": a fact the run checks
|
|
117
|
+
gets `(seen on <profile>)`, a fact that would pass while wrong gets
|
|
118
|
+
`(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`.
|
|
73
119
|
- A check with no expected value ("check X", "can see Y") becomes: the observed behaviour, marked as observed.
|
|
74
120
|
A spec may be generated from it.
|
|
75
121
|
- Values that a check asserts (logo name, label, option list) are observed per profile and written into the case's
|
|
@@ -101,13 +147,19 @@ answer, adds `(page shows <x> on <profile-id>)` and reports it as a finding. It
|
|
|
101
147
|
|
|
102
148
|
## Final report
|
|
103
149
|
Report in the user's language. List every open block with its question and the parent sentence it would confirm, so
|
|
104
|
-
the user can see what deleting it accepts.
|
|
150
|
+
the user can see what deleting it accepts. List the markers separately, each with its reason and whether it changes an
|
|
151
|
+
assertion: that is the short list a person actually has to look at. The last line of the final message is plain text
|
|
152
|
+
and nothing else, exactly one of:
|
|
105
153
|
|
|
106
154
|
```
|
|
107
155
|
RESULT: ready
|
|
108
156
|
RESULT: needs-input
|
|
109
157
|
```
|
|
110
158
|
|
|
159
|
+
Never put that line in a code fence or inline code, never make it bold, a heading or a list item, and put no label,
|
|
160
|
+
number or emoji in front of it: the panel reads it to show how the run ended. (The block above only lists the two
|
|
161
|
+
tokens; write the one that applies as the last line of your message, not the block.)
|
|
162
|
+
|
|
111
163
|
- `ready`: no `[blocking]` question remains. Non-blocking ones may remain; list them so QA can confirm them.
|
|
112
164
|
- `needs-input`: at least one `[blocking]` question remains in the case file (list each with its parent sentence),
|
|
113
165
|
or the work could not be done (no environment, nobody logged in, or no permission to write files: a plan-mode run,
|
|
@@ -22,9 +22,10 @@ A check for something **missing, empty or hidden** is only valid after a positiv
|
|
|
22
22
|
means: the list finished loading and has rows, and none is X.
|
|
23
23
|
|
|
24
24
|
## Exploring
|
|
25
|
-
- Know the key requests: after the action, call `browser_network_requests
|
|
26
|
-
app's API, the ones that return the content). Wait until they are finished
|
|
27
|
-
|
|
25
|
+
- Know the key requests: after the action, call `browser_network_requests` (`static: false`), pick the data calls
|
|
26
|
+
(XHR/fetch to the app's API, the ones that return the content). Wait until they are finished. `browser_network_clear`
|
|
27
|
+
is not runnable in this MCP, so take a marker instead: read the list before the action, keep the highest number, and
|
|
28
|
+
after the action only the entries above it belong to it (`tools.md`, "Network requests without a clear").
|
|
28
29
|
- Then wait for the content signal (`browser_wait_for` with text, or `browser_find` that the content is there), and
|
|
29
30
|
take the snapshot only then. A snapshot taken right after a click is a frame, not a fact.
|
|
30
31
|
- Observations record the ready signal: `(observed after <signal>, ...)`. A fact with no signal is not recorded.
|
|
@@ -62,8 +63,9 @@ await expectImageLoaded(logo);
|
|
|
62
63
|
|
|
63
64
|
- Register the response wait **before** the action (`actAndWait` does).
|
|
64
65
|
- Every action that opens a dialog, a tab, a menu with data, a list or a preview waits for its key requests and its content
|
|
65
|
-
signal. The request URL patterns come from `
|
|
66
|
-
seen (the action was not run), say
|
|
66
|
+
signal. The request URL patterns come from the exploration note (`notes.md`) or, when it holds none, from
|
|
67
|
+
`browser_network_requests` during this replay; never from guesses. If none was seen (the action was not run), say
|
|
68
|
+
"not verified on the page" in the report.
|
|
67
69
|
- Never `waitForTimeout`, never `networkidle`. They wait for time or for silence, not for the content. `pnpm spec:lint` fails both.
|
|
68
70
|
- Never `if (await x.isVisible()) ...` to decide something that may not have loaded yet. Wait for a ready signal, then branch.
|
|
69
71
|
Where an element is genuinely optional, say what proves the page finished (`// ready: ...`) before looking for it.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# The browser tools this MCP can run, and the ones it only lists
|
|
2
|
+
|
|
3
|
+
Shared by `exploring-cases`, `generating-specs` and healing. It is binding in all three: read it before the first
|
|
4
|
+
browser call of a run.
|
|
5
|
+
|
|
6
|
+
`playwright run-test-mcp-server` answers `tools/list` with **every** browser tool Playwright has, but a call runs
|
|
7
|
+
inside a paused test, and that backend installs one capability set:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const config = { capabilities: ["testing"] }; // playwright/lib/mcp/test/browserBackend.js
|
|
11
|
+
const toolList = tools.filteredTools(config); // core* + testing, minus `skillOnly`
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Everything outside that set is listed and still cannot run: it answers `Tool "<name>" not found`. Two groups:
|
|
15
|
+
- a capability other than `core`/`testing` (`devtools`, `storage`, `network`, `pdf`, `vision`, `config`);
|
|
16
|
+
- `skillOnly: true` tools, which the backend never installs (`browser_network_clear`, `browser_console_clear`).
|
|
17
|
+
|
|
18
|
+
**`Tool "<name>" not found` is not a broken MCP server.** The server is up and every other tool works. Never stop
|
|
19
|
+
with `RESULT: needs-input` for it, never report a missing MCP server, and never fall back to an ad-hoc Playwright
|
|
20
|
+
script (it sees a different browser and its network view is not `browser_network_requests`, see A2). Use the
|
|
21
|
+
substitute below instead, and say in the report which substitute you used.
|
|
22
|
+
|
|
23
|
+
`pnpm asrun doctor` prints how many tools the installed Playwright actually runs and warns when this file names one
|
|
24
|
+
it cannot (it happens when Playwright changes the filter; the file then needs the same edit).
|
|
25
|
+
|
|
26
|
+
## Starting a session
|
|
27
|
+
|
|
28
|
+
Both `exploring-cases` and `generating-specs` (and healing) drive the browser through this MCP, one profile at a time.
|
|
29
|
+
|
|
30
|
+
1. Check the session with `pnpm status --env <env>` (or `pnpm status` when the environment is the profile's). It can fail
|
|
31
|
+
with a sandbox error (`listen EPERM`, a Chromium start failure): then open the profile's page and judge by the login
|
|
32
|
+
page instead.
|
|
33
|
+
2. Start the session with `generator_setup_page` (`project=<profile-id>`, `seedFile=tests/seed.spec.ts`, `plan=""`).
|
|
34
|
+
Every later call belongs to that profile.
|
|
35
|
+
3. A profile that is not logged in is skipped and named in the report; a human runs `pnpm signin <profile-id>`. Never
|
|
36
|
+
explore a profile a selection leaves out (`--profiles` in exploration), and never add one to fill a gap.
|
|
37
|
+
|
|
38
|
+
If no `browser_*` / `generator_*` tool is registered at all, or the server reports that it failed to start, stop: tell
|
|
39
|
+
the user which MCP server is missing and to run `pnpm asrun doctor`, write nothing, and end with `RESULT: needs-input`.
|
|
40
|
+
|
|
41
|
+
## Short calls
|
|
42
|
+
|
|
43
|
+
A call that hangs is the normal failure mode of this MCP, so keep every call small.
|
|
44
|
+
|
|
45
|
+
- One `browser_run_code_unsafe` call does one thing and finishes within about 60 seconds. Do not close a tab, click,
|
|
46
|
+
wait for a dialog and read it in one call: split it and take a `browser_snapshot` between the steps. Only the handoff
|
|
47
|
+
wait loop may take minutes.
|
|
48
|
+
- The browser times out an action or a lookup after 15 s (the config sets it). Pass `{timeout: 5000}` when you wait for
|
|
49
|
+
something that may not appear, and use `.catch()` or `isVisible()` for a one-time look **while exploring only**.
|
|
50
|
+
`isVisible()` answers for this instant and ignores any `timeout` passed to it, so take a snapshot or wait for a signal
|
|
51
|
+
after an action first. Never copy this pattern into a spec (`readiness.md`, "Optional confirmations").
|
|
52
|
+
- After a click that should open something, take a snapshot and see what opened before you wait for it by role. A dialog
|
|
53
|
+
that did not appear is a finding (what did open instead?), not something to wait for or retry.
|
|
54
|
+
- A step that fails or times out twice is not tried a third time: record what you saw and go on (a `TODO [blocking]`
|
|
55
|
+
under the step in a case, a `needs-input` report when writing a spec).
|
|
56
|
+
|
|
57
|
+
## Runnable
|
|
58
|
+
|
|
59
|
+
Navigation and reading:
|
|
60
|
+
`browser_navigate`, `browser_navigate_back`, `browser_snapshot`, `browser_find`, `browser_evaluate`,
|
|
61
|
+
`browser_run_code_unsafe`, `browser_tabs`, `browser_resize`, `browser_close`, `browser_handle_dialog`,
|
|
62
|
+
`browser_console_messages`, `browser_emulate_media`, `browser_wait_for`.
|
|
63
|
+
|
|
64
|
+
Interaction: `browser_click`, `browser_type`, `browser_fill_form`, `browser_press_key`, `browser_hover`,
|
|
65
|
+
`browser_drag`, `browser_drop`, `browser_select_option`, `browser_file_upload`.
|
|
66
|
+
|
|
67
|
+
Screenshots: `browser_take_screenshot`.
|
|
68
|
+
|
|
69
|
+
Network: `browser_network_requests`, `browser_network_request`.
|
|
70
|
+
|
|
71
|
+
Test helpers: `browser_generate_locator`, `browser_verify_element_visible`, `browser_verify_text_visible`,
|
|
72
|
+
`browser_verify_list_visible`, `browser_verify_value`.
|
|
73
|
+
|
|
74
|
+
`browser_run_code_unsafe` takes Playwright's own API (a `page`, `context`, `expect` are in scope), which is what
|
|
75
|
+
every substitute below uses.
|
|
76
|
+
|
|
77
|
+
## Listed but not runnable
|
|
78
|
+
|
|
79
|
+
| Tool | Why | Do this instead |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `browser_highlight`, `browser_hide_highlight` | `devtools` | [Outline a target](#outlining-a-target-without-browser_highlight) |
|
|
82
|
+
| `browser_network_clear` | `skillOnly` | [Network requests without a clear](#network-requests-without-a-clear) |
|
|
83
|
+
| `browser_console_clear` | `skillOnly` | read `browser_console_messages` and note the messages you already saw |
|
|
84
|
+
| `browser_start_recording`, `browser_stop_recording` | `devtools` | the handoff card (`user-handoff.md`, "Recording") |
|
|
85
|
+
| `browser_annotate` | `devtools` | let the person name the element in the handoff card, then outline it yourself |
|
|
86
|
+
| `browser_start/stop_tracing`, `browser_start/stop_video`, `browser_video_*`, `browser_resume` | `devtools` | not needed by a case |
|
|
87
|
+
| `browser_cookie_*`, `browser_localstorage_*`, `browser_sessionstorage_*`, `browser_storage_state`, `browser_set_storage_state` | `storage` | never: saved logins are passwords (F5) |
|
|
88
|
+
| `browser_route`, `browser_unroute`, `browser_route_list`, `browser_network_state_set` | `network` | never: a case checks the real page |
|
|
89
|
+
| `browser_pdf_save` | `pdf` | a page that serves a PDF is downloaded and rendered (`pnpm pdf:render`) |
|
|
90
|
+
| `browser_mouse_move_xy`, `browser_mouse_down`, `browser_mouse_up`, `browser_mouse_wheel`, `browser_mouse_click_xy`, `browser_mouse_drag_xy` | `vision` | `browser_click` with a locator |
|
|
91
|
+
| `browser_get_config` | `config` | `pnpm status --env <env>` |
|
|
92
|
+
|
|
93
|
+
## Arguments the schema requires
|
|
94
|
+
|
|
95
|
+
A call that looks complete still fails when one of these is missing. Both come from the MCP's own schema, and both
|
|
96
|
+
answer `Missing key`:
|
|
97
|
+
|
|
98
|
+
1. **`intent` on every action, input and assertion tool.** The test runner adds it so the recorded step has a name.
|
|
99
|
+
One short sentence, imperative, about what this call is for: `intent: "open the article preview"`. It belongs on
|
|
100
|
+
`browser_navigate`, `browser_navigate_back`, `browser_click`, `browser_type`, `browser_fill_form`,
|
|
101
|
+
`browser_press_key`, `browser_hover`, `browser_drag`, `browser_drop`, `browser_select_option`,
|
|
102
|
+
`browser_file_upload`, `browser_evaluate`, `browser_run_code_unsafe`, `browser_tabs`, `browser_close`,
|
|
103
|
+
`browser_resize`, `browser_handle_dialog`, `browser_emulate_media`, and on `browser_wait_for` and every
|
|
104
|
+
`browser_verify_*`. The read-only tools (`browser_snapshot`, `browser_find`, `browser_take_screenshot`,
|
|
105
|
+
`browser_console_messages`, `browser_network_requests`, `browser_network_request`, `browser_generate_locator`) take
|
|
106
|
+
none.
|
|
107
|
+
2. **A field with a default is published as required:**
|
|
108
|
+
- `browser_take_screenshot` → `scale: "css"` (CSS pixels, the size baselines expect).
|
|
109
|
+
- `browser_network_requests` → `static: false` (data calls only).
|
|
110
|
+
- `browser_network_request` → `index` (from the numbered list).
|
|
111
|
+
- `browser_tabs` → `action` (`"list"`, `"new"`, `"select"` or `"close"`).
|
|
112
|
+
- `browser_console_messages` → `level` (`"error"` is enough).
|
|
113
|
+
- `browser_handle_dialog` → `accept` (`true` to accept, `false` to dismiss).
|
|
114
|
+
- `generator_setup_page` → `project`, `seedFile` and `plan` (`plan: ""` when there is no plan).
|
|
115
|
+
|
|
116
|
+
## Outlining a target without `browser_highlight`
|
|
117
|
+
|
|
118
|
+
`browser_highlight` is a one-line wrapper over `locator.highlight()`, and that method is public Playwright API. The
|
|
119
|
+
overlay is the real one, not an approximation, and nothing of the page is changed.
|
|
120
|
+
|
|
121
|
+
1. Outline (one call, one element, the card's `Identified by` as the locator):
|
|
122
|
+
```js
|
|
123
|
+
// browser_run_code_unsafe({intent: 'outline the target for the reference screenshot', code: ...})
|
|
124
|
+
async (page) => {
|
|
125
|
+
const target = page.getByRole('img', {name: '<the Identified by value of the card>'});
|
|
126
|
+
await target.highlight({style: 'outline: 3px solid #e11d48; outline-offset: 2px'});
|
|
127
|
+
return await target.toString();
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
2. Screenshot the page (not the element) so the reader sees where it sits:
|
|
131
|
+
`browser_take_screenshot({scale: 'css', filename: '.explore/targets/<case-id>/T<n>-<profile-id>.png'})`
|
|
132
|
+
3. Remove it before the next step: `browser_run_code_unsafe` with `intent: 'remove the highlight overlay'` and
|
|
133
|
+
`async (page) => { await page.hideHighlight(); }`.
|
|
134
|
+
|
|
135
|
+
A highlight overlay is not page state: it never appears in a snapshot, it is not part of what the case checks, and
|
|
136
|
+
`page.hideHighlight()` clears all of them. Do this for every candidate when several elements are compared, and say in
|
|
137
|
+
the card what the screenshot shows.
|
|
138
|
+
|
|
139
|
+
## Network requests without a clear
|
|
140
|
+
|
|
141
|
+
`browser_network_requests` returns a numbered list that only grows while the page stays open, so a marker works:
|
|
142
|
+
|
|
143
|
+
1. Before the action: `browser_network_requests({static: false})`, note the highest number in the list (`mark`).
|
|
144
|
+
2. Do the action.
|
|
145
|
+
3. Read `browser_network_requests({static: false})` again and take the entries above `mark`: those, and only those,
|
|
146
|
+
were started by the action. `browser_network_request({index})` gives one request's detail and status.
|
|
147
|
+
|
|
148
|
+
Record the marker as a plain note in the report (`after 12 new requests; the data call was 7`), never in the case.
|
|
149
|
+
|
|
150
|
+
4. While exploring, write the data-call paths of the action into the profile's exploration note (`notes.md`): path only,
|
|
151
|
+
no host, no query string. `/gen-spec` turns them into the `responses` of `actAndWait`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# User handoff: a person works in the browser, the agent waits
|
|
2
2
|
|
|
3
3
|
Used by `exploring-cases` (and by `making-cases` never: it does not open a browser). `generating-specs` never hands
|
|
4
|
-
off; it replays the step recipes that an exploration recorded.
|
|
4
|
+
off; it replays the step recipes and the exploration note (`notes.md`) that an exploration recorded.
|
|
5
5
|
|
|
6
6
|
## The rule
|
|
7
7
|
**An agent never writes data.** Not in staging, not in production. Every step that creates, edits, publishes, shares,
|
|
@@ -29,7 +29,9 @@ that nobody watches.
|
|
|
29
29
|
copied with one click.
|
|
30
30
|
3. Never ask the user to send email (production: never; other environments: only when `email` is `true` for the
|
|
31
31
|
profile). Never ask for login or 2FA. Name everything the user creates `[asrun] <something>`.
|
|
32
|
-
4.
|
|
32
|
+
4. Do not start a recording yourself: the card starts and stops it (see "Recording"). Never call
|
|
33
|
+
`browser_start_recording` or `browser_stop_recording`: the `playwright-test` MCP lists them but cannot run them
|
|
34
|
+
(`devtools` tools; see `tools.md`).
|
|
33
35
|
|
|
34
36
|
## The wait loop
|
|
35
37
|
Run it with one `browser_run_code_unsafe` call. A single call may wait for minutes (verified: 150 s without a
|
|
@@ -63,8 +65,9 @@ inside the call that runs the card: OpenCode reports a tool call only when it en
|
|
|
63
65
|
way the web UI can see that a person is needed while the card waits. With the card alone there is no "Your turn"
|
|
64
66
|
badge, no count in the tab title and no notification (a HINT in the job log says so).
|
|
65
67
|
|
|
66
|
-
1. Set the options on the browser context (this call only sets data):
|
|
68
|
+
1. Set the options on the browser context (this call only sets data; `browser_run_code_unsafe` needs `intent`):
|
|
67
69
|
```js
|
|
70
|
+
// browser_run_code_unsafe({intent: 'set the handoff card options', code: ...})
|
|
68
71
|
async (page) => {
|
|
69
72
|
page.context().__handoffCfg = {
|
|
70
73
|
message: '<what the user does, one sentence>',
|
|
@@ -77,11 +80,16 @@ badge, no count in the tab title and no notification (a HINT in the job log says
|
|
|
77
80
|
```
|
|
78
81
|
2. Run the card and wait: `browser_run_code_unsafe` with `filename` set to the absolute path of
|
|
79
82
|
`<framework>/.claude/skills/exploring-cases/references/handoff-card.js` (use the absolute path of the repository, a
|
|
80
|
-
relative one is resolved against the workspace root, which may be the parent folder)
|
|
83
|
+
relative one is resolved against the workspace root, which may be the parent folder) and an `intent` such as
|
|
84
|
+
`'hand off step 5 to the person'`. It returns
|
|
85
|
+
`{outcome: 'done' | 'skip' | 'condition' | 'timeout', waitedMs, recording, recorded}`: `recorded` holds the
|
|
86
|
+
actions the person performed, one Playwright line each, across every tab (`page`, `page1`, ...), and is empty when
|
|
87
|
+
the recording was unavailable (`recording: false`; `recordingError` says why).
|
|
81
88
|
|
|
82
89
|
Check once, after the card shows, that it looks right: take a screenshot and see a title bar with a move icon, a
|
|
83
|
-
chevron button at its right and, when `copy` is not empty, a line per text with a copy icon.
|
|
84
|
-
|
|
90
|
+
chevron button at its right and, when `copy` is not empty, a line per text with a copy icon. Playwright's own recording
|
|
91
|
+
toolbar at the top of the page is expected and is not part of the card. If the card is not right, stop and report it;
|
|
92
|
+
do not hand off with a broken card.
|
|
85
93
|
|
|
86
94
|
Tell the user in the chat (the terminal or the UI job log) as well: "Your turn in the browser window: <task>. Press
|
|
87
95
|
Done on the card (bottom-left; drag it if it is in the way) when you finish." When the run was started from the web
|
|
@@ -105,17 +113,21 @@ The Done button is always there (also on the minimized card) as the fallback, an
|
|
|
105
113
|
for two seconds before it is removed, so the user knows when they may let go.
|
|
106
114
|
- `outcome: 'timeout'` or `'skip'`: do not guess. Write a `TODO [blocking]` under the step and go on with what does
|
|
107
115
|
not depend on it.
|
|
108
|
-
- Take a snapshot
|
|
116
|
+
- Take a snapshot, write down what the user did from the card's `recorded` (see "Recording"), then continue read-only.
|
|
109
117
|
- At the end of the exploration hand off once more when the user created data: "Delete the [asrun] items you
|
|
110
118
|
created, then press Done". The case must clean up after itself, and so must the exploration.
|
|
111
119
|
|
|
112
120
|
## Recording
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
The card records what the person does: it starts the recording when it shows and stops it on every way out (Done,
|
|
122
|
+
"Cannot do this", the finish condition, the timeout, an error), in the same `browser_run_code_unsafe` call. The agent
|
|
123
|
+
neither starts nor stops it. The result is the card's `recorded`: one Playwright line per action, `page` for the first
|
|
124
|
+
tab that acted and `page1`, `page2`, ... for the others. The aliases follow the order in which actions first appear,
|
|
125
|
+
not the order in which the tabs were opened, so read the actions before you describe tabs.
|
|
126
|
+
|
|
127
|
+
The recorded Playwright code is **raw material, never saved as it is**. It is turned into a general recipe and written
|
|
128
|
+
under the step as `- Recipe (phase): ...` lines (format below). The recipe holds what the user did in words, plus the
|
|
129
|
+
**labels and roles of the controls** they used ("the textbox 'Title'", "the button 'Next'"), because `/gen-spec` cannot
|
|
130
|
+
write data and so cannot see those controls itself. It never holds selectors, ids or the names of data that can change.
|
|
119
131
|
|
|
120
132
|
Make the recipe as general as possible, because lists and data change:
|
|
121
133
|
- **A choice from a list that can change** (items, search results, a contact list): record the position, never the
|
|
@@ -123,14 +135,31 @@ Make the recipe as general as possible, because lists and data change:
|
|
|
123
135
|
- **Random is allowed only when the user asks for it** ("pick any 3"). The default is the position, because it can be
|
|
124
136
|
repeated. A random pick must be written into the test's annotations when the spec runs.
|
|
125
137
|
- **Text the case creates**: `[asrun] <something>`.
|
|
126
|
-
- **A value that has to exist** (an existing name, a search term):
|
|
127
|
-
returns a result") and the observed value as `(
|
|
128
|
-
|
|
138
|
+
- **A value that has to exist** (an existing name, a search term): the recipe says what it must be like ("a name
|
|
139
|
+
that returns a result") and notes the observed value as plain text `(seen on <profile>: "<value>")`. Do not put a
|
|
140
|
+
brittle literal in as the only truth. One claim carries one signal: when a `[non-blocking]` TODO under the recipe
|
|
141
|
+
asks whether any such value is acceptable, the observed value gets **no** marker (`open-questions.md`, "Marker or
|
|
142
|
+
question, never both").
|
|
129
143
|
- **Nothing that depends on the pick** may become an assertion (a total changes with the items). Say so in a
|
|
130
144
|
TODO when a check would.
|
|
131
145
|
- Settings that the user changed on purpose (a toggle, a template) are recorded as they were set, by label.
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
The recording is raw: merge and prune it before you write the recipe.
|
|
148
|
+
- **The same action can appear twice**, and clicking a checkbox or its label can give both a `click` and a `check`.
|
|
149
|
+
Write it once ("check 'Agree'").
|
|
150
|
+
- **Side products**: clicking a textbox to focus it, `Cmd+C`, `Scroll`, `Escape`. Keep only actions that change data
|
|
151
|
+
or the page's state.
|
|
152
|
+
- **The card's own buttons are filtered out** of `recorded`. A click on the card's text (not a button) can still show
|
|
153
|
+
up; ignore it.
|
|
154
|
+
- **Not recorded at all**: drag and drop, native file pickers, OS dialogs. Describe those from before/after snapshots.
|
|
155
|
+
|
|
156
|
+
If `recording` is `false` or `recorded` is empty, do not silently write a recipe from snapshots: write a
|
|
157
|
+
`🚫 TODO [blocking]` under the step that says the step was not recorded (and why, from `recordingError` when there is
|
|
158
|
+
one). A recipe from before/after snapshots may be added next to that block only, and must say that it came from
|
|
159
|
+
snapshots, not from a recording.
|
|
160
|
+
|
|
161
|
+
If Playwright's own recording toolbar shows at the top of the page while the card waits, it is not AsRun's: leave it
|
|
162
|
+
alone.
|
|
134
163
|
|
|
135
164
|
## Recipe lines under a step
|
|
136
165
|
One or more `- Recipe (phase): ...` lines directly under the `[input]` or `[writes]` step they belong to, indented by
|
|
@@ -139,7 +168,7 @@ keep in sync.
|
|
|
139
168
|
|
|
140
169
|
```
|
|
141
170
|
3. Select an existing document and pick 3 items. [input]
|
|
142
|
-
- Recipe (start): search for an existing document and pick result #1 (
|
|
171
|
+
- Recipe (start): search for an existing document and pick result #1 (seen on example-staging: "quarterly report").
|
|
143
172
|
- ❓ TODO [non-blocking]: Is any result that opens fine as the starting point? (seen on example-staging)
|
|
144
173
|
- ✍️ Answer:
|
|
145
174
|
- Recipe (items): select 3 items by position (#1 to #3) from the list.
|