explorbot 0.1.0 → 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 +93 -36
- package/dist/bin/explorbot-cli.js +71 -16
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -10
- 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.js +29 -10
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher.js +3 -4
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +2 -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 +12 -5
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +20 -5
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +3 -3
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +37 -15
- 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 +19 -0
- package/dist/src/explorer.js +2 -1
- 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/package.json +2 -1
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +7 -9
- 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.ts +28 -9
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher.ts +3 -4
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +2 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +12 -5
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +21 -5
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +3 -3
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +45 -16
- 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 +23 -0
- package/src/explorbot.ts +21 -0
- package/src/explorer.ts +3 -2
- 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/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.'),
|
|
@@ -335,7 +335,7 @@ export function createCodeceptJSTools(explorer, task) {
|
|
|
335
335
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
336
336
|
commandsExecuted: lines.length,
|
|
337
337
|
code: codeBlock,
|
|
338
|
-
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").',
|
|
339
339
|
});
|
|
340
340
|
}
|
|
341
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,27 +8,34 @@ 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
|
|
18
|
-
const
|
|
23
|
+
const { opts } = this.parseArgs(args);
|
|
24
|
+
const isVisual = !!(opts.visual || opts.screenshot);
|
|
25
|
+
await explorer.annotateElements();
|
|
19
26
|
if (isVisual) {
|
|
20
27
|
const cachedResearch = Researcher.getCachedResearch(state);
|
|
21
28
|
const containers = cachedResearch ? extractValidContainers(cachedResearch) : [];
|
|
22
29
|
await explorer.visuallyAnnotateElements({ containers });
|
|
23
30
|
}
|
|
24
|
-
const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual
|
|
31
|
+
const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual });
|
|
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;
|
|
@@ -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,
|
|
@@ -22,11 +22,11 @@ const config = {
|
|
|
22
22
|
|
|
23
23
|
ai: {
|
|
24
24
|
// fast model with tool calling capabilities
|
|
25
|
-
model: openrouter('
|
|
25
|
+
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
26
26
|
// vision model for screenshot analysis
|
|
27
|
-
visionModel: openrouter('
|
|
27
|
+
visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
|
|
28
28
|
// agentic model for decision making
|
|
29
|
-
agenticModel: openrouter('
|
|
29
|
+
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -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 {
|
|
@@ -11,30 +14,49 @@ export class PlanCommand extends BaseCommand {
|
|
|
11
14
|
{ flags: '--focus <feature>', description: 'Focus area for test planning' },
|
|
12
15
|
];
|
|
13
16
|
async execute(args) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const style = styleMatch?.[1];
|
|
18
|
-
const focusMatch = args.match(/--focus\s+("[^"]+"|'[^']+'|\S+)/);
|
|
19
|
-
const focusFromFlag = focusMatch?.[1]?.replace(/^["']|["']$/g, '');
|
|
20
|
-
const focusFromText = args
|
|
21
|
-
.replace('--clear', '')
|
|
22
|
-
.replace('--fresh', '')
|
|
23
|
-
.replace(/--style\s+\S+/, '')
|
|
24
|
-
.replace(/--focus\s+("[^"]+"|'[^']+'|\S+)/, '')
|
|
25
|
-
.trim();
|
|
26
|
-
const focus = focusFromFlag || focusFromText;
|
|
27
|
-
if (clear) {
|
|
17
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
18
|
+
const focus = opts.focus || remaining.join(' ') || undefined;
|
|
19
|
+
if (opts.clear) {
|
|
28
20
|
this.explorBot.clearPlan();
|
|
29
21
|
tag('success').log('Plan cleared');
|
|
30
22
|
}
|
|
31
23
|
if (focus) {
|
|
32
24
|
tag('info').log(`Planning focus: ${focus}`);
|
|
33
25
|
}
|
|
34
|
-
await this.explorBot.plan(focus
|
|
26
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style });
|
|
35
27
|
const plan = this.explorBot.getCurrentPlan();
|
|
36
28
|
if (!plan?.tests.length) {
|
|
37
29
|
throw new Error('No test scenarios in the current plan.');
|
|
38
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');
|
|
39
61
|
}
|
|
40
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";
|
|
@@ -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
|
@@ -244,7 +244,8 @@ class Explorer {
|
|
|
244
244
|
return action;
|
|
245
245
|
}
|
|
246
246
|
async annotateElements() {
|
|
247
|
-
|
|
247
|
+
const { elements } = await annotatePageElements(this.playwrightHelper.page);
|
|
248
|
+
return elements;
|
|
248
249
|
}
|
|
249
250
|
async visuallyAnnotateElements(opts) {
|
|
250
251
|
return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Reflection } from '@codeceptjs/reflection';
|
|
4
|
+
import { ConfigParser } from "./config.js";
|
|
5
|
+
import { normalizeUrl } from "./state-manager.js";
|
|
6
|
+
import { parsePlanFromMarkdown } from "./utils/test-plan-markdown.js";
|
|
7
|
+
import { createDebug } from "./utils/logger.js";
|
|
8
|
+
const debugLog = createDebug('explorbot:suite');
|
|
9
|
+
export class Suite {
|
|
10
|
+
url;
|
|
11
|
+
_automatedTests = null;
|
|
12
|
+
_plannedScenarios = null;
|
|
13
|
+
constructor(url) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
}
|
|
16
|
+
getAutomatedTests() {
|
|
17
|
+
if (this._automatedTests !== null)
|
|
18
|
+
return this._automatedTests;
|
|
19
|
+
this._automatedTests = this.loadAutomatedTests();
|
|
20
|
+
return this._automatedTests;
|
|
21
|
+
}
|
|
22
|
+
getPlannedScenarios() {
|
|
23
|
+
if (this._plannedScenarios !== null)
|
|
24
|
+
return this._plannedScenarios;
|
|
25
|
+
this._plannedScenarios = this.loadPlannedScenarios();
|
|
26
|
+
return this._plannedScenarios;
|
|
27
|
+
}
|
|
28
|
+
getActiveScenarioTitles() {
|
|
29
|
+
return new Set(this.getAutomatedTests()
|
|
30
|
+
.filter((t) => !t.pending)
|
|
31
|
+
.map((t) => t.title.toLowerCase()));
|
|
32
|
+
}
|
|
33
|
+
get automatedTestCount() {
|
|
34
|
+
return this.getAutomatedTests().filter((t) => !t.pending).length;
|
|
35
|
+
}
|
|
36
|
+
getAutomatedTestNames() {
|
|
37
|
+
return this.getAutomatedTests()
|
|
38
|
+
.filter((t) => !t.pending)
|
|
39
|
+
.map((t) => t.title);
|
|
40
|
+
}
|
|
41
|
+
getAutomatedTestFiles() {
|
|
42
|
+
return [...new Set(this.getAutomatedTests().map((t) => t.file))];
|
|
43
|
+
}
|
|
44
|
+
loadAutomatedTests() {
|
|
45
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
46
|
+
if (!existsSync(testsDir))
|
|
47
|
+
return [];
|
|
48
|
+
const jsFiles = readdirSync(testsDir)
|
|
49
|
+
.filter((f) => f.endsWith('.js'))
|
|
50
|
+
.map((f) => path.resolve(testsDir, f));
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const filePath of jsFiles) {
|
|
53
|
+
const parsed = this.parseTestFile(filePath);
|
|
54
|
+
if (!parsed)
|
|
55
|
+
continue;
|
|
56
|
+
if (normalizeUrl(parsed.url) !== normalizeUrl(this.url))
|
|
57
|
+
continue;
|
|
58
|
+
results.push(...parsed.tests);
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
parseTestFile(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
const scanned = Reflection.scanFile(filePath);
|
|
65
|
+
if (!scanned.suites?.length)
|
|
66
|
+
return null;
|
|
67
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
68
|
+
const suiteRef = Reflection.forSuite(scanned.suites[0]);
|
|
69
|
+
const beforeHooks = suiteRef.findHook('Before');
|
|
70
|
+
if (!beforeHooks?.length)
|
|
71
|
+
return null;
|
|
72
|
+
const hookBody = content.slice(beforeHooks[0].range.start, beforeHooks[0].range.end);
|
|
73
|
+
const match = hookBody.match(/I\.amOnPage\(['"]([^'"]+)['"]\)/);
|
|
74
|
+
if (!match)
|
|
75
|
+
return null;
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
const tests = (scanned.tests || []).map((t) => {
|
|
78
|
+
const line = lines[t.line - 1] || '';
|
|
79
|
+
const pending = line.includes('Scenario.skip') || line.includes('Scenario.todo');
|
|
80
|
+
return { title: t.title, pending, file: filePath };
|
|
81
|
+
});
|
|
82
|
+
return { url: match[1], tests };
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
debugLog('Failed to parse test file %s: %s', filePath, err.message);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
loadPlannedScenarios() {
|
|
90
|
+
try {
|
|
91
|
+
const plansDir = ConfigParser.getInstance().getPlansDir();
|
|
92
|
+
if (!existsSync(plansDir))
|
|
93
|
+
return [];
|
|
94
|
+
const mdFiles = readdirSync(plansDir)
|
|
95
|
+
.filter((f) => f.endsWith('.md'))
|
|
96
|
+
.map((f) => path.resolve(plansDir, f));
|
|
97
|
+
const scenarios = [];
|
|
98
|
+
for (const filePath of mdFiles) {
|
|
99
|
+
const plan = parsePlanFromMarkdown(filePath);
|
|
100
|
+
if (!plan.url)
|
|
101
|
+
continue;
|
|
102
|
+
if (normalizeUrl(plan.url) !== normalizeUrl(this.url))
|
|
103
|
+
continue;
|
|
104
|
+
for (const test of plan.tests) {
|
|
105
|
+
scenarios.push(test.scenario);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return scenarios;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
debugLog('Failed to load planned scenarios: %s', err.message);
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/src/utils/html.js
CHANGED
|
@@ -424,18 +424,15 @@ export function htmlMinimalUISnapshot(html, htmlConfig) {
|
|
|
424
424
|
node.attrs = node.attrs.filter((attr) => {
|
|
425
425
|
const { name, value } = attr;
|
|
426
426
|
if (name === 'class') {
|
|
427
|
-
// Remove classes containing digits
|
|
428
427
|
attr.value = value
|
|
429
428
|
.split(' ')
|
|
430
|
-
// remove classes containing digits/
|
|
431
429
|
.filter((className) => !/\d/.test(className))
|
|
432
|
-
// remove popular trash classes
|
|
433
430
|
.filter((className) => !className.match(trashHtmlClasses))
|
|
434
|
-
// remove classes with : and __ in them
|
|
435
431
|
.filter((className) => !className.match(/(:|__)/))
|
|
436
|
-
// remove tailwind utility classes
|
|
437
432
|
.filter((className) => !TAILWIND_CLASS_PATTERNS.some((pattern) => pattern.test(className)))
|
|
438
433
|
.join(' ');
|
|
434
|
+
if (attr.value === '')
|
|
435
|
+
return false;
|
|
439
436
|
}
|
|
440
437
|
return allowedAttrs.includes(name) || name.startsWith('data-explorbot-');
|
|
441
438
|
});
|