explorbot 0.5.9 → 0.6.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/README.md +1 -1
- package/bin/explorbot-cli.ts +495 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/api-tester/src/commands/init-command.ts +2 -0
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/dist/bin/explorbot-cli.js +456 -444
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/boat/api-tester/src/commands/init-command.js +2 -0
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +8 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/options/auth-option.d.ts +6 -0
- package/dist/src/commands/options/auth-option.js +14 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +3 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +18 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/docs/workflow/agentic-usage.md +7 -0
- package/docs/workflow/knowledge.md +6 -0
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +10 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/options/auth-option.ts +15 -0
- package/src/commands/options/index.ts +2 -0
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +5 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
package/dist/src/utils/logger.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import debug from 'debug';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
7
|
import stripAnsi from 'strip-ansi';
|
|
8
|
+
import wrapAnsi from 'wrap-ansi';
|
|
8
9
|
import { ConfigParser } from '../config.js';
|
|
9
10
|
import { Observability } from "../observability.js";
|
|
10
11
|
import { RecentStepFilter } from "./log-filters.js";
|
|
@@ -54,6 +55,10 @@ class ConsoleDestination {
|
|
|
54
55
|
forceEnabled = false;
|
|
55
56
|
quiet = false;
|
|
56
57
|
recentSteps = new RecentStepFilter();
|
|
58
|
+
width;
|
|
59
|
+
constructor() {
|
|
60
|
+
this.width = process.stdout.columns || 80;
|
|
61
|
+
}
|
|
57
62
|
isEnabled() {
|
|
58
63
|
if (this.quiet)
|
|
59
64
|
return false;
|
|
@@ -87,8 +92,7 @@ class ConsoleDestination {
|
|
|
87
92
|
if (entry.type === 'multiline' || entry.type === 'details') {
|
|
88
93
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
89
94
|
const parsed = parseMarkdownToTerminal(cleaned);
|
|
90
|
-
content = parsed
|
|
91
|
-
content = `${chalk.gray(content)}\n`;
|
|
95
|
+
content = `${chalk.dim('\n ┌'.padEnd(this.width, '─'))}\n${this.prefixLines(parsed, ' │ ', chalk.dim, false)}\n`;
|
|
92
96
|
}
|
|
93
97
|
else if (entry.type === 'success') {
|
|
94
98
|
content = `${chalk.green(content)}\n`;
|
|
@@ -100,16 +104,33 @@ class ConsoleDestination {
|
|
|
100
104
|
content = chalk.yellow(content);
|
|
101
105
|
}
|
|
102
106
|
else if (entry.type === 'step') {
|
|
103
|
-
content = chalk.gray
|
|
107
|
+
content = this.prefixLines(content, ' ', chalk.gray);
|
|
104
108
|
}
|
|
105
109
|
else if (entry.type === 'operation') {
|
|
106
|
-
content =
|
|
110
|
+
content = this.prefixLines(content, ' · ', chalk.gray);
|
|
107
111
|
}
|
|
108
112
|
else if (entry.type === 'substep') {
|
|
109
|
-
content =
|
|
113
|
+
content = this.prefixLines(content, ' > ', chalk.gray);
|
|
110
114
|
}
|
|
111
115
|
console.log(content);
|
|
112
116
|
}
|
|
117
|
+
prefixLines(content, prefix, style, strip = true) {
|
|
118
|
+
const prefixWidth = stripAnsi(prefix).length;
|
|
119
|
+
const available = Math.max(20, this.width - prefixWidth);
|
|
120
|
+
const lines = content
|
|
121
|
+
.split('\n')
|
|
122
|
+
.filter((line) => stripAnsi(line).trim() !== '')
|
|
123
|
+
.flatMap((line) => {
|
|
124
|
+
const text = strip ? stripAnsi(line) : line;
|
|
125
|
+
const wrapped = wrapAnsi(text, available, { hard: true });
|
|
126
|
+
return wrapped.split('\n').map((part) => style(`${prefix}${part}`));
|
|
127
|
+
});
|
|
128
|
+
if (lines.length > 10) {
|
|
129
|
+
const remaining = lines.length - 10;
|
|
130
|
+
return [...lines.slice(0, 10), chalk.gray(` │ --- +${remaining} lines ---`)].join('\n');
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
113
134
|
}
|
|
114
135
|
class DebugDestination {
|
|
115
136
|
verboseMode = false;
|
|
@@ -138,13 +159,9 @@ class DebugDestination {
|
|
|
138
159
|
class FileDestination {
|
|
139
160
|
initialized = false;
|
|
140
161
|
logFilePath = null;
|
|
141
|
-
verboseMode = true;
|
|
142
162
|
isEnabled() {
|
|
143
163
|
return true;
|
|
144
164
|
}
|
|
145
|
-
setVerboseMode(enabled) {
|
|
146
|
-
this.verboseMode = enabled;
|
|
147
|
-
}
|
|
148
165
|
write(entry) {
|
|
149
166
|
if (entry.type === 'html')
|
|
150
167
|
return;
|
|
@@ -358,7 +375,7 @@ class Logger {
|
|
|
358
375
|
truncateTagContent(content, tagName) {
|
|
359
376
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
360
377
|
const tagRegex = new RegExp(`<${escapedTag}>([\\s\\S]*?)<\\/${escapedTag}>`, 'g');
|
|
361
|
-
return content.replace(tagRegex, (
|
|
378
|
+
return content.replace(tagRegex, (_, innerContent) => {
|
|
362
379
|
const trimmed = innerContent.trim();
|
|
363
380
|
const totalLength = trimmed.length;
|
|
364
381
|
if (totalLength <= 100) {
|
|
@@ -21,10 +21,9 @@ if (renderer) {
|
|
|
21
21
|
const originalListitem = renderer.listitem;
|
|
22
22
|
renderer.listitem = function (text) {
|
|
23
23
|
if (typeof text === 'object') {
|
|
24
|
-
const item = text;
|
|
25
24
|
const parser = this.parser;
|
|
26
25
|
const originalParse = parser.parse.bind(parser);
|
|
27
|
-
parser.parse = (tokens
|
|
26
|
+
parser.parse = (tokens) => parser.parseInline(tokens);
|
|
28
27
|
const result = originalListitem.call(this, text);
|
|
29
28
|
parser.parse = originalParse;
|
|
30
29
|
return result;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figures from 'figures';
|
|
2
4
|
import { tag } from './logger.js';
|
|
3
5
|
export function relativeToCwd(absPath) {
|
|
4
6
|
const rel = path.relative(process.cwd(), absPath);
|
|
@@ -7,25 +9,27 @@ export function relativeToCwd(absPath) {
|
|
|
7
9
|
export function printNextSteps(sections) {
|
|
8
10
|
if (sections.length === 0)
|
|
9
11
|
return;
|
|
10
|
-
const
|
|
12
|
+
const lines = [''];
|
|
13
|
+
let isDefault = true;
|
|
11
14
|
for (const section of sections) {
|
|
12
|
-
const lines = [];
|
|
13
15
|
const headerPath = section.path ? relativeToCwd(section.path) : '';
|
|
14
|
-
lines.push(headerPath ? `${section.label}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const padded = `${cmd.label}:`.padEnd(maxLabel + 2);
|
|
25
|
-
lines.push(` ${padded} ${cmd.command}`);
|
|
16
|
+
lines.push(headerPath ? `${chalk.bold(section.label)} ${chalk.gray(headerPath)}` : `${chalk.bold(section.label)}`);
|
|
17
|
+
for (const cmd of section.commands || []) {
|
|
18
|
+
if (cmd.label)
|
|
19
|
+
lines.push(BLANK);
|
|
20
|
+
if (isDefault) {
|
|
21
|
+
if (cmd.label)
|
|
22
|
+
lines.push(chalk.bold.yellow(`${figures.pointer} ${cmd.label}`));
|
|
23
|
+
lines.push(chalk.bold(` ${cmd.command}`));
|
|
24
|
+
isDefault = false;
|
|
25
|
+
continue;
|
|
26
26
|
}
|
|
27
|
+
if (cmd.label)
|
|
28
|
+
lines.push(chalk.dim(` ${cmd.label}`));
|
|
29
|
+
lines.push(` ${cmd.command}`);
|
|
27
30
|
}
|
|
28
|
-
|
|
31
|
+
lines.push(BLANK);
|
|
29
32
|
}
|
|
30
|
-
tag('
|
|
33
|
+
tag('info').log(lines.join('\n'));
|
|
31
34
|
}
|
|
35
|
+
const BLANK = ' ';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare function waitForPageReadiness(page: any, options?: PageReadinessOptions): Promise<
|
|
1
|
+
export declare function waitForPageReadiness(page: any, options?: PageReadinessOptions): Promise<boolean>;
|
|
2
|
+
export declare function waitForDomChange(page: any, timeout: number): Promise<boolean>;
|
|
2
3
|
export declare function sleep(ms: number): Promise<void>;
|
|
3
4
|
interface PageReadinessOptions {
|
|
4
5
|
timeout?: number;
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
export async function waitForPageReadiness(page, options = {}) {
|
|
2
2
|
if (!page)
|
|
3
|
-
return;
|
|
3
|
+
return false;
|
|
4
4
|
const timeout = options.timeout ?? 6000;
|
|
5
5
|
await page.waitForLoadState?.('domcontentloaded', { timeout })?.catch(() => { });
|
|
6
6
|
await Promise.race([waitForNetworkIdle(page, timeout), waitForDomQuiet(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => { });
|
|
7
|
-
|
|
7
|
+
return waitForPageBodyContent(page, timeout);
|
|
8
|
+
}
|
|
9
|
+
export function waitForDomChange(page, timeout) {
|
|
10
|
+
if (!page?.evaluate)
|
|
11
|
+
return Promise.resolve(false);
|
|
12
|
+
return page
|
|
13
|
+
.evaluate((ms) => new Promise((resolve) => {
|
|
14
|
+
const observer = new MutationObserver(() => {
|
|
15
|
+
observer.disconnect();
|
|
16
|
+
resolve(true);
|
|
17
|
+
});
|
|
18
|
+
observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
observer.disconnect();
|
|
21
|
+
resolve(false);
|
|
22
|
+
}, ms);
|
|
23
|
+
}), timeout)
|
|
24
|
+
.catch(() => true);
|
|
8
25
|
}
|
|
9
26
|
const DOM_QUIET_MS = 350;
|
|
10
27
|
function waitForDomQuiet(page, timeout) {
|
|
@@ -50,15 +67,17 @@ async function waitForVisibleSpinnersHidden(page, selectors, timeout) {
|
|
|
50
67
|
}
|
|
51
68
|
function waitForPageBodyContent(page, timeout) {
|
|
52
69
|
if (!page?.waitForFunction)
|
|
53
|
-
return Promise.resolve();
|
|
70
|
+
return Promise.resolve(true);
|
|
54
71
|
return page
|
|
55
72
|
.waitForFunction(() => {
|
|
56
73
|
const body = document.body;
|
|
57
74
|
if (!body)
|
|
58
75
|
return false;
|
|
59
|
-
|
|
76
|
+
if (body.innerText.trim().length > 0)
|
|
77
|
+
return true;
|
|
78
|
+
return !!body.querySelector('input, button, select, textarea, img, svg, canvas, iframe, video');
|
|
60
79
|
}, undefined, { timeout })
|
|
61
|
-
.
|
|
80
|
+
.then(() => true, () => false);
|
|
62
81
|
}
|
|
63
82
|
export function sleep(ms) {
|
|
64
83
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2,6 +2,9 @@ import { Plan } from '../test-plan.js';
|
|
|
2
2
|
export declare function planToCompactAiContext(plan: Plan): string;
|
|
3
3
|
export declare function parsePlanFromMarkdown(filePath: string): Plan;
|
|
4
4
|
export declare function parsePlansFromMarkdown(filePath: string): Plan[];
|
|
5
|
+
export declare function parsePlansFromMarkdownText(content: string): Plan[];
|
|
6
|
+
export declare function findPlanMarkdownProblems(markdown: string): string[];
|
|
7
|
+
export declare function readPlanMarkdownComment(markdown: string, key: string): string | null;
|
|
5
8
|
export declare function savePlanToMarkdown(plan: Plan, filePath: string): void;
|
|
6
9
|
export declare function savePlansToMarkdown(plans: Plan[], filePath: string): void;
|
|
7
10
|
export declare function planToAiContext(plan: Plan, options?: {
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { Plan, Test } from "../test-plan.js";
|
|
3
3
|
import { mdq } from "./markdown-query.js";
|
|
4
4
|
const NOISE_PREFIXES = ['Test started', 'Finish requested:', 'Session name:'];
|
|
5
|
+
const CURRENT_PAGE = 'Current page';
|
|
5
6
|
function isNoiseNote(message) {
|
|
6
7
|
return NOISE_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
7
8
|
}
|
|
@@ -118,7 +119,9 @@ export function parsePlanFromMarkdown(filePath) {
|
|
|
118
119
|
return main;
|
|
119
120
|
}
|
|
120
121
|
export function parsePlansFromMarkdown(filePath) {
|
|
121
|
-
|
|
122
|
+
return parsePlansFromMarkdownText(readFileSync(filePath, 'utf-8'));
|
|
123
|
+
}
|
|
124
|
+
export function parsePlansFromMarkdownText(content) {
|
|
122
125
|
const lines = content.split('\n');
|
|
123
126
|
const plans = [];
|
|
124
127
|
let currentPlan = null;
|
|
@@ -184,7 +187,8 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
184
187
|
continue;
|
|
185
188
|
}
|
|
186
189
|
if (currentTest && inRequirements && line && !line.startsWith('##')) {
|
|
187
|
-
|
|
190
|
+
if (line !== CURRENT_PAGE)
|
|
191
|
+
currentTest.startUrl = line;
|
|
188
192
|
continue;
|
|
189
193
|
}
|
|
190
194
|
if (currentTest && inSteps && line.startsWith('* ')) {
|
|
@@ -207,7 +211,7 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
207
211
|
continue;
|
|
208
212
|
const nextSuite = content.indexOf('<!-- suite -->', suiteStart + 1);
|
|
209
213
|
const suiteContent = nextSuite === -1 ? content.slice(suiteStart) : content.slice(suiteStart, nextSuite);
|
|
210
|
-
const firstItem = mdq(suiteContent).query('section[0] item[0]').
|
|
214
|
+
const firstItem = mdq(suiteContent).query('section[0] item[0]').nodes()[0]?.text.trim() || '';
|
|
211
215
|
const urlMatch = firstItem.match(/^URL:\s*(.+)/);
|
|
212
216
|
if (urlMatch) {
|
|
213
217
|
plan.url = urlMatch[1].replace(/\*\*|`|\*|_|~~?/g, '').trim();
|
|
@@ -219,6 +223,33 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
219
223
|
}
|
|
220
224
|
return plans;
|
|
221
225
|
}
|
|
226
|
+
export function findPlanMarkdownProblems(markdown) {
|
|
227
|
+
const doc = mdq(markdown);
|
|
228
|
+
const problems = [];
|
|
229
|
+
const headings = doc.query('h1').nodes();
|
|
230
|
+
if (!doc.query('comment(/^suite/)').exists() || headings.length === 0) {
|
|
231
|
+
problems.push('Missing <!-- suite --> comment followed by the plan name heading');
|
|
232
|
+
}
|
|
233
|
+
const scenarioCount = Math.max(headings.length - 1, 0);
|
|
234
|
+
const testComments = doc.query('comment(/^test/)').count();
|
|
235
|
+
if (testComments !== scenarioCount) {
|
|
236
|
+
problems.push(`Found ${scenarioCount} scenario headings but ${testComments} <!-- test --> comments`);
|
|
237
|
+
}
|
|
238
|
+
for (const section of doc.query('section1').each().slice(1)) {
|
|
239
|
+
const title = section.query('heading').nodes()[0]?.text;
|
|
240
|
+
if (!section.query('section2("Steps") item').exists())
|
|
241
|
+
problems.push(`Scenario "${title}" has no ## Steps items`);
|
|
242
|
+
if (!section.query('section2("Expected") item').exists())
|
|
243
|
+
problems.push(`Scenario "${title}" has no ## Expected items`);
|
|
244
|
+
}
|
|
245
|
+
return problems;
|
|
246
|
+
}
|
|
247
|
+
export function readPlanMarkdownComment(markdown, key) {
|
|
248
|
+
const body = mdq(markdown).query(`comment(/^${key}:/)`).nodes()[0]?.text;
|
|
249
|
+
if (!body)
|
|
250
|
+
return null;
|
|
251
|
+
return body.slice(key.length + 1).trim() || null;
|
|
252
|
+
}
|
|
222
253
|
function formatPlanSuite(plan) {
|
|
223
254
|
let content = `<!-- suite -->\n# ${plan.title}\n\n`;
|
|
224
255
|
if (plan.url) {
|
|
@@ -231,7 +262,7 @@ function formatPlanSuite(plan) {
|
|
|
231
262
|
content += `<!-- test\npriority: ${test.priority}\n-->\n`;
|
|
232
263
|
content += `# ${test.scenario}\n\n`;
|
|
233
264
|
content += '## Requirements\n';
|
|
234
|
-
content += `${test.startUrl ||
|
|
265
|
+
content += `${test.startUrl || CURRENT_PAGE}\n\n`;
|
|
235
266
|
if (test.plannedSteps.length > 0) {
|
|
236
267
|
content += '## Steps\n';
|
|
237
268
|
for (const step of test.plannedSteps) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function isDynamicSegment(segment: string): boolean;
|
|
2
2
|
export declare function hasDynamicUrlSegment(url: string): boolean;
|
|
3
3
|
export declare function isSamePageFamily(urlA: string, urlB: string): boolean;
|
|
4
|
+
export declare function isSamePageIgnoringQuery(urlA: string, urlB: string): boolean;
|
|
4
5
|
export declare function generalizeSegment(segment: string): string;
|
|
5
6
|
export declare function generalizeUrl(url: string, replaceSegment?: (segment: string) => string): string;
|
|
6
7
|
export declare function matchesUrl(pattern: string, path: string): boolean;
|
|
@@ -51,6 +51,11 @@ export function isSamePageFamily(urlA, urlB) {
|
|
|
51
51
|
}
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
|
+
export function isSamePageIgnoringQuery(urlA, urlB) {
|
|
55
|
+
const a = new URL(urlA, 'http://localhost');
|
|
56
|
+
const b = new URL(urlB, 'http://localhost');
|
|
57
|
+
return a.pathname === b.pathname && a.hash === b.hash;
|
|
58
|
+
}
|
|
54
59
|
export function generalizeSegment(segment) {
|
|
55
60
|
if (/^\d+$/.test(segment))
|
|
56
61
|
return '\\d+';
|
package/docs/basics/providers.md
CHANGED
|
@@ -59,7 +59,7 @@ export default {
|
|
|
59
59
|
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
60
60
|
visionModel: openrouter('openai/gpt-5.6-luna'),
|
|
61
61
|
agenticModel: openrouter('anthropic/claude-haiku-5.5'),
|
|
62
|
-
decisionModel: { provider: 'openrouter', model: 'typesafe/jev-latest' },
|
|
62
|
+
decisionModel: { provider: 'openrouter', model: '~typesafe/jev-latest' },
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
```
|
|
@@ -28,6 +28,14 @@ The plan (`tests/regression/seeds/vault-plan.md`) describes *what* to do but nev
|
|
|
28
28
|
|
|
29
29
|
> The code is carried in a knowledge file, not only experience, on purpose. Knowledge is Explorbot's prominent, deterministic channel for page-specific data — the same mechanism scenario A uses for login credentials. Empirically, cheap models followed an experience-only code recipe only occasionally, so an experience-only gate was a coin-flip; knowledge makes the gate reliable while the experience file still exercises recipe reuse.
|
|
30
30
|
|
|
31
|
+
### Scenario C — late-rendering start page (`regression:slow`)
|
|
32
|
+
|
|
33
|
+
Runs scenario A again with the fixture's slow start on: on the first visit of a session `/issues` arrives as an empty shell and renders client-side 12 seconds later, longer than the page-readiness wait. Scenario A's assertions must still hold, which proves the Researcher waited for the real page instead of researching the blank shell. It executes one test instead of five (`--max-tests 1`, at least one passing): research and planning are what it checks, and scenario A already covers running the plan.
|
|
34
|
+
|
|
35
|
+
### Scenario D — sign-in form at the start URL (`regression:gated`)
|
|
36
|
+
|
|
37
|
+
Runs scenario A's assertions against `explore /workspace`. Signed out, `/workspace` shows the sign-in form at its own URL instead of redirecting to `/login`, and signing in there keeps you on `/workspace`. The planner reports the page blocked by a sign-in form, so explore must hand the report to Captain, sign in with the credentials in `seeds/knowledge/workspace.md` and plan the page again. Like scenario C, it executes one test.
|
|
38
|
+
|
|
31
39
|
## The fixture app — "Trackly"
|
|
32
40
|
|
|
33
41
|
A self-contained issue tracker served in-process by `tests/regression/fixture/server.ts` (`Bun.serve`, ephemeral port, fresh in-memory store per attempt). No external network, no database.
|
|
@@ -40,6 +48,7 @@ A self-contained issue tracker served in-process by `tests/regression/fixture/se
|
|
|
40
48
|
| `/issues/:id` | yes | detail, change-status menu, comment form, delete modal |
|
|
41
49
|
| `/settings` | yes | Profile/Preferences tabs + an activity iframe |
|
|
42
50
|
| `/vault` | no | scenario B access-code gate |
|
|
51
|
+
| `/workspace` | no / yes | scenario D: the sign-in form when signed out, the issue list when signed in |
|
|
43
52
|
| `/api/*` | cookie | REST API over the same store, `+ /api/openapi.json` |
|
|
44
53
|
|
|
45
54
|
The REST API mirrors the store and is served for a future API-testing scenario; the current scenarios do not exercise it.
|
|
@@ -68,7 +77,9 @@ bunx bunosh regression:serve
|
|
|
68
77
|
export OPENROUTER_API_KEY=sk-...
|
|
69
78
|
bunx bunosh regression:experience --retries 0 # cheapest AI path
|
|
70
79
|
bunx bunosh regression:basic --retries 0
|
|
71
|
-
bunx bunosh regression:
|
|
80
|
+
bunx bunosh regression:slow --retries 0
|
|
81
|
+
bunx bunosh regression:gated --retries 0
|
|
82
|
+
bunx bunosh regression:all # all scenarios, default retries
|
|
72
83
|
```
|
|
73
84
|
|
|
74
85
|
Each attempt runs in an isolated throwaway directory under `tests/regression/.runs/` (gitignored). Inspect `tests/regression/.runs/<scenario>-<variant>-a<n>/output/` for the research, plans, and reporter markdown a run produced, and `tests/regression/.runs/report.md` for the summary.
|
|
@@ -83,9 +94,13 @@ bunx bunosh regression:variants --scenario basic --variants native,aria,plain
|
|
|
83
94
|
|
|
84
95
|
## CI and the approval gate
|
|
85
96
|
|
|
86
|
-
`.github/workflows/regression.yml` runs on `pull_request` and `workflow_dispatch`. The job uses the `regression` GitHub environment, which holds `OPENROUTER_API_KEY` behind a **required reviewer**. Every run — including fork PRs — pauses at the environment gate before the secret is exposed. The job runs the PR's own code, so the reviewer must read the PR diff (workflow, Bunoshfile, fixture, lib) before approving; that review is the security boundary. The workflow never uses `pull_request_target`.
|
|
97
|
+
`.github/workflows/regression.yml` runs on `pull_request` and `workflow_dispatch`. `regression:all` runs the scenarios in parallel inside one job, so a run lasts as long as its slowest scenario and needs one approval. The job uses the `regression` GitHub environment, which holds `OPENROUTER_API_KEY` behind a **required reviewer**. Every run — including fork PRs — pauses at the environment gate before the secret is exposed. The job runs the PR's own code, so the reviewer must read the PR diff (workflow, Bunoshfile, fixture, lib) before approving; that review is the security boundary. The workflow never uses `pull_request_target`.
|
|
98
|
+
|
|
99
|
+
Every attempt writes a `record.json` into its run directory. `regression:report` merges those records and posts the report as a sticky PR comment (on `pull_request`) or a `Regression Reports` GitHub Discussion (on `workflow_dispatch`). Fork PRs get a read-only token, so comment posting may fail; the report is also written to the step summary.
|
|
100
|
+
|
|
101
|
+
### Speed comparison
|
|
87
102
|
|
|
88
|
-
The report is
|
|
103
|
+
The report's `Time` column is the duration of the passing attempt. `Previous median` is the median of that scenario's passing time over the last 5 runs, and `Change` flags a difference of 20% or more. The job uploads the run's records as the `regression-summary` artifact (the run directories go to `regression-runs`) (kept 90 days), and the next run downloads the latest five to compare against. Which tests the planner picks changes a run's time, so read one flagged run as a hint and a trend over several as a regression.
|
|
89
104
|
|
|
90
105
|
The report also embeds the **Session Analysis** from the fresh-explore run — Explorbot's Analyst agent writes a prose summary of what works, defects, UX issues, and execution issues, which the harness reads from `output/reports/<label>.md` and appends to the comment. The Analyst is enabled in the config template (`ai.agents.analyst.enabled: true`); it adds one AI call per basic run.
|
|
91
106
|
|
|
@@ -93,7 +108,7 @@ The report also embeds the **Session Analysis** from the fresh-explore run — E
|
|
|
93
108
|
|
|
94
109
|
- **Thresholds** (`MIN_PLANNED_TESTS`, `MIN_PASSED`, `MIN_KEYWORD_HITS`, `MIN_FEATURE_GROUPS`) live at the top of `tests/regression/lib/assertions.ts`.
|
|
95
110
|
- **Models** live in `tests/regression/fixture/explorbot.config.js`. If cheap models are too flaky, promote a stronger model to `ai.model` or lower `MIN_PASSED`.
|
|
96
|
-
- **Run size** — adjust
|
|
111
|
+
- **Run size** — adjust `maxTests` / `minPassed` on the scenario constants and the per-CLI `timeout` values in `Bunoshfile.js`.
|
|
97
112
|
|
|
98
113
|
## Owner prerequisites (one-time)
|
|
99
114
|
|
|
@@ -34,7 +34,7 @@ Inside the TUI, use the matching slash command: `/explore`, `/research`, `/plan`
|
|
|
34
34
|
| List saved plans | `npx explorbot plans [plan]` | `/plans [plan]` | Show plans and their tests |
|
|
35
35
|
| Navigate to a URL | `npx explorbot navigate <url>` | `/navigate <target>` | Reachability probe + session capture |
|
|
36
36
|
| Drill page components | `npx explorbot drill <url>` | `/drill [--save-knowledge <path>] [--max-components <n>]` | Learn interactions |
|
|
37
|
-
| Execute plan tests | `npx explorbot test <planfile> [index]` | `/test [scenario\|number\|*]` | Run scenarios |
|
|
37
|
+
| Execute plan tests | `npx explorbot test <planfile> [index]` | `/test [planfile] [scenario\|number\|*]` | Run scenarios |
|
|
38
38
|
| Re-run generated tests | `npx explorbot rerun <file> [index]` | `/rerun <file> [index]` | With AI auto-healing |
|
|
39
39
|
| List generated tests | `npx explorbot runs [file]` | `/runs [file]` | Index + dry-run |
|
|
40
40
|
| Store domain knowledge | `npx explorbot learn [url] [note]` | `/learn [note]` | Persisted to `knowledge/` |
|
|
@@ -93,6 +93,17 @@ npx explorbot explore /pay --knowledge 'Test card 4111 1111 1111 1111, any futur
|
|
|
93
93
|
|
|
94
94
|
Like `--ws`, it is a program-level option rather than a per-command one: it works on every command — including `api`, `docs` and `prima` — and can go anywhere on the line. It is listed under `npx explorbot --help` rather than in each command's own help.
|
|
95
95
|
|
|
96
|
+
### `--auth`
|
|
97
|
+
|
|
98
|
+
Passes login details to the run in free form — credentials, a token, whatever the app asks for. It joins the run's knowledge on every page and API endpoint, stays in memory, and is never written to disk. `EXPLORBOT_AUTH` does the same from the environment; the flag wins when both are set.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx explorbot explore /dashboard --auth 'login: admin@example.com password: secret123'
|
|
102
|
+
npx explorbot api explore /orders --auth 'Bearer: eyJhbGciOi...'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Like `--knowledge`, it works on every command of `explorbot`, `api`, `docs` and `prima`, and can go anywhere on the line.
|
|
106
|
+
|
|
96
107
|
### `--session`
|
|
97
108
|
|
|
98
109
|
Saves browser state (cookies, localStorage, sessionStorage) to a JSON file. The next run restores the session, so you skip login and setup steps.
|
|
@@ -129,6 +140,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
129
140
|
| `EXPLORBOT_EPHEMERAL` | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
130
141
|
| `EXPLORBOT_KNOWLEDGE` | Inline knowledge text, applied to every page |
|
|
131
142
|
| `EXPLORBOT_KNOWLEDGE_FILE` | Path to a knowledge markdown file |
|
|
143
|
+
| `EXPLORBOT_AUTH` | Auth details in free form (credentials, a token), kept in memory for the run and never written to disk; same as --auth |
|
|
132
144
|
| `EXPLORBOT_SPEC` | Docbot application spec directory or index.md, used as page knowledge |
|
|
133
145
|
| `EXPLORBOT_API_SPEC` | OpenAPI spec path for the API boat |
|
|
134
146
|
| `EXPLORBOT_API_HEADERS` | Headers sent with every API request, one "Name: value" per line |
|
|
@@ -442,6 +454,8 @@ Execute test scenarios using the Tester agent.
|
|
|
442
454
|
```bash
|
|
443
455
|
# CLI
|
|
444
456
|
npx explorbot test output/plans/login.md # run all enabled tests
|
|
457
|
+
npx explorbot test login # same, plan looked up in output/plans
|
|
458
|
+
npx explorbot test login --path example # plan looked up in example/output/plans
|
|
445
459
|
npx explorbot test output/plans/login.md 3 # run test #3
|
|
446
460
|
npx explorbot test output/plans/login.md 1-5 # range
|
|
447
461
|
npx explorbot test output/plans/login.md 1,3,7 # selection
|
|
@@ -451,9 +465,11 @@ npx explorbot test 3 --from-plan output/plans/login.md # index first, plan via
|
|
|
451
465
|
|
|
452
466
|
```
|
|
453
467
|
# TUI
|
|
454
|
-
/test # Run next pending test
|
|
468
|
+
/test # Run next pending test of the current plan
|
|
455
469
|
/test * # Run all pending tests
|
|
456
470
|
/test 2 # Run test #2 from plan
|
|
471
|
+
/test login.md # Load plan file and run all its tests
|
|
472
|
+
/test login.md 2 # Load plan file and run test #2
|
|
457
473
|
/test login # Run tests matching "login"
|
|
458
474
|
/test User can logout successfully # Create and run ad-hoc test
|
|
459
475
|
```
|
|
@@ -218,7 +218,7 @@ The Researcher takes all standard agent options plus options that control intera
|
|
|
218
218
|
| Option | Type | Description |
|
|
219
219
|
|--------|------|-------------|
|
|
220
220
|
| `maxExpandableClicks` | `number` | Maximum expandable elements clicked during deep analysis (default: 10) |
|
|
221
|
-
| `errorPageTimeout` | `number` | Seconds to wait for a
|
|
221
|
+
| `errorPageTimeout` | `number` | Seconds to wait for a blank or barely rendered page to finish loading before research; uses the same readiness checks as every action, and the judge when `ai.decisionModel` is set (default: 10, `0` disables the wait) |
|
|
222
222
|
| `focusSections` | `string[]` | CSS selectors that narrow research to a matching element when present (e.g. an open modal or drawer). First match wins. Applies only to the per-section fallback used after a truncated research response. |
|
|
223
223
|
|
|
224
224
|
```javascript
|
|
@@ -125,6 +125,14 @@ The Captain steps in when a slash command isn't enough — answering questions a
|
|
|
125
125
|
|
|
126
126
|
When a test hits a fatal browser error, the Captain tries to recover — reload, restart the browser, open a fresh tab, or close extra tabs — before the test is stopped, then tells the Tester how to continue. It runs on explicit TUI requests and on test interrupts (stop, pass, skip, or redirect a running test).
|
|
127
127
|
|
|
128
|
+
Wherever Explorbot opens a page, plans a page or starts a test, including in `explore` and `freesail`, the Captain also takes over when another agent reports it is blocked:
|
|
129
|
+
|
|
130
|
+
- the Planner can't plan a page, for example because it shows a sign-in form
|
|
131
|
+
- the Navigator can't reach a page after all its attempts
|
|
132
|
+
- the page open at a test's start isn't where that test can start; this check needs [`ai.decisionModel`](../reference/configuration.md)
|
|
133
|
+
|
|
134
|
+
The Captain gets what the agent was asked to do, where it stopped and why, and what was already tried. It works out from the browser session how to get past the blocker. Then the planning, the page or the test continues. If the Captain can't resolve it, the page or test is skipped, and the same blocker isn't handed to it again in that run.
|
|
135
|
+
|
|
128
136
|
## Per-Agent Model Configuration
|
|
129
137
|
|
|
130
138
|
Use different models for different agents to control cost:
|
|
@@ -27,6 +27,8 @@ Log in with these credentials:
|
|
|
27
27
|
|
|
28
28
|
Explorbot reads this when it opens the login page and signs in on its own. See [Knowledge](../workflow/knowledge.md).
|
|
29
29
|
|
|
30
|
+
Some apps show the sign-in form at the page you asked for instead of redirecting to `/login`. When the planner reports that the page is blocked by a sign-in form, `explore` hands that report to Captain. Captain looks at the browser session, decides how to get past the form with this knowledge, and re-plans the page itself. If Captain adds no tests, the page is skipped with the planner's reason. Set the knowledge file's `url` to that page so the credentials load there.
|
|
31
|
+
|
|
30
32
|
## Stay logged in between runs
|
|
31
33
|
|
|
32
34
|
Logging in on every run is slow. Use `--session` to save cookies and local storage, then restore them next time:
|
|
@@ -39,7 +39,7 @@ ai: {
|
|
|
39
39
|
| `sections` | `string[]` | all sections | Page sections to identify (order = priority) |
|
|
40
40
|
| `focusSections` | `string[]` | `[]` | CSS selectors used in the truncated-response fallback, when research is split into per-section requests (first match wins). The matching element becomes the container for the focused section — see [Handling Truncated Responses](#handling-truncated-responses). |
|
|
41
41
|
| `maxExpandableClicks` | `number` | `10` | Max expandable elements clicked during deep research |
|
|
42
|
-
| `errorPageTimeout` | `number` | `10` | Seconds to wait for
|
|
42
|
+
| `errorPageTimeout` | `number` | `10` | Seconds to wait for a blank or barely rendered page to settle before research. Uses the same readiness checks as every action (network idle, quiet DOM, `playwright.spinnerSelectors`, visible content), so a small page that has rendered stops the wait early. With `ai.decisionModel` set, the judge decides whether a page that still looks unfinished is loading, an error page or loaded, and Explorbot waits for the page to change before asking again. Error pages detected during this wait abort research. Set `0` to skip the wait |
|
|
43
43
|
| `retries` | `number` | `2` | Retries when most locators are broken in Stage 2 |
|
|
44
44
|
| `reasoning` | `string` | `'low'` | AI SDK v7 reasoning effort: `'none'`, `'minimal'`, `'low'`, `'medium'`, `'high'`, `'xhigh'`, `'provider-default'` |
|
|
45
45
|
| `providerOptions` | `object` | - | Provider-specific options. Reasoning keys here take precedence over `reasoning`. |
|
|
@@ -57,6 +57,7 @@ No `init`, no config file, no project directory, no model IDs to look up. These
|
|
|
57
57
|
| `EXPLORBOT_EPHEMERAL` | no | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
58
58
|
| `EXPLORBOT_KNOWLEDGE` | no | Inline knowledge text, applied to every page |
|
|
59
59
|
| `EXPLORBOT_KNOWLEDGE_FILE` | no | Path to a knowledge markdown file |
|
|
60
|
+
| `EXPLORBOT_AUTH` | no | Auth details in free form (credentials, a token), kept in memory for the run and never written to disk; same as --auth |
|
|
60
61
|
| `EXPLORBOT_SPEC` | no | Docbot application spec directory or index.md, used as page knowledge |
|
|
61
62
|
| `EXPLORBOT_API_SPEC` | no | OpenAPI spec path for the API boat |
|
|
62
63
|
| `EXPLORBOT_API_HEADERS` | no | Headers sent with every API request, one "Name: value" per line |
|
|
@@ -133,6 +134,12 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
133
134
|
npx explorbot explore /admin/users
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
`EXPLORBOT_AUTH` takes the same free-form credentials but keeps them in memory only — nothing lands in the knowledge directory. `--auth` is its flag twin:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
EXPLORBOT_AUTH="login: admin@example.com password: secret123" npx explorbot explore /admin/users
|
|
141
|
+
```
|
|
142
|
+
|
|
136
143
|
`EXPLORBOT_KNOWLEDGE_FILE` points at a markdown file the agent wrote. Its frontmatter is preserved, so it can target specific URLs — see [Knowledge](./knowledge.md) for the format:
|
|
137
144
|
|
|
138
145
|
```bash
|
|
@@ -88,6 +88,12 @@ Send X-Api-Key: ${env.API_KEY} on every request'
|
|
|
88
88
|
|
|
89
89
|
For runs driven from the environment — config-free, or on the global configuration — `EXPLORBOT_KNOWLEDGE` does the same job for the length of one run. See [Agentic usage](./agentic-usage.md).
|
|
90
90
|
|
|
91
|
+
For credentials alone, `--auth` (or `EXPLORBOT_AUTH`) takes free-form text, applies it everywhere, and never writes it to disk:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx explorbot explore /dashboard --auth 'login: admin@example.com password: secret123'
|
|
95
|
+
```
|
|
96
|
+
|
|
91
97
|
## URL Patterns
|
|
92
98
|
|
|
93
99
|
| Pattern | Matches |
|
package/models.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"sambanova-ai-provider": "^1.2.2",
|
|
132
132
|
"strip-ansi": "^7.1.2",
|
|
133
133
|
"turndown": "^7.2.1",
|
|
134
|
+
"wrap-ansi": "^9.0.2",
|
|
134
135
|
"unique-names-generator": "^4.7.1",
|
|
135
136
|
"yaml": "^2.8.3",
|
|
136
137
|
"yargs": "^17.7.2",
|
|
@@ -65,14 +65,8 @@ Trying to fill the form on the page
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
```js
|
|
68
|
-
I.fillField('
|
|
68
|
+
I.fillField('//html/body//form//input[@name="name"]', 'Value');
|
|
69
69
|
```
|
|
70
70
|
</example_output>
|
|
71
71
|
|
|
72
|
-
If
|
|
73
|
-
|
|
74
|
-
<example_output>
|
|
75
|
-
```js
|
|
76
|
-
throw new Error('No resolution');
|
|
77
|
-
```
|
|
78
|
-
</example_output>
|
|
72
|
+
If no solution is possible, call stop() with the reason instead of proposing code.
|
|
@@ -26,6 +26,10 @@ Prefer ARIA locators for reliable element detection.
|
|
|
26
26
|
I.seeElement('.success-message');
|
|
27
27
|
</example>
|
|
28
28
|
|
|
29
|
+
To assert text inside element via `has-text` CSS, prefer using I.see() instead:
|
|
30
|
+
|
|
31
|
+
Prefer `I.see('Collection', '.users')` over `I.seeElement(".users div:has-text("Collection")")`
|
|
32
|
+
|
|
29
33
|
### I.seeInField
|
|
30
34
|
|
|
31
35
|
I.seeInField(<locator>, <value>, <context>)
|
|
@@ -141,8 +145,8 @@ NEVER use `:has-text(...)` inside a seeElement/dontSeeElement locator. Checking
|
|
|
141
145
|
NEVER emit two assertions that check the same fact with different shapes. `I.see(text, locator)` and `I.seeElement("<locator>:has-text('text')")` verify the same thing — pick one (prefer I.see). One claim, one assertion.
|
|
142
146
|
</verification_rules>
|
|
143
147
|
|
|
144
|
-
[
|
|
148
|
+
[Prefer the commands listed here; use other CodeceptJS I.* commands when none of these fits]
|
|
145
149
|
[INTERACT ONLY WITH ELEMENTS THAT ARE ON THE PAGE HTML OR ARIA SNAPSHOT]
|
|
146
150
|
[DO NOT USE WAIT FUNCTIONS]
|
|
147
151
|
|
|
148
|
-
</actions>
|
|
152
|
+
</actions>
|
package/src/action-result.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
|
|
4
3
|
import type { Link, WebPageState } from './state-manager.ts';
|
|
5
4
|
import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
|
|
@@ -742,7 +741,7 @@ export class Diff {
|
|
|
742
741
|
async calculate(): Promise<void> {
|
|
743
742
|
if (!this.previous) return;
|
|
744
743
|
|
|
745
|
-
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html
|
|
744
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html);
|
|
746
745
|
|
|
747
746
|
if (!this._isSameUrl) {
|
|
748
747
|
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|