explorbot 0.0.5 → 0.1.1
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/bin/explorbot-cli.ts +97 -39
- package/dist/bin/explorbot-cli.js +75 -19
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -7
- package/dist/src/ai/historian.js +34 -3
- package/dist/src/ai/navigator.js +35 -28
- package/dist/src/ai/pilot.js +33 -9
- package/dist/src/ai/planner/subpages.js +42 -6
- package/dist/src/ai/planner.js +44 -13
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/coordinates.js +4 -2
- package/dist/src/ai/researcher/deep-analysis.js +16 -19
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/parser.js +4 -3
- package/dist/src/ai/researcher/research-result.js +2 -0
- package/dist/src/ai/researcher.js +3 -3
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +6 -2
- package/dist/src/commands/add-rule-command.js +1 -2
- package/dist/src/commands/base-command.js +12 -0
- package/dist/src/commands/context-command.js +10 -3
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +21 -6
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +7 -5
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +38 -11
- package/dist/src/commands/rerun-command.js +42 -0
- package/dist/src/commands/research-command.js +10 -4
- package/dist/src/commands/runs-command.js +22 -0
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/test-command.js +3 -3
- package/dist/src/components/App.js +8 -0
- package/dist/src/config.js +3 -0
- package/dist/src/explorbot.js +20 -1
- package/dist/src/explorer.js +59 -16
- package/dist/src/suite.js +115 -0
- package/dist/src/utils/html.js +2 -5
- package/dist/src/utils/rules-loader.js +33 -17
- package/dist/src/utils/test-files.js +103 -0
- package/dist/src/utils/web-element.js +6 -4
- package/package.json +3 -2
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +8 -6
- package/src/ai/historian.ts +37 -3
- package/src/ai/navigator.ts +35 -28
- package/src/ai/pilot.ts +33 -9
- package/src/ai/planner/subpages.ts +37 -7
- package/src/ai/planner.ts +44 -12
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +14 -8
- package/src/ai/researcher/coordinates.ts +8 -7
- package/src/ai/researcher/deep-analysis.ts +18 -21
- package/src/ai/researcher/locators.ts +3 -3
- package/src/ai/researcher/parser.ts +4 -4
- package/src/ai/researcher/research-result.ts +1 -0
- package/src/ai/researcher.ts +3 -3
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +7 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +10 -3
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +22 -6
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +8 -5
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +46 -12
- package/src/commands/rerun-command.ts +46 -0
- package/src/commands/research-command.ts +10 -4
- package/src/commands/runs-command.ts +27 -0
- package/src/commands/start-command.ts +0 -1
- package/src/commands/test-command.ts +3 -3
- package/src/components/App.tsx +8 -0
- package/src/config.ts +24 -0
- package/src/explorbot.ts +22 -1
- package/src/explorer.ts +68 -20
- package/src/suite.ts +135 -0
- package/src/utils/html.ts +1 -5
- package/src/utils/rules-loader.ts +35 -17
- package/src/utils/test-files.ts +122 -0
- package/src/utils/web-element.ts +12 -10
package/dist/src/ai/tools.js
CHANGED
|
@@ -274,7 +274,7 @@ export function createCodeceptJSTools(explorer, task) {
|
|
|
274
274
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
275
275
|
|
|
276
276
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
277
|
-
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
277
|
+
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
278
278
|
`,
|
|
279
279
|
inputSchema: z.object({
|
|
280
280
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -301,7 +301,11 @@ export function createCodeceptJSTools(explorer, task) {
|
|
|
301
301
|
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
302
302
|
const formLocator = codeLines[0] || 'form';
|
|
303
303
|
const action = explorer.createAction();
|
|
304
|
+
const wasInIframe = await explorer.isInsideIframe();
|
|
304
305
|
await action.attempt(codeBlock, explanation);
|
|
306
|
+
if (action.lastError && !wasInIframe && (await explorer.isInsideIframe())) {
|
|
307
|
+
await explorer.switchToMainFrame();
|
|
308
|
+
}
|
|
305
309
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, formLocator);
|
|
306
310
|
if (action.lastError) {
|
|
307
311
|
const message = action.lastError ? String(action.lastError) : 'Unknown error';
|
|
@@ -331,7 +335,7 @@ export function createCodeceptJSTools(explorer, task) {
|
|
|
331
335
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
332
336
|
commandsExecuted: lines.length,
|
|
333
337
|
code: codeBlock,
|
|
334
|
-
suggestion: 'Verify the form was filled in correctly using see() tool.
|
|
338
|
+
suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
|
|
335
339
|
});
|
|
336
340
|
}
|
|
337
341
|
catch (error) {
|
|
@@ -5,8 +5,7 @@ import React from 'react';
|
|
|
5
5
|
import { tag } from '../utils/logger.js';
|
|
6
6
|
import { BaseCommand } from './base-command.js';
|
|
7
7
|
export class AddRuleCommand extends BaseCommand {
|
|
8
|
-
name = '
|
|
9
|
-
aliases = ['add-rule'];
|
|
8
|
+
name = 'add-rule';
|
|
10
9
|
description = 'Create a rule file for an agent';
|
|
11
10
|
suggestions = ['/add-rule researcher check-tooltips'];
|
|
12
11
|
async execute(args) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
export class BaseCommand {
|
|
2
3
|
aliases = [];
|
|
3
4
|
options = [];
|
|
@@ -10,4 +11,15 @@ export class BaseCommand {
|
|
|
10
11
|
matches(commandName) {
|
|
11
12
|
return this.name === commandName || this.aliases.includes(commandName);
|
|
12
13
|
}
|
|
14
|
+
parseArgs(args) {
|
|
15
|
+
const cmd = new Command();
|
|
16
|
+
cmd.exitOverride();
|
|
17
|
+
for (const opt of this.options) {
|
|
18
|
+
cmd.option(opt.flags, opt.description);
|
|
19
|
+
}
|
|
20
|
+
cmd.argument('[args...]');
|
|
21
|
+
const argv = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((s) => s.replace(/^["']|["']$/g, ''));
|
|
22
|
+
cmd.parse(argv, { from: 'user' });
|
|
23
|
+
return { opts: cmd.opts(), args: cmd.args };
|
|
24
|
+
}
|
|
13
25
|
}
|
|
@@ -8,13 +8,20 @@ export class ContextCommand extends BaseCommand {
|
|
|
8
8
|
name = 'context';
|
|
9
9
|
description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
|
|
10
10
|
suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
|
|
11
|
+
options = [
|
|
12
|
+
{ flags: '--visual', description: 'Include annotated screenshot' },
|
|
13
|
+
{ flags: '--screenshot', description: 'Include annotated screenshot' },
|
|
14
|
+
{ flags: '--full', description: 'Show full context with HTML' },
|
|
15
|
+
{ flags: '--attached', description: 'Show attached context mode' },
|
|
16
|
+
];
|
|
11
17
|
async execute(args) {
|
|
12
18
|
const explorer = this.explorBot.getExplorer();
|
|
13
19
|
const state = explorer.getStateManager().getCurrentState();
|
|
14
20
|
if (!state) {
|
|
15
21
|
throw new Error('No active page to show context for');
|
|
16
22
|
}
|
|
17
|
-
const
|
|
23
|
+
const { opts } = this.parseArgs(args);
|
|
24
|
+
const isVisual = !!(opts.visual || opts.screenshot);
|
|
18
25
|
await explorer.annotateElements();
|
|
19
26
|
if (isVisual) {
|
|
20
27
|
const cachedResearch = Researcher.getCachedResearch(state);
|
|
@@ -25,10 +32,10 @@ export class ContextCommand extends BaseCommand {
|
|
|
25
32
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
26
33
|
const knowledgeTracker = this.explorBot.getKnowledgeTracker();
|
|
27
34
|
let mode = 'compact';
|
|
28
|
-
if (
|
|
35
|
+
if (opts.full) {
|
|
29
36
|
mode = 'full';
|
|
30
37
|
}
|
|
31
|
-
else if (
|
|
38
|
+
else if (opts.attached) {
|
|
32
39
|
mode = 'attached';
|
|
33
40
|
}
|
|
34
41
|
const contextData = {
|
|
@@ -2,7 +2,6 @@ import { BaseCommand } from './base-command.js';
|
|
|
2
2
|
export class DrillCommand extends BaseCommand {
|
|
3
3
|
name = 'drill';
|
|
4
4
|
description = 'Drill all components on current page to learn interactions';
|
|
5
|
-
aliases = ['bosun'];
|
|
6
5
|
suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
|
|
7
6
|
async execute(args) {
|
|
8
7
|
const knowledgePath = this.parseKnowledgeArg(args);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
1
2
|
import figureSet from 'figures';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getStyles } from '../ai/planner/styles.js';
|
|
5
|
+
import { ConfigParser } from "../config.js";
|
|
4
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
5
7
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
6
8
|
import { tag } from '../utils/logger.js';
|
|
@@ -14,12 +16,11 @@ export class ExploreCommand extends BaseCommand {
|
|
|
14
16
|
testsRun = 0;
|
|
15
17
|
completedPlans = [];
|
|
16
18
|
async execute(args) {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
this.maxTests = Number.parseInt(
|
|
20
|
-
args = args.replace(/--max-tests\s+\d+/, '').trim();
|
|
19
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
20
|
+
if (opts.maxTests) {
|
|
21
|
+
this.maxTests = Number.parseInt(opts.maxTests, 10);
|
|
21
22
|
}
|
|
22
|
-
const feature =
|
|
23
|
+
const feature = remaining.join(' ') || undefined;
|
|
23
24
|
const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
|
|
24
25
|
await this.runAllStyles(mainUrl, feature);
|
|
25
26
|
const mainPlan = this.explorBot.getCurrentPlan();
|
|
@@ -56,6 +57,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
56
57
|
await this.explorBot.visit(mainUrl);
|
|
57
58
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
58
59
|
this.printResults(savedPath);
|
|
60
|
+
this.printRerunSuggestions();
|
|
59
61
|
}
|
|
60
62
|
async runAllStyles(pageUrl, feature, parentPlan, completedPlans) {
|
|
61
63
|
let fresh = true;
|
|
@@ -72,7 +74,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
printResults(savedPath) {
|
|
75
|
-
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.
|
|
77
|
+
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
|
|
76
78
|
if (allTests.length === 0)
|
|
77
79
|
return;
|
|
78
80
|
const hasSubPages = this.completedPlans.length > 1;
|
|
@@ -107,6 +109,19 @@ export class ExploreCommand extends BaseCommand {
|
|
|
107
109
|
tag('info').log(`Re-run tests: ${getCliName()} test ${relativePath} <index>`);
|
|
108
110
|
}
|
|
109
111
|
}
|
|
112
|
+
printRerunSuggestions() {
|
|
113
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
114
|
+
if (!existsSync(testsDir))
|
|
115
|
+
return;
|
|
116
|
+
const testFiles = readdirSync(testsDir).filter((f) => f.endsWith('.js'));
|
|
117
|
+
if (testFiles.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
for (const file of testFiles) {
|
|
120
|
+
tag('info').log(`Generated: ${file}`);
|
|
121
|
+
}
|
|
122
|
+
tag('info').log(`List tests: ${getCliName()} runs`);
|
|
123
|
+
tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
|
|
124
|
+
}
|
|
110
125
|
isLimitReached() {
|
|
111
126
|
return this.maxTests != null && this.testsRun >= this.maxTests;
|
|
112
127
|
}
|
|
@@ -16,7 +16,14 @@ export class FreesailCommand extends BaseCommand {
|
|
|
16
16
|
{ flags: '--max-tests <number>', description: 'Maximum number of tests to run' },
|
|
17
17
|
];
|
|
18
18
|
async execute(args) {
|
|
19
|
-
const {
|
|
19
|
+
const { opts } = this.parseArgs(args);
|
|
20
|
+
let strategy;
|
|
21
|
+
if (opts.deep)
|
|
22
|
+
strategy = 'deep';
|
|
23
|
+
if (opts.shallow)
|
|
24
|
+
strategy = 'shallow';
|
|
25
|
+
const scope = opts.scope;
|
|
26
|
+
const maxTests = opts.maxTests ? Number.parseInt(opts.maxTests, 10) : undefined;
|
|
20
27
|
await this.explorBot.visitInitialState();
|
|
21
28
|
let testsRun = 0;
|
|
22
29
|
await loop(async (ctx) => {
|
|
@@ -60,24 +67,3 @@ export class FreesailCommand extends BaseCommand {
|
|
|
60
67
|
}, { maxAttempts: Number.POSITIVE_INFINITY });
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
|
-
function parseArgs(args) {
|
|
64
|
-
const parts = args.trim().split(/\s+/);
|
|
65
|
-
let strategy;
|
|
66
|
-
let scope;
|
|
67
|
-
let maxTests;
|
|
68
|
-
for (let i = 0; i < parts.length; i++) {
|
|
69
|
-
if (parts[i] === '--deep')
|
|
70
|
-
strategy = 'deep';
|
|
71
|
-
if (parts[i] === '--shallow')
|
|
72
|
-
strategy = 'shallow';
|
|
73
|
-
if (parts[i] === '--scope' && parts[i + 1]) {
|
|
74
|
-
scope = parts[i + 1];
|
|
75
|
-
i++;
|
|
76
|
-
}
|
|
77
|
-
if (parts[i] === '--max-tests' && parts[i + 1]) {
|
|
78
|
-
maxTests = Number.parseInt(parts[i + 1], 10);
|
|
79
|
-
i++;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return { strategy, scope, maxTests };
|
|
83
|
-
}
|
|
@@ -22,7 +22,9 @@ import { PlanEditCommand } from './plan-edit-command.js';
|
|
|
22
22
|
import { PlanLoadCommand } from './plan-load-command.js';
|
|
23
23
|
import { PlanReloadCommand } from './plan-reload-command.js';
|
|
24
24
|
import { PlanSaveCommand } from './plan-save-command.js';
|
|
25
|
+
import { RerunCommand } from './rerun-command.js';
|
|
25
26
|
import { ResearchCommand } from './research-command.js';
|
|
27
|
+
import { RunsCommand } from './runs-command.js';
|
|
26
28
|
import { StartCommand } from './start-command.js';
|
|
27
29
|
import { StatusCommand } from "./status-command.js";
|
|
28
30
|
import { TestCommand } from './test-command.js';
|
|
@@ -53,6 +55,8 @@ const commandClasses = [
|
|
|
53
55
|
ContextExperienceCommand,
|
|
54
56
|
ContextDataCommand,
|
|
55
57
|
TestCommand,
|
|
58
|
+
RunsCommand,
|
|
59
|
+
RerunCommand,
|
|
56
60
|
StatusCommand,
|
|
57
61
|
DebugCommand,
|
|
58
62
|
ExitCommand,
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import { log, tag } from '../utils/logger.js';
|
|
4
4
|
import dedent from 'dedent';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
6
7
|
const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
7
8
|
// import { '<your provider here>' } from '<your provider package here>';
|
|
@@ -21,11 +22,11 @@ const config = {
|
|
|
21
22
|
|
|
22
23
|
ai: {
|
|
23
24
|
// fast model with tool calling capabilities
|
|
24
|
-
model: openrouter('
|
|
25
|
+
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
25
26
|
// vision model for screenshot analysis
|
|
26
|
-
visionModel: openrouter('
|
|
27
|
+
visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
|
|
27
28
|
// agentic model for decision making
|
|
28
|
-
agenticModel: openrouter('
|
|
29
|
+
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
|
|
@@ -95,9 +96,10 @@ export function runInitCommand(options) {
|
|
|
95
96
|
log('2. Set AI models config file');
|
|
96
97
|
log('3. Set web application URL in the config file');
|
|
97
98
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
98
|
-
tag('substep').log(`${getCliName()} learn * 'to aurhorize use these credentials: admin@example.com / secret123'`);
|
|
99
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to aurhorize use these credentials: admin@example.com / secret123'`));
|
|
100
|
+
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
99
101
|
log('5. Launch application on a relative URL');
|
|
100
|
-
tag('substep').log(`${getCliName()} start /dashboard`);
|
|
102
|
+
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
101
103
|
if (!existsSync('./output')) {
|
|
102
104
|
mkdirSync('./output', { recursive: true });
|
|
103
105
|
log('Created directory: ./output');
|
|
@@ -5,7 +5,8 @@ export class PathCommand extends BaseCommand {
|
|
|
5
5
|
description = 'Display ASCII graph of navigation paths during the session';
|
|
6
6
|
options = [{ flags: '--links', description: 'Show outgoing links from each page' }];
|
|
7
7
|
async execute(args) {
|
|
8
|
-
const
|
|
8
|
+
const { opts } = this.parseArgs(args);
|
|
9
|
+
const showLinks = !!opts.links;
|
|
9
10
|
const stateManager = this.explorBot.getExplorer().getStateManager();
|
|
10
11
|
const history = stateManager.getStateHistory();
|
|
11
12
|
if (history.length === 0) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figureSet from 'figures';
|
|
1
4
|
import { tag } from '../utils/logger.js';
|
|
2
5
|
import { BaseCommand } from './base-command.js';
|
|
3
6
|
export class PlanCommand extends BaseCommand {
|
|
@@ -8,28 +11,52 @@ export class PlanCommand extends BaseCommand {
|
|
|
8
11
|
{ flags: '--fresh', description: 'Regenerate plan from scratch' },
|
|
9
12
|
{ flags: '--clear', description: 'Clear plan before regenerating' },
|
|
10
13
|
{ flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
|
|
14
|
+
{ flags: '--focus <feature>', description: 'Focus area for test planning' },
|
|
11
15
|
];
|
|
12
16
|
async execute(args) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const style = styleMatch?.[1];
|
|
17
|
-
const focus = args
|
|
18
|
-
.replace('--clear', '')
|
|
19
|
-
.replace('--fresh', '')
|
|
20
|
-
.replace(/--style\s+\S+/, '')
|
|
21
|
-
.trim();
|
|
22
|
-
if (clear) {
|
|
17
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
18
|
+
const focus = opts.focus || remaining.join(' ') || undefined;
|
|
19
|
+
if (opts.clear) {
|
|
23
20
|
this.explorBot.clearPlan();
|
|
24
21
|
tag('success').log('Plan cleared');
|
|
25
22
|
}
|
|
26
23
|
if (focus) {
|
|
27
24
|
tag('info').log(`Planning focus: ${focus}`);
|
|
28
25
|
}
|
|
29
|
-
await this.explorBot.plan(focus
|
|
26
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style });
|
|
30
27
|
const plan = this.explorBot.getCurrentPlan();
|
|
31
28
|
if (!plan?.tests.length) {
|
|
32
29
|
throw new Error('No test scenarios in the current plan.');
|
|
33
30
|
}
|
|
31
|
+
this.printPlanSummary();
|
|
32
|
+
this.updateSuggestions();
|
|
33
|
+
}
|
|
34
|
+
printPlanSummary() {
|
|
35
|
+
const suite = this.explorBot.getSuite();
|
|
36
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
37
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
38
|
+
const names = suite.getAutomatedTestNames();
|
|
39
|
+
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
40
|
+
for (let i = 0; i < names.length; i++) {
|
|
41
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (plan?.tests.length) {
|
|
45
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
46
|
+
for (let i = 0; i < plan.tests.length; i++) {
|
|
47
|
+
const t = plan.tests[i];
|
|
48
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
updateSuggestions() {
|
|
53
|
+
this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
|
|
54
|
+
const suite = this.explorBot.getSuite();
|
|
55
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
56
|
+
for (const f of suite.getAutomatedTestFiles()) {
|
|
57
|
+
this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
|
|
34
61
|
}
|
|
35
62
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ConfigParser } from "../config.js";
|
|
4
|
+
import { tag } from "../utils/logger.js";
|
|
5
|
+
import { BaseCommand } from './base-command.js';
|
|
6
|
+
export class RerunCommand extends BaseCommand {
|
|
7
|
+
name = 'rerun';
|
|
8
|
+
description = 'Re-run generated tests with AI auto-healing';
|
|
9
|
+
tuiEnabled = true;
|
|
10
|
+
async execute(args) {
|
|
11
|
+
const { args: remaining } = this.parseArgs(args);
|
|
12
|
+
const filename = remaining[0];
|
|
13
|
+
const indexArg = remaining[1];
|
|
14
|
+
if (!filename) {
|
|
15
|
+
tag('error').log('Usage: /rerun <filename> [index]');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let filePath = resolve(filename);
|
|
19
|
+
if (!existsSync(filePath)) {
|
|
20
|
+
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
21
|
+
}
|
|
22
|
+
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
23
|
+
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseTestIndices(input) {
|
|
27
|
+
if (input === '*' || input === 'all')
|
|
28
|
+
return [];
|
|
29
|
+
const indices = new Set();
|
|
30
|
+
for (const part of input.split(',')) {
|
|
31
|
+
const trimmed = part.trim();
|
|
32
|
+
const range = trimmed.match(/^(\d+)-(\d+)$/);
|
|
33
|
+
if (range) {
|
|
34
|
+
for (let i = Number.parseInt(range[1]); i <= Number.parseInt(range[2]); i++)
|
|
35
|
+
indices.add(i - 1);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
indices.add(Number.parseInt(trimmed) - 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...indices].sort((a, b) => a - b);
|
|
42
|
+
}
|
|
@@ -6,11 +6,17 @@ export class ResearchCommand extends BaseCommand {
|
|
|
6
6
|
name = 'research';
|
|
7
7
|
description = 'Research current page or navigate to URI and research. Use --deep to explore interactive elements by clicking them. Use --data to include page data.';
|
|
8
8
|
suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
|
|
9
|
+
options = [
|
|
10
|
+
{ flags: '--data', description: 'Include page data' },
|
|
11
|
+
{ flags: '--deep', description: 'Explore interactive elements by clicking them' },
|
|
12
|
+
{ flags: '--no-fix', description: 'Skip fixing research issues' },
|
|
13
|
+
];
|
|
9
14
|
async execute(args) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
15
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
16
|
+
const includeData = !!opts.data;
|
|
17
|
+
const enableDeep = !!opts.deep;
|
|
18
|
+
const noFix = !!opts.noFix;
|
|
19
|
+
const target = remaining.join(' ');
|
|
14
20
|
if (target) {
|
|
15
21
|
await this.explorBot.agentNavigator().visit(target);
|
|
16
22
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as codeceptjs from 'codeceptjs';
|
|
2
|
+
import { ConfigParser } from "../config.js";
|
|
3
|
+
import { dryRunTestFile, loadTestSuites, printTestList } from "../utils/test-files.js";
|
|
4
|
+
import { BaseCommand } from './base-command.js';
|
|
5
|
+
export class RunsCommand extends BaseCommand {
|
|
6
|
+
name = 'runs';
|
|
7
|
+
description = 'List generated test files and their scenarios';
|
|
8
|
+
tuiEnabled = true;
|
|
9
|
+
async execute(args) {
|
|
10
|
+
if (!this.explorBot.isExploring) {
|
|
11
|
+
codeceptjs.container.create({ helpers: {} }, {});
|
|
12
|
+
}
|
|
13
|
+
const { args: remaining } = this.parseArgs(args);
|
|
14
|
+
const filePath = remaining[0];
|
|
15
|
+
if (filePath) {
|
|
16
|
+
await dryRunTestFile(filePath);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const suites = loadTestSuites(ConfigParser.getInstance().getTestsDir());
|
|
20
|
+
printTestList(suites);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -2,7 +2,6 @@ import { BaseCommand } from './base-command.js';
|
|
|
2
2
|
import { ExploreCommand } from './explore-command.js';
|
|
3
3
|
export class StartCommand extends BaseCommand {
|
|
4
4
|
name = 'start';
|
|
5
|
-
aliases = ['sail'];
|
|
6
5
|
description = 'Start web exploration';
|
|
7
6
|
suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
|
|
8
7
|
async execute(args) {
|
|
@@ -24,10 +24,10 @@ export class TestCommand extends BaseCommand {
|
|
|
24
24
|
toExecute.push(...requirePlan().getPendingTests());
|
|
25
25
|
}
|
|
26
26
|
else if (args.match(/^[\d,\-\s]+$/)) {
|
|
27
|
-
const
|
|
28
|
-
const indices = parseTestIndices(args,
|
|
27
|
+
const visible = requirePlan().tests.filter((t) => t.enabled);
|
|
28
|
+
const indices = parseTestIndices(args, visible.length);
|
|
29
29
|
for (const idx of indices) {
|
|
30
|
-
toExecute.push(
|
|
30
|
+
toExecute.push(visible[idx]);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
@@ -206,6 +206,14 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
206
206
|
setShowPlanEditor(true);
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
|
+
if (key.upArrow) {
|
|
210
|
+
setTaskScrollOffset((prev) => Math.max(0, prev - 1));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (key.downArrow) {
|
|
214
|
+
setTaskScrollOffset((prev) => prev + 1);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
209
217
|
}
|
|
210
218
|
if (!showInput && !showPlanEditor) {
|
|
211
219
|
if (key.upArrow) {
|
package/dist/src/config.js
CHANGED
package/dist/src/explorbot.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Planner } from "./ai/planner.js";
|
|
|
14
14
|
import { AIProvider } from "./ai/provider.js";
|
|
15
15
|
import { Quartermaster } from "./ai/quartermaster.js";
|
|
16
16
|
import { Researcher } from "./ai/researcher.js";
|
|
17
|
+
import { Rerunner } from "./ai/rerunner.js";
|
|
17
18
|
import { Tester } from "./ai/tester.js";
|
|
18
19
|
import { createAgentTools } from "./ai/tools.js";
|
|
19
20
|
import { ConfigParser } from "./config.js";
|
|
@@ -84,7 +85,7 @@ export class ExplorBot {
|
|
|
84
85
|
await this.explorer.openFreshTab();
|
|
85
86
|
}
|
|
86
87
|
getCurrentState() {
|
|
87
|
-
return this.explorer
|
|
88
|
+
return this.explorer?.getStateManager().getCurrentState() ?? null;
|
|
88
89
|
}
|
|
89
90
|
getExplorer() {
|
|
90
91
|
return this.explorer;
|
|
@@ -202,6 +203,21 @@ export class ExplorBot {
|
|
|
202
203
|
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager());
|
|
203
204
|
}));
|
|
204
205
|
}
|
|
206
|
+
agentRerunner() {
|
|
207
|
+
if (!this.agents.rerunner) {
|
|
208
|
+
this.agents.rerunner = this.createAgent(({ ai, explorer }) => {
|
|
209
|
+
const researcher = this.agentResearcher();
|
|
210
|
+
const navigator = this.agentNavigator();
|
|
211
|
+
const tools = createAgentTools({ explorer, researcher, navigator });
|
|
212
|
+
return new Rerunner(explorer, ai, tools);
|
|
213
|
+
});
|
|
214
|
+
const qm = this.agentQuartermaster();
|
|
215
|
+
if (qm)
|
|
216
|
+
this.agents.rerunner.setQuartermaster(qm);
|
|
217
|
+
this.agents.rerunner.setHistorian(this.agentHistorian());
|
|
218
|
+
}
|
|
219
|
+
return this.agents.rerunner;
|
|
220
|
+
}
|
|
205
221
|
agentBosun() {
|
|
206
222
|
return (this.agents.bosun ||= this.createAgent(({ ai, explorer }) => {
|
|
207
223
|
const researcher = this.agentResearcher();
|
|
@@ -243,6 +259,9 @@ export class ExplorBot {
|
|
|
243
259
|
getCurrentPlan() {
|
|
244
260
|
return this.currentPlan;
|
|
245
261
|
}
|
|
262
|
+
getSuite() {
|
|
263
|
+
return this.agentPlanner().getSuite();
|
|
264
|
+
}
|
|
246
265
|
getPlanFeature() {
|
|
247
266
|
return this.planFeature;
|
|
248
267
|
}
|
package/dist/src/explorer.js
CHANGED
|
@@ -15,6 +15,7 @@ import { StateManager } from './state-manager.js';
|
|
|
15
15
|
import { RequestStore } from "./api/request-store.js";
|
|
16
16
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
17
17
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
18
|
+
import { WebElement, extractElementData } from "./utils/web-element.js";
|
|
18
19
|
const debugLog = createDebug('explorbot:explorer');
|
|
19
20
|
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
20
21
|
class Explorer {
|
|
@@ -243,19 +244,8 @@ class Explorer {
|
|
|
243
244
|
return action;
|
|
244
245
|
}
|
|
245
246
|
async annotateElements() {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
let idx = 1;
|
|
249
|
-
for (const role of roles) {
|
|
250
|
-
const elements = await page.getByRole(role).all();
|
|
251
|
-
for (const el of elements) {
|
|
252
|
-
await el.evaluate((node, i) => {
|
|
253
|
-
node.setAttribute('data-explorbot-eidx', String(i));
|
|
254
|
-
}, idx);
|
|
255
|
-
idx++;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return idx - 1;
|
|
247
|
+
const { elements } = await annotatePageElements(this.playwrightHelper.page);
|
|
248
|
+
return elements;
|
|
259
249
|
}
|
|
260
250
|
async visuallyAnnotateElements(opts) {
|
|
261
251
|
return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);
|
|
@@ -269,7 +259,7 @@ class Explorer {
|
|
|
269
259
|
for (const el of elements) {
|
|
270
260
|
const attr = await el.getAttribute('data-explorbot-eidx');
|
|
271
261
|
if (attr)
|
|
272
|
-
result.push(
|
|
262
|
+
result.push(attr);
|
|
273
263
|
}
|
|
274
264
|
return result;
|
|
275
265
|
}
|
|
@@ -286,8 +276,7 @@ class Explorer {
|
|
|
286
276
|
const page = this.playwrightHelper.page;
|
|
287
277
|
const base = container ? page.locator(container) : page;
|
|
288
278
|
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
289
|
-
|
|
290
|
-
return eidx ? Number.parseInt(eidx, 10) : null;
|
|
279
|
+
return await el.first().getAttribute('data-explorbot-eidx');
|
|
291
280
|
}
|
|
292
281
|
catch (error) {
|
|
293
282
|
if (this.isFatalBrowserError(error)) {
|
|
@@ -607,4 +596,58 @@ function toCodeceptjsTest(test) {
|
|
|
607
596
|
codeceptjsTest.notes = test.getPrintableNotes();
|
|
608
597
|
return codeceptjsTest;
|
|
609
598
|
}
|
|
599
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
600
|
+
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
601
|
+
function parseAriaRefs(ariaSnapshot) {
|
|
602
|
+
const entries = [];
|
|
603
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
604
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
605
|
+
if (!match)
|
|
606
|
+
continue;
|
|
607
|
+
if (!ANNOTATABLE_ROLES.has(match[2]))
|
|
608
|
+
continue;
|
|
609
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
610
|
+
}
|
|
611
|
+
return entries;
|
|
612
|
+
}
|
|
613
|
+
export async function annotatePageElements(page) {
|
|
614
|
+
const ariaSnapshot = await page.locator('body').ariaSnapshot({ forAI: true });
|
|
615
|
+
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
616
|
+
const byRole = new Map();
|
|
617
|
+
for (const { role, name, ref } of refEntries) {
|
|
618
|
+
let list = byRole.get(role);
|
|
619
|
+
if (!list) {
|
|
620
|
+
list = [];
|
|
621
|
+
byRole.set(role, list);
|
|
622
|
+
}
|
|
623
|
+
list.push({ name, ref });
|
|
624
|
+
}
|
|
625
|
+
const elements = [];
|
|
626
|
+
for (const [role, entries] of byRole) {
|
|
627
|
+
try {
|
|
628
|
+
const rawList = await page.getByRole(role).evaluateAll((domElements, [data, extractFnStr]) => {
|
|
629
|
+
const extract = new Function(`return ${extractFnStr}`)();
|
|
630
|
+
const results = [];
|
|
631
|
+
let ariaIdx = 0;
|
|
632
|
+
for (const el of domElements) {
|
|
633
|
+
if (ariaIdx >= data.length)
|
|
634
|
+
break;
|
|
635
|
+
el.setAttribute('data-explorbot-eidx', data[ariaIdx].ref);
|
|
636
|
+
const elData = extract(el);
|
|
637
|
+
if (elData)
|
|
638
|
+
results.push(elData);
|
|
639
|
+
ariaIdx++;
|
|
640
|
+
}
|
|
641
|
+
return results;
|
|
642
|
+
}, [entries, extractElementData.toString()]);
|
|
643
|
+
for (const raw of rawList) {
|
|
644
|
+
elements.push(WebElement.fromRawData(raw, role));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
debugLog(`Failed to annotate role=${role}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return { ariaSnapshot, elements };
|
|
652
|
+
}
|
|
610
653
|
export default Explorer;
|