explorbot 0.2.3 → 0.2.4
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/README.md +1 -1
- package/bin/explorbot-cli.ts +7 -1
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +115 -86
- package/boat/prima/src/envelope.ts +80 -52
- package/boat/prima/src/prima.ts +547 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +7 -1
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +97 -77
- package/dist/boat/prima/src/envelope.js +70 -44
- package/dist/boat/prima/src/prima.js +515 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +36 -1
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +7 -0
- package/dist/src/ai/navigator.js +10 -6
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +33 -33
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +13 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +3 -1
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +38 -2
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +14 -8
- package/src/ai/pilot.ts +53 -2
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +32 -33
- package/src/ai/tools.ts +132 -36
- package/src/config.ts +15 -4
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +4 -2
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -2,81 +2,63 @@ import { Command } from 'commander';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
4
4
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from "./activity-line.js";
|
|
6
7
|
import { renderEnvelope } from "./envelope.js";
|
|
7
8
|
import { Prima } from "./prima.js";
|
|
8
9
|
const helpContract = dedent `
|
|
9
|
-
Prima
|
|
10
|
-
prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
|
|
10
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
16
|
-
click / fill One action described in words; AI resolves it on the current page.
|
|
17
|
-
prima click "the primary action button in the header"
|
|
18
|
-
prima fill "the search box" "a search term"
|
|
19
|
-
do <steps...> Several described steps, run tester-style in one process.
|
|
20
|
-
prima do "open the account menu" "choose the settings entry"
|
|
21
|
-
Never pass a locator or a function expression to click/fill/do - describe the target.
|
|
22
|
-
Never pass a description to pw - it takes executable code only.
|
|
12
|
+
playwright-cli open <url> starts the session
|
|
13
|
+
prima <command> ... drives it
|
|
14
|
+
playwright-cli close ends it
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
prima go <url|path|words> reach the page you want to work on
|
|
26
|
-
prima research once per new page; returns verified locators
|
|
27
|
-
prima pw "..." drive the page with those locators
|
|
28
|
-
prima verify "..." assert the outcome (prima ask "..." to inspect instead)
|
|
29
|
-
Fall back to click/fill/do whenever research left you no locator to hold.
|
|
16
|
+
One call takes a whole job:
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
bin; from source under Bun the connection does not open.
|
|
67
|
-
When no AI model is usable pw still works; for everything else drive
|
|
68
|
-
playwright-cli directly.
|
|
69
|
-
Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
|
|
70
|
-
you pass.
|
|
18
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
19
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
20
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
21
|
+
`;
|
|
22
|
+
const checkHelp = dedent `
|
|
23
|
+
check takes an outcome rather than a click path, and works out how to reach it.
|
|
24
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
25
|
+
scenario text is the single expected outcome. Each comes back under
|
|
26
|
+
### Expected outcomes as PASSED, FAILED or not verified - "not verified"
|
|
27
|
+
means the run never checked it, which is not the same as false.
|
|
28
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
29
|
+
`;
|
|
30
|
+
const doHelp = dedent `
|
|
31
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
|
|
32
|
+
with what proved it. One that could not be carried out fails the command and says why.
|
|
33
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
34
|
+
what makes this tier cheap.
|
|
35
|
+
`;
|
|
36
|
+
const verifyHelp = dedent `
|
|
37
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
38
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
39
|
+
could not be expressed, which is not the same as false.
|
|
40
|
+
`;
|
|
41
|
+
const reportHelp = dedent `
|
|
42
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
43
|
+
The most recent session is reported unless --pw-session names another.
|
|
44
|
+
`;
|
|
45
|
+
const sessionHelp = dedent `
|
|
46
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
47
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
48
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
49
|
+
attached, since the attached session keeps its own
|
|
50
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
51
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
52
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
71
53
|
`;
|
|
72
|
-
|
|
54
|
+
let rootOptions = () => ({});
|
|
55
|
+
function buildOptions(subcommand) {
|
|
56
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
73
57
|
return {
|
|
74
|
-
verbose: options.verbose || options.debug,
|
|
75
58
|
config: options.config,
|
|
76
59
|
path: options.path,
|
|
77
60
|
instance: options.instance,
|
|
78
61
|
session: options.session,
|
|
79
|
-
heal: options.heal,
|
|
80
62
|
ephemeral: options.ephemeral,
|
|
81
63
|
framework: options.framework,
|
|
82
64
|
noVision: options.vision === false,
|
|
@@ -88,29 +70,38 @@ function buildOptions(options) {
|
|
|
88
70
|
pwSession: options.pwSession,
|
|
89
71
|
};
|
|
90
72
|
}
|
|
73
|
+
function stripEmpty(options) {
|
|
74
|
+
const present = {};
|
|
75
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
present[key] = value;
|
|
79
|
+
}
|
|
80
|
+
return present;
|
|
81
|
+
}
|
|
91
82
|
function addCommonOptions(cmd) {
|
|
92
83
|
return cmd
|
|
93
|
-
.option('-v, --verbose', 'Enable verbose logging')
|
|
94
|
-
.option('--debug', 'Enable debug logging (same as --verbose)')
|
|
95
84
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
96
85
|
.option('-p, --path <path>', 'Working directory path')
|
|
97
86
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
98
87
|
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
99
|
-
.option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
|
|
100
88
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
101
89
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
102
90
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
103
91
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
104
|
-
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
92
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
93
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
105
94
|
}
|
|
106
95
|
function primaFor(options) {
|
|
107
|
-
setPreserveConsoleLogs(true);
|
|
108
96
|
if (options.ephemeral)
|
|
109
97
|
process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
110
98
|
return new Prima(buildOptions(options));
|
|
111
99
|
}
|
|
112
|
-
async function runPrima(options, command, run) {
|
|
100
|
+
async function runPrima(options, command, run, record = true) {
|
|
101
|
+
setQuietMode(!isVerboseMode());
|
|
102
|
+
trackActivityLine();
|
|
113
103
|
const prima = primaFor(options);
|
|
104
|
+
const startedAt = Date.now();
|
|
114
105
|
let envelope;
|
|
115
106
|
try {
|
|
116
107
|
await prima.start();
|
|
@@ -119,6 +110,9 @@ async function runPrima(options, command, run) {
|
|
|
119
110
|
catch (error) {
|
|
120
111
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
121
112
|
}
|
|
113
|
+
if (record)
|
|
114
|
+
prima.record(envelope, Date.now() - startedAt);
|
|
115
|
+
clearActivityLine();
|
|
122
116
|
console.log(renderEnvelope(envelope));
|
|
123
117
|
await prima.stop().catch(() => { });
|
|
124
118
|
process.exit(envelope.ok ? 0 : 1);
|
|
@@ -136,24 +130,31 @@ async function runBrowser(options, run) {
|
|
|
136
130
|
}
|
|
137
131
|
export function createPrimaCommands(name = 'prima') {
|
|
138
132
|
const cmd = new Command(name);
|
|
139
|
-
cmd.description('
|
|
133
|
+
cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
|
|
134
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
135
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
140
136
|
cmd.addHelpText('after', `\n${helpContract}`);
|
|
137
|
+
rootOptions = () => cmd.opts();
|
|
141
138
|
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
142
139
|
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
143
140
|
});
|
|
144
|
-
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
141
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
142
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
143
|
+
.action(async (instructions, options) => {
|
|
145
144
|
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
146
145
|
});
|
|
147
|
-
addCommonOptions(cmd.command('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await runPrima(options, `
|
|
146
|
+
addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
|
|
147
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
|
|
148
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
149
|
+
.action(async (scenario, options) => {
|
|
150
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
152
151
|
});
|
|
153
152
|
addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
|
|
154
153
|
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
155
154
|
});
|
|
156
|
-
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
155
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
156
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
157
|
+
.action(async (assertion, options) => {
|
|
157
158
|
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
158
159
|
});
|
|
159
160
|
addCommonOptions(cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')).action(async (options) => {
|
|
@@ -164,6 +165,25 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
164
165
|
options.baseUrl = target;
|
|
165
166
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
166
167
|
});
|
|
168
|
+
addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
|
|
169
|
+
setQuietMode(!isVerboseMode());
|
|
170
|
+
const prima = primaFor(options);
|
|
171
|
+
console.log(await prima.config().catch((error) => browserErrorMessage(error)));
|
|
172
|
+
await prima.stop().catch(() => { });
|
|
173
|
+
process.exit(0);
|
|
174
|
+
});
|
|
175
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
176
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
177
|
+
});
|
|
178
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
179
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
setQuietMode(!isVerboseMode());
|
|
182
|
+
console.log(await primaFor(options)
|
|
183
|
+
.report()
|
|
184
|
+
.catch((error) => browserErrorMessage(error)));
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
167
187
|
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
168
188
|
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
169
189
|
.option('-s, --show', 'Launch the browser in a visible window')
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
const EXPECTATION_LABELS = {
|
|
4
|
+
passed: 'PASSED ',
|
|
5
|
+
failed: 'FAILED ',
|
|
6
|
+
unverified: 'not verified',
|
|
7
|
+
};
|
|
3
8
|
export function renderEnvelope(data) {
|
|
4
|
-
const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data
|
|
9
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
5
10
|
return sections.filter((section) => section).join('\n\n');
|
|
6
11
|
}
|
|
7
12
|
export function writeArtifacts(dir, snapshot) {
|
|
@@ -9,29 +14,21 @@ export function writeArtifacts(dir, snapshot) {
|
|
|
9
14
|
const paths = {
|
|
10
15
|
aria: path.resolve(dir, 'aria.yml'),
|
|
11
16
|
html: path.resolve(dir, 'page.html'),
|
|
12
|
-
network: path.resolve(dir, 'network.jsonl'),
|
|
13
17
|
};
|
|
14
18
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
15
19
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
20
|
+
if (!snapshot.requests.length)
|
|
21
|
+
return paths;
|
|
22
|
+
paths.network = path.resolve(dir, 'network.jsonl');
|
|
16
23
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
17
24
|
return paths;
|
|
18
25
|
}
|
|
19
26
|
function renderResult(data) {
|
|
20
27
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
21
|
-
const healed = renderHealed(data);
|
|
22
|
-
if (healed)
|
|
23
|
-
lines.push(healed);
|
|
24
28
|
if (data.used?.length)
|
|
25
29
|
lines.push(`used: ${data.used.join('; ')}`);
|
|
26
30
|
return section('Result', lines.join('\n'));
|
|
27
31
|
}
|
|
28
|
-
function renderHealed(data) {
|
|
29
|
-
if (data.healed === undefined)
|
|
30
|
-
return null;
|
|
31
|
-
if (data.healNote)
|
|
32
|
-
return `healed: ${data.healed} (${data.healNote})`;
|
|
33
|
-
return `healed: ${data.healed}`;
|
|
34
|
-
}
|
|
35
32
|
function renderPage(data) {
|
|
36
33
|
const { url, previousUrl, title, state, visits } = data.page;
|
|
37
34
|
const urlLabel = `url: ${url}`;
|
|
@@ -43,67 +40,96 @@ function renderPage(data) {
|
|
|
43
40
|
const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
|
|
44
41
|
return section('Page', lines.join('\n'));
|
|
45
42
|
}
|
|
43
|
+
function renderValue(data) {
|
|
44
|
+
if (data.value === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
return section('Value', data.value);
|
|
47
|
+
}
|
|
48
|
+
function renderChanges(data) {
|
|
49
|
+
if (data.changes === undefined || data.changes === null)
|
|
50
|
+
return null;
|
|
51
|
+
return section('Changes', data.changes);
|
|
52
|
+
}
|
|
53
|
+
function renderSteps(data) {
|
|
54
|
+
if (!data.steps?.length)
|
|
55
|
+
return null;
|
|
56
|
+
const lines = [];
|
|
57
|
+
data.steps.forEach((step, index) => {
|
|
58
|
+
lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
|
|
59
|
+
for (const line of (step.proof || '').split('\n').filter(Boolean))
|
|
60
|
+
lines.push(` ${line}`);
|
|
61
|
+
});
|
|
62
|
+
if (data.stepFiles)
|
|
63
|
+
lines.push('', `page after each step: ${data.stepFiles}`);
|
|
64
|
+
return section('Steps', lines.join('\n'));
|
|
65
|
+
}
|
|
66
|
+
function renderExpectations(data) {
|
|
67
|
+
if (!data.expectations?.length)
|
|
68
|
+
return null;
|
|
69
|
+
const lines = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
70
|
+
return section('Expected outcomes', lines.join('\n'));
|
|
71
|
+
}
|
|
46
72
|
function renderOutcome(data) {
|
|
47
|
-
if (data.changes)
|
|
48
|
-
return section('Changes', data.changes);
|
|
49
73
|
if (data.answer)
|
|
50
74
|
return section('Answer', data.answer);
|
|
51
75
|
if (data.research)
|
|
52
76
|
return section('Research', data.research);
|
|
53
|
-
if (!data.
|
|
77
|
+
if (!data.assertions)
|
|
54
78
|
return null;
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
if (!data.assertions.length)
|
|
80
|
+
return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
|
|
81
|
+
const lines = data.assertions.map((assertion) => {
|
|
82
|
+
const code = assertion.code
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((line) => line.trim())
|
|
85
|
+
.filter((line) => line && !line.startsWith('//'))
|
|
86
|
+
.join(' ');
|
|
87
|
+
return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
|
|
88
|
+
});
|
|
89
|
+
const proof = data.assertions.flatMap((assertion) => assertion.proof);
|
|
90
|
+
if (proof.length)
|
|
91
|
+
lines.push('', 'playwright:', ...proof);
|
|
92
|
+
return section('Assertions', lines.join('\n'));
|
|
57
93
|
}
|
|
58
94
|
function renderFailure(data) {
|
|
59
95
|
if (!data.failure)
|
|
60
96
|
return [];
|
|
61
|
-
|
|
62
|
-
if (data.failure.reasoning)
|
|
63
|
-
lines.push(`reasoning: ${data.failure.reasoning}`);
|
|
64
|
-
return [section('Failure', lines.join('\n')), renderAttempts(data.failure.attempts), renderCompactAria(data.failure.compactAria)];
|
|
65
|
-
}
|
|
66
|
-
function renderAttempts(attempts) {
|
|
67
|
-
if (!attempts?.length)
|
|
68
|
-
return null;
|
|
69
|
-
const labels = attempts.map((attempt, index) => `${index + 1}. ${attempt.code}`);
|
|
70
|
-
const width = Math.max(...labels.map((label) => label.length)) + 3;
|
|
71
|
-
const lines = labels.map((label, index) => align(label, `→ ${attempts[index].outcome}`, width));
|
|
72
|
-
return section(`Healing attempts (${attempts.length})`, lines.join('\n'));
|
|
97
|
+
return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
|
|
73
98
|
}
|
|
74
99
|
function renderCompactAria(compactAria) {
|
|
75
100
|
if (!compactAria)
|
|
76
101
|
return null;
|
|
77
102
|
return section('Current page (compact ARIA)', compactAria);
|
|
78
103
|
}
|
|
79
|
-
function renderInstance(
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return others.join(', ');
|
|
104
|
+
function renderInstance(data) {
|
|
105
|
+
const instance = data.instance;
|
|
106
|
+
const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
|
|
107
|
+
if (instance.others.length)
|
|
108
|
+
parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
|
|
109
|
+
if (data.status)
|
|
110
|
+
parts.push(`details: prima status ${data.status}`);
|
|
111
|
+
return section('Instance', parts.join(' | '));
|
|
88
112
|
}
|
|
89
113
|
function browserLine(instance) {
|
|
90
114
|
if (instance.attached)
|
|
91
|
-
return `
|
|
115
|
+
return `attached to ${instance.attached}`;
|
|
92
116
|
if (instance.startedAgo)
|
|
93
|
-
return `
|
|
117
|
+
return `running, started ${instance.startedAgo} ago`;
|
|
94
118
|
if (instance.tabs > 0)
|
|
95
|
-
return '
|
|
96
|
-
return '
|
|
119
|
+
return 'running';
|
|
120
|
+
return 'not running';
|
|
97
121
|
}
|
|
98
122
|
function tabsLabel(tabs) {
|
|
99
123
|
if (tabs === 1)
|
|
100
124
|
return '1 tab';
|
|
101
125
|
return `${tabs} tabs`;
|
|
102
126
|
}
|
|
103
|
-
function renderArtifacts(data) {
|
|
127
|
+
export function renderArtifacts(data) {
|
|
104
128
|
if (!data.artifacts)
|
|
105
129
|
return null;
|
|
106
|
-
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}
|
|
130
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
131
|
+
if (data.artifacts.network)
|
|
132
|
+
lines.push(`network: ${data.artifacts.network}`);
|
|
107
133
|
return section('Artifacts', lines.join('\n'));
|
|
108
134
|
}
|
|
109
135
|
function align(label, marker, width) {
|