explorbot 0.2.2 → 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 +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- 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/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- 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/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- 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 +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- 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/cli-name.js +6 -2
- 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/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -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/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- 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/cli-name.ts +5 -2
- 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/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from '../../../src/application-spec-contract.ts';
|
|
2
4
|
import { type WebPageState } from '../../../src/state-manager.ts';
|
|
5
|
+
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
3
6
|
import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
|
|
4
7
|
import type { DocumentationScreenshot } from './screenshots.ts';
|
|
5
|
-
import {
|
|
6
|
-
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
8
|
+
import { type DocumentedPage, type SkippedPage, buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from './state-diagram.ts';
|
|
7
9
|
|
|
8
10
|
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
|
|
9
11
|
const lines: string[] = [];
|
|
@@ -33,6 +35,13 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const interactions = documentation.interactions;
|
|
38
|
+
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
39
|
+
if (pageStateDiagram) {
|
|
40
|
+
lines.push('## State Map');
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`\`\`\`mermaid\n${pageStateDiagram}\n\`\`\``);
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
36
45
|
if (interactions && interactions.length > 0) {
|
|
37
46
|
lines.push('## State Transitions');
|
|
38
47
|
lines.push('');
|
|
@@ -101,7 +110,11 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
101
110
|
lines.push('');
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
return `${lines.join('\n').trimEnd()}\n
|
|
113
|
+
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
114
|
+
url: state.url,
|
|
115
|
+
format: APPLICATION_SPEC_FORMAT,
|
|
116
|
+
version: APPLICATION_SPEC_VERSION,
|
|
117
|
+
});
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
function renderSpecIndex(outputDir: string, startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): string {
|
|
@@ -116,9 +129,10 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
116
129
|
lines.push(`Max pages: ${maxPages}`);
|
|
117
130
|
lines.push('');
|
|
118
131
|
const graph = buildStateGraph(outputDir, pages);
|
|
132
|
+
const mermaid = renderMermaidFromGraph(graph, true);
|
|
119
133
|
lines.push('## State Transitions');
|
|
120
134
|
lines.push('');
|
|
121
|
-
lines.push(`\`\`\`mermaid\n${
|
|
135
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
122
136
|
lines.push('');
|
|
123
137
|
const stateMap = renderStateMapFromGraph(graph);
|
|
124
138
|
if (stateMap) {
|
|
@@ -69,8 +69,12 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
|
|
|
69
69
|
continue;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (adjacency.get(sourceId)?.has(targetId)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (createsCycle(sourceId, targetId, adjacency)) {
|
|
77
|
+
const pairKey = `${sourceId}>${targetId}`;
|
|
74
78
|
if (drawnBack.has(pairKey)) {
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
@@ -78,10 +82,6 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
|
|
|
78
82
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
|
|
79
83
|
continue;
|
|
80
84
|
}
|
|
81
|
-
|
|
82
|
-
if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
85
|
adjacency.get(sourceId)?.add(targetId);
|
|
86
86
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
|
|
87
87
|
}
|
|
@@ -104,8 +104,8 @@ function renderMermaidBody(outputDir: string, pages: DocumentedPage[]): string {
|
|
|
104
104
|
return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function renderMermaidFromGraph(graph: StateGraph): string {
|
|
108
|
-
const lines: string[] = ['flowchart TD'];
|
|
107
|
+
function renderMermaidFromGraph(graph: StateGraph, compact = false): string {
|
|
108
|
+
const lines: string[] = [compact ? 'flowchart LR' : 'flowchart TD'];
|
|
109
109
|
if (graph.pages.length === 0) {
|
|
110
110
|
lines.push(' empty["No documented states"]');
|
|
111
111
|
return lines.join('\n');
|
|
@@ -117,6 +117,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
|
|
|
117
117
|
if (!children || children.length === 0) {
|
|
118
118
|
continue;
|
|
119
119
|
}
|
|
120
|
+
if (compact) {
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
lines.push(` ${renderNodeLine(child)}`);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
120
126
|
lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
|
|
121
127
|
for (const child of children) {
|
|
122
128
|
lines.push(` ${renderNodeLine(child)}`);
|
|
@@ -125,11 +131,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
|
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
for (const edge of graph.edges) {
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
arrow
|
|
134
|
+
const arrow = edge.isBack ? '-.->' : '-->';
|
|
135
|
+
if (compact) {
|
|
136
|
+
lines.push(` ${edge.source} ${arrow} ${edge.target}`);
|
|
137
|
+
} else {
|
|
138
|
+
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
131
139
|
}
|
|
132
|
-
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
|
|
@@ -174,6 +181,46 @@ function renderStateMapFromGraph(graph: StateGraph): string {
|
|
|
174
181
|
return rows.join('\n');
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function renderPageStateDiagram(label: string, url: string, interactions: StateTransition[]): string {
|
|
185
|
+
const targets = new Map<string, { node: StateNode; action: string; screenshot?: { title: string; relativePath: string } }>();
|
|
186
|
+
let index = 0;
|
|
187
|
+
for (const interaction of interactions) {
|
|
188
|
+
const targetState = interaction.targetState;
|
|
189
|
+
if (!targetState) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const key = `${targetState.kind}:${targetState.label}:${normalizeUrl(targetState.url)}`;
|
|
193
|
+
if (targets.has(key)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
targets.set(key, {
|
|
197
|
+
node: { id: `target${index++}`, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind },
|
|
198
|
+
action: interaction.action,
|
|
199
|
+
screenshot: interaction.screenshot,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (targets.size === 0) {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lines: string[] = ['flowchart LR'];
|
|
208
|
+
lines.push(` ${renderNodeLine({ id: 'self', kind: 'page', label, subLabel: url })}`);
|
|
209
|
+
for (const target of targets.values()) {
|
|
210
|
+
lines.push(` ${renderNodeLine(target.node)}`);
|
|
211
|
+
}
|
|
212
|
+
for (const target of targets.values()) {
|
|
213
|
+
lines.push(` self -->|"${escapeMermaidLabel(target.action)}"| ${target.node.id}`);
|
|
214
|
+
}
|
|
215
|
+
for (const target of targets.values()) {
|
|
216
|
+
if (target.screenshot) {
|
|
217
|
+
lines.push(` click ${target.node.id} "${target.screenshot.relativePath}" "${escapeMermaidLabel(target.screenshot.title)}"`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
177
224
|
function renderNodeLine(node: StateNode): string {
|
|
178
225
|
const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
|
|
179
226
|
if (node.kind === 'dialog' || node.kind === 'modal') {
|
|
@@ -207,7 +254,7 @@ function createsCycle(sourceId: string, targetId: string, adjacency: Map<string,
|
|
|
207
254
|
}
|
|
208
255
|
|
|
209
256
|
function escapeMermaidLabel(value: string): string {
|
|
210
|
-
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('|', '|');
|
|
257
|
+
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('#', '#').replaceAll('<', '<').replaceAll('>', '>').replaceAll('|', '|');
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
function escapeTable(value: string): string {
|
|
@@ -277,5 +324,5 @@ interface StateGraph {
|
|
|
277
324
|
classAssignment: Map<StateClass, string[]>;
|
|
278
325
|
}
|
|
279
326
|
|
|
280
|
-
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
|
|
327
|
+
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph };
|
|
281
328
|
export type { DocumentedPage, SkippedPage, StateGraph, StateNode, StateEdge, StateClick };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prima",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High-level browser driver CLI for orchestrating agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "prima": "./bin/prima-cli.ts" },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"format": "biome format --write .",
|
|
9
|
+
"lint:fix": "biome lint --write .",
|
|
10
|
+
"check:fix": "biome check --write ."
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^14.0.1",
|
|
14
|
+
"dedent": "^1.6.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { type ActivityEntry, addActivityListener, removeActivityListener } from '../../../src/activity.ts';
|
|
3
|
+
import { isVerboseMode } from '../../../src/utils/logger.ts';
|
|
4
|
+
|
|
5
|
+
const RESET_LINE = '\r\u001b[2K';
|
|
6
|
+
|
|
7
|
+
const stream = process.stderr;
|
|
8
|
+
let tracking = false;
|
|
9
|
+
|
|
10
|
+
export function trackActivityLine(): void {
|
|
11
|
+
if (tracking) return;
|
|
12
|
+
if (!stream.isTTY) return;
|
|
13
|
+
if (isVerboseMode()) return;
|
|
14
|
+
|
|
15
|
+
tracking = true;
|
|
16
|
+
addActivityListener(writeActivityLine);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearActivityLine(): void {
|
|
20
|
+
if (!tracking) return;
|
|
21
|
+
|
|
22
|
+
tracking = false;
|
|
23
|
+
removeActivityListener(writeActivityLine);
|
|
24
|
+
stream.write(RESET_LINE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeActivityLine(activity: ActivityEntry | null): void {
|
|
28
|
+
if (!activity) return;
|
|
29
|
+
|
|
30
|
+
const width = (stream.columns || 80) - 2;
|
|
31
|
+
const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
|
|
32
|
+
stream.write(`${RESET_LINE}${chalk.gray(message)}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
4
|
+
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
|
+
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
7
|
+
import { type EnvelopeData, renderEnvelope } from './envelope.ts';
|
|
8
|
+
import { Prima, type PrimaOptions } from './prima.ts';
|
|
9
|
+
|
|
10
|
+
const helpContract = dedent`
|
|
11
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
12
|
+
|
|
13
|
+
playwright-cli open <url> starts the session
|
|
14
|
+
prima <command> ... drives it
|
|
15
|
+
playwright-cli close ends it
|
|
16
|
+
|
|
17
|
+
One call takes a whole job:
|
|
18
|
+
|
|
19
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
20
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
21
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const checkHelp = dedent`
|
|
25
|
+
check takes an outcome rather than a click path, and works out how to reach it.
|
|
26
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
27
|
+
scenario text is the single expected outcome. Each comes back under
|
|
28
|
+
### Expected outcomes as PASSED, FAILED or not verified - "not verified"
|
|
29
|
+
means the run never checked it, which is not the same as false.
|
|
30
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const doHelp = dedent`
|
|
34
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
|
|
35
|
+
with what proved it. One that could not be carried out fails the command and says why.
|
|
36
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
37
|
+
what makes this tier cheap.
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const verifyHelp = dedent`
|
|
41
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
42
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
43
|
+
could not be expressed, which is not the same as false.
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const reportHelp = dedent`
|
|
47
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
48
|
+
The most recent session is reported unless --pw-session names another.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const sessionHelp = dedent`
|
|
52
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
53
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
54
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
55
|
+
attached, since the attached session keeps its own
|
|
56
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
57
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
58
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
let rootOptions: () => any = () => ({});
|
|
62
|
+
|
|
63
|
+
function buildOptions(subcommand: any): PrimaOptions {
|
|
64
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
65
|
+
return {
|
|
66
|
+
config: options.config,
|
|
67
|
+
path: options.path,
|
|
68
|
+
instance: options.instance,
|
|
69
|
+
session: options.session,
|
|
70
|
+
ephemeral: options.ephemeral,
|
|
71
|
+
framework: options.framework,
|
|
72
|
+
noVision: options.vision === false,
|
|
73
|
+
url: options.url,
|
|
74
|
+
baseUrl: options.baseUrl,
|
|
75
|
+
show: options.show,
|
|
76
|
+
headless: options.headless,
|
|
77
|
+
endpoint: options.endpoint,
|
|
78
|
+
pwSession: options.pwSession,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripEmpty(options: any): any {
|
|
83
|
+
const present: any = {};
|
|
84
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
85
|
+
if (value === undefined) continue;
|
|
86
|
+
present[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return present;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addCommonOptions(cmd: Command): Command {
|
|
92
|
+
return cmd
|
|
93
|
+
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
94
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
95
|
+
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
96
|
+
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
97
|
+
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
98
|
+
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
99
|
+
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
100
|
+
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
101
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
102
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function primaFor(options: any): Prima {
|
|
106
|
+
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
107
|
+
return new Prima(buildOptions(options));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
|
|
111
|
+
setQuietMode(!isVerboseMode());
|
|
112
|
+
trackActivityLine();
|
|
113
|
+
const prima = primaFor(options);
|
|
114
|
+
const startedAt = Date.now();
|
|
115
|
+
|
|
116
|
+
let envelope: EnvelopeData;
|
|
117
|
+
try {
|
|
118
|
+
await prima.start();
|
|
119
|
+
envelope = await run(prima);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
envelope = await prima.toolFailureEnvelope(command, error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (record) prima.record(envelope, Date.now() - startedAt);
|
|
125
|
+
clearActivityLine();
|
|
126
|
+
console.log(renderEnvelope(envelope));
|
|
127
|
+
await prima.stop().catch(() => {});
|
|
128
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>): Promise<void> {
|
|
132
|
+
let ok = false;
|
|
133
|
+
try {
|
|
134
|
+
ok = await run(primaFor(options));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(browserErrorMessage(error));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.exit(ok ? 0 : 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createPrimaCommands(name = 'prima'): Command {
|
|
144
|
+
const cmd = new Command(name);
|
|
145
|
+
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');
|
|
146
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
147
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
148
|
+
cmd.addHelpText('after', `\n${helpContract}`);
|
|
149
|
+
rootOptions = () => cmd.opts();
|
|
150
|
+
|
|
151
|
+
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
152
|
+
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
156
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
157
|
+
.action(async (instructions, options) => {
|
|
158
|
+
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
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'))
|
|
162
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
|
|
163
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
164
|
+
.action(async (scenario, options) => {
|
|
165
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
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) => {
|
|
169
|
+
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
173
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
174
|
+
.action(async (assertion, options) => {
|
|
175
|
+
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
addCommonOptions(
|
|
179
|
+
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')
|
|
180
|
+
).action(async (options) => {
|
|
181
|
+
await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
|
|
185
|
+
if (URL.canParse(target)) options.baseUrl = target;
|
|
186
|
+
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
|
|
190
|
+
setQuietMode(!isVerboseMode());
|
|
191
|
+
const prima = primaFor(options);
|
|
192
|
+
console.log(await prima.config().catch((error: unknown) => browserErrorMessage(error)));
|
|
193
|
+
await prima.stop().catch(() => {});
|
|
194
|
+
process.exit(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
198
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
202
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
203
|
+
.action(async (options) => {
|
|
204
|
+
setQuietMode(!isVerboseMode());
|
|
205
|
+
console.log(
|
|
206
|
+
await primaFor(options)
|
|
207
|
+
.report()
|
|
208
|
+
.catch((error: unknown) => browserErrorMessage(error))
|
|
209
|
+
);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
214
|
+
|
|
215
|
+
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
216
|
+
.option('-s, --show', 'Launch the browser in a visible window')
|
|
217
|
+
.option('--headless', 'Launch the browser without a window')
|
|
218
|
+
.action(async (options) => {
|
|
219
|
+
await runBrowser(options, async (prima) => {
|
|
220
|
+
await prima.browserStart();
|
|
221
|
+
console.log(await prima.browserStatus());
|
|
222
|
+
return keepServerRunning(() => prima.browserStop());
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
|
|
227
|
+
.option('--all', 'Stop every running instance')
|
|
228
|
+
.action(async (options) => {
|
|
229
|
+
await runBrowser(options, async (prima) => {
|
|
230
|
+
const stopped = await prima.browserStop(options.all);
|
|
231
|
+
console.log(await prima.browserStatus());
|
|
232
|
+
return stopped;
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
|
|
237
|
+
await runBrowser(options, async (prima) => {
|
|
238
|
+
console.log(await prima.browserStatus());
|
|
239
|
+
return true;
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
|
|
244
|
+
await runBrowser(options, async (prima) => {
|
|
245
|
+
console.log(await prima.browserList());
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return cmd;
|
|
251
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const EXPECTATION_LABELS = {
|
|
5
|
+
passed: 'PASSED ',
|
|
6
|
+
failed: 'FAILED ',
|
|
7
|
+
unverified: 'not verified',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface InstanceInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
tabs: number;
|
|
13
|
+
startedAgo?: string;
|
|
14
|
+
attached?: string;
|
|
15
|
+
others: Array<{ name: string; tabs: number }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EnvelopeData {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
command: string;
|
|
21
|
+
used?: string[];
|
|
22
|
+
page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
|
|
23
|
+
changes?: string | null;
|
|
24
|
+
steps?: Array<{ label: string; ok: boolean; proof: string }>;
|
|
25
|
+
expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>;
|
|
26
|
+
stepFiles?: string;
|
|
27
|
+
value?: string;
|
|
28
|
+
answer?: string;
|
|
29
|
+
research?: string;
|
|
30
|
+
assertions?: Array<{ code: string; passed: boolean; proof: string[] }>;
|
|
31
|
+
failure?: { error: string; compactAria?: string };
|
|
32
|
+
instance: InstanceInfo;
|
|
33
|
+
status?: string;
|
|
34
|
+
artifacts?: { aria: string; html: string; network?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderEnvelope(data: EnvelopeData): string {
|
|
38
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
39
|
+
return sections.filter((section) => section).join('\n\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; requests: unknown[] }): { aria: string; html: string; network?: string } {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
const paths: { aria: string; html: string; network?: string } = {
|
|
45
|
+
aria: path.resolve(dir, 'aria.yml'),
|
|
46
|
+
html: path.resolve(dir, 'page.html'),
|
|
47
|
+
};
|
|
48
|
+
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
49
|
+
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
50
|
+
|
|
51
|
+
if (!snapshot.requests.length) return paths;
|
|
52
|
+
|
|
53
|
+
paths.network = path.resolve(dir, 'network.jsonl');
|
|
54
|
+
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
55
|
+
return paths;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderResult(data: EnvelopeData): string {
|
|
59
|
+
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
60
|
+
if (data.used?.length) lines.push(`used: ${data.used.join('; ')}`);
|
|
61
|
+
return section('Result', lines.join('\n'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderPage(data: EnvelopeData): string {
|
|
65
|
+
const { url, previousUrl, title, state, visits } = data.page;
|
|
66
|
+
const urlLabel = `url: ${url}`;
|
|
67
|
+
const stateLabel = `state: ${state}`;
|
|
68
|
+
const width = Math.max(urlLabel.length, stateLabel.length) + 3;
|
|
69
|
+
let changedMarker = '';
|
|
70
|
+
if (previousUrl && previousUrl !== url) changedMarker = `(changed: ${previousUrl} → ${url})`;
|
|
71
|
+
const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
|
|
72
|
+
return section('Page', lines.join('\n'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderValue(data: EnvelopeData): string | null {
|
|
76
|
+
if (data.value === undefined) return null;
|
|
77
|
+
return section('Value', data.value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderChanges(data: EnvelopeData): string | null {
|
|
81
|
+
if (data.changes === undefined || data.changes === null) return null;
|
|
82
|
+
return section('Changes', data.changes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderSteps(data: EnvelopeData): string | null {
|
|
86
|
+
if (!data.steps?.length) return null;
|
|
87
|
+
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
data.steps.forEach((step, index) => {
|
|
90
|
+
lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
|
|
91
|
+
for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
92
|
+
});
|
|
93
|
+
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
|
|
94
|
+
return section('Steps', lines.join('\n'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderExpectations(data: EnvelopeData): string | null {
|
|
98
|
+
if (!data.expectations?.length) return null;
|
|
99
|
+
const lines = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
100
|
+
return section('Expected outcomes', lines.join('\n'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderOutcome(data: EnvelopeData): string | null {
|
|
104
|
+
if (data.answer) return section('Answer', data.answer);
|
|
105
|
+
if (data.research) return section('Research', data.research);
|
|
106
|
+
if (!data.assertions) return null;
|
|
107
|
+
|
|
108
|
+
if (!data.assertions.length) return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
|
|
109
|
+
|
|
110
|
+
const lines = data.assertions.map((assertion) => {
|
|
111
|
+
const code = assertion.code
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((line) => line.trim())
|
|
114
|
+
.filter((line) => line && !line.startsWith('//'))
|
|
115
|
+
.join(' ');
|
|
116
|
+
return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const proof = data.assertions.flatMap((assertion) => assertion.proof);
|
|
120
|
+
if (proof.length) lines.push('', 'playwright:', ...proof);
|
|
121
|
+
|
|
122
|
+
return section('Assertions', lines.join('\n'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderFailure(data: EnvelopeData): Array<string | null> {
|
|
126
|
+
if (!data.failure) return [];
|
|
127
|
+
return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderCompactAria(compactAria?: string): string | null {
|
|
131
|
+
if (!compactAria) return null;
|
|
132
|
+
return section('Current page (compact ARIA)', compactAria);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderInstance(data: EnvelopeData): string | null {
|
|
136
|
+
const instance = data.instance;
|
|
137
|
+
const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
|
|
138
|
+
if (instance.others.length) parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
|
|
139
|
+
if (data.status) parts.push(`details: prima status ${data.status}`);
|
|
140
|
+
return section('Instance', parts.join(' | '));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function browserLine(instance: InstanceInfo): string {
|
|
144
|
+
if (instance.attached) return `attached to ${instance.attached}`;
|
|
145
|
+
if (instance.startedAgo) return `running, started ${instance.startedAgo} ago`;
|
|
146
|
+
if (instance.tabs > 0) return 'running';
|
|
147
|
+
return 'not running';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function tabsLabel(tabs: number): string {
|
|
151
|
+
if (tabs === 1) return '1 tab';
|
|
152
|
+
return `${tabs} tabs`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function renderArtifacts(data: EnvelopeData): string | null {
|
|
156
|
+
if (!data.artifacts) return null;
|
|
157
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
158
|
+
if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
|
|
159
|
+
return section('Artifacts', lines.join('\n'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function align(label: string, marker: string, width: number): string {
|
|
163
|
+
if (!marker) return label;
|
|
164
|
+
return `${label.padEnd(width)}${marker}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function section(title: string, body: string): string {
|
|
168
|
+
return `### ${title}\n${body}`;
|
|
169
|
+
}
|