explorbot 0.2.0 → 0.2.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/README.md +21 -5
- package/bin/explorbot-cli.ts +29 -7
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +5 -2
- package/boat/doc-collector/src/ai/tools.ts +12 -11
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +3 -3
- package/boat/doc-collector/src/screenshots.ts +2 -2
- package/dist/bin/explorbot-cli.js +26 -6
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +4 -2
- package/dist/boat/doc-collector/src/ai/tools.js +11 -11
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +3 -3
- package/dist/boat/doc-collector/src/screenshots.js +2 -2
- package/dist/models.json +30 -0
- package/dist/package.json +5 -2
- package/dist/src/action.d.ts +11 -3
- package/dist/src/action.js +22 -5
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -22
- package/dist/src/ai/captain.js +13 -26
- package/dist/src/ai/driller.d.ts +2 -13
- package/dist/src/ai/driller.js +23 -38
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.d.ts +3 -2
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/navigator.d.ts +6 -2
- package/dist/src/ai/navigator.js +26 -22
- package/dist/src/ai/pilot.d.ts +13 -6
- package/dist/src/ai/pilot.js +49 -44
- package/dist/src/ai/planner.d.ts +3 -2
- package/dist/src/ai/planner.js +19 -17
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.d.ts +3 -2
- package/dist/src/ai/quartermaster.js +4 -4
- package/dist/src/ai/rerunner.d.ts +2 -11
- package/dist/src/ai/rerunner.js +9 -22
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +6 -6
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +2 -2
- package/dist/src/ai/researcher.js +29 -54
- package/dist/src/ai/task-agent.d.ts +18 -5
- package/dist/src/ai/task-agent.js +35 -6
- package/dist/src/ai/tester.d.ts +6 -12
- package/dist/src/ai/tester.js +37 -51
- package/dist/src/ai/tools.d.ts +11 -10
- package/dist/src/ai/tools.js +24 -36
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +7 -5
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/learn-command.js +4 -4
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/App.js +1 -1
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +144 -10
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.d.ts +17 -8
- package/dist/src/explorbot.js +74 -36
- package/dist/src/explorer.d.ts +66 -91
- package/dist/src/explorer.js +326 -567
- package/dist/src/state-manager.d.ts +6 -1
- package/dist/src/state-manager.js +1 -0
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +1 -0
- package/dist/src/utils/web-element.js +29 -0
- package/models.json +30 -0
- package/package.json +5 -2
- package/src/action.ts +25 -5
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -23
- package/src/ai/captain.ts +12 -25
- package/src/ai/driller.ts +50 -67
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +4 -3
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian.ts +3 -2
- package/src/ai/navigator.ts +30 -25
- package/src/ai/pilot.ts +55 -47
- package/src/ai/planner.ts +22 -20
- package/src/ai/provider.ts +0 -1
- package/src/ai/quartermaster.ts +5 -4
- package/src/ai/rerunner.ts +10 -29
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +8 -6
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/sections.ts +4 -2
- package/src/ai/researcher.ts +32 -61
- package/src/ai/task-agent.ts +45 -11
- package/src/ai/tester.ts +40 -57
- package/src/ai/tools.ts +32 -53
- package/src/command-handler.ts +1 -1
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +7 -5
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/learn-command.ts +4 -4
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/App.tsx +1 -1
- package/src/config.ts +173 -11
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +78 -39
- package/src/explorer.ts +389 -628
- package/src/state-manager.ts +7 -1
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +30 -0
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx explorbot start https://your-app.com
|
|
|
14
14
|
|
|
15
15
|
It runs with no babysitting and reports back what it finds. This is vibe-testing.
|
|
16
16
|
|
|
17
|
+
Explorbot works with any AI provider through the [Vercel AI SDK](https://sdk.vercel.ai/providers). See [`models.json`](models.json) for the current recommended provider and model setup, and [Providers](docs/basics/providers.md) for how to configure each one.
|
|
18
|
+
|
|
17
19
|
New here? Read the [Getting Started guide](docs/basics/getting-started.md).
|
|
18
20
|
|
|
19
21
|
## Use Cases
|
|
@@ -92,7 +94,7 @@ Explorbot won't replace your regression tests — it covers what they can't. You
|
|
|
92
94
|
## Requirements
|
|
93
95
|
|
|
94
96
|
- Node.js 24+ or **Bun**
|
|
95
|
-
- An **AI provider key** — OpenRouter recommended; Groq, Cerebras, OpenAI, Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
|
|
97
|
+
- An **AI provider key** — OpenRouter recommended; Groq, Cerebras, [OpenAI](docs/basics/providers.md#openai), Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
|
|
96
98
|
- A **modern terminal** — iTerm2, WARP, Kitty, Ghostty, or Windows Terminal with WSL
|
|
97
99
|
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/basics/prerequisites.md)
|
|
98
100
|
|
|
@@ -125,6 +127,19 @@ Type `/explore`, and Explorbot runs its loop on its own — research, plan, test
|
|
|
125
127
|
|
|
126
128
|
That's the gist. The [**Getting Started guide**](docs/basics/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
|
|
127
129
|
|
|
130
|
+
### Or skip the config file
|
|
131
|
+
|
|
132
|
+
For a CI job, a demo, or a coding agent, pass everything as environment variables. Name a provider and Explorbot picks its recommended models:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
EXPLORBOT_URL=https://app.example.com \
|
|
136
|
+
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
137
|
+
EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
|
|
138
|
+
npx explorbot explore /admin/users --max-tests 3
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Output lands in a temp directory and nothing is written to your project. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
142
|
+
|
|
128
143
|
## Teaching Explorbot
|
|
129
144
|
|
|
130
145
|
Explorbot gets better when you tell it about your app:
|
|
@@ -146,12 +161,13 @@ When you're ready to go deeper, the [full documentation](docs/) covers everythin
|
|
|
146
161
|
## FAQ
|
|
147
162
|
|
|
148
163
|
**Can I run it in Cursor or Claude Code?**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
> However, Explorbot can be used as a subagent or terminal command controlled by a coding agent.
|
|
164
|
+
Not as a replacement — Explorbot is a separate application designed for constant testing, while Cursor, Codex, and Claude Code are coding agents. But a coding agent can drive Explorbot as a terminal command or subagent: it writes the test plan, Explorbot executes it against the real app. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
152
165
|
|
|
153
166
|
**Can I bring a Cursor or OpenAI subscription?**
|
|
154
|
-
No.
|
|
167
|
+
No. Explorbot needs an API key, not a chat subscription. Use pay-per-token access — Groq, OpenRouter, or OpenAI's own API.
|
|
168
|
+
|
|
169
|
+
**Can I use OpenAI directly?**
|
|
170
|
+
Yes. Add your `OPENAI_API_KEY` and point the models at OpenAI — a nano-class model for `model` and `visionModel`, a stronger one for `agenticModel`. Expect it to run a bit slower than hosted OSS models on Groq or Cerebras. See [Providers](docs/basics/providers.md#openai) for the config.
|
|
155
171
|
|
|
156
172
|
**I want to use Opus!!!**
|
|
157
173
|
Opus is great for coding. Testing needs a simpler model that can safely consume lots of HTML tokens, fast. Save the expensive models for sophisticated decision-making.
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
-
import { ConfigParser } from '../src/config.js';
|
|
12
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
14
|
import { Stats } from '../src/stats.js';
|
|
15
15
|
import { Plan } from '../src/test-plan.js';
|
|
@@ -43,8 +43,6 @@ interface CLIOptions {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
|
|
46
|
-
const sessionFile = options.session === true ? path.join(path.resolve(options.path || process.cwd()), 'output', 'session.json') : options.session;
|
|
47
|
-
|
|
48
46
|
return {
|
|
49
47
|
from,
|
|
50
48
|
verbose: options.verbose || options.debug,
|
|
@@ -53,7 +51,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
53
51
|
show: options.show,
|
|
54
52
|
headless: options.headless,
|
|
55
53
|
incognito: options.incognito,
|
|
56
|
-
session:
|
|
54
|
+
session: options.session,
|
|
57
55
|
} as ExplorBotOptions;
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -690,7 +688,7 @@ program
|
|
|
690
688
|
path: options.path,
|
|
691
689
|
config: options.config,
|
|
692
690
|
headless: true,
|
|
693
|
-
session: options.session
|
|
691
|
+
session: options.session,
|
|
694
692
|
};
|
|
695
693
|
|
|
696
694
|
const explorBot = new ExplorBot(mainOptions);
|
|
@@ -720,11 +718,11 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
720
718
|
await explorBot.start();
|
|
721
719
|
await explorBot.agentNavigator().visit(url);
|
|
722
720
|
|
|
723
|
-
const action = explorBot.getExplorer().
|
|
721
|
+
const action = explorBot.getExplorer().action();
|
|
724
722
|
await action.execute(command);
|
|
725
723
|
|
|
726
724
|
log('Command executed successfully');
|
|
727
|
-
const state = explorBot.
|
|
725
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
728
726
|
if (state) log(`URL: ${state.url}`);
|
|
729
727
|
|
|
730
728
|
await explorBot.stop();
|
|
@@ -876,4 +874,28 @@ import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
|
876
874
|
program.addCommand(createApiCommands('api'));
|
|
877
875
|
program.addCommand(createDocsCommands('docs'));
|
|
878
876
|
|
|
877
|
+
const envHelp = () => {
|
|
878
|
+
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
879
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
880
|
+
|
|
881
|
+
return `
|
|
882
|
+
Environment variables (config-free one-liner mode):
|
|
883
|
+
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
884
|
+
|
|
885
|
+
${rows}
|
|
886
|
+
|
|
887
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
888
|
+
Example:
|
|
889
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
890
|
+
${cli} explore /login --max-tests 3
|
|
891
|
+
`;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const addEnvHelp = (cmd: Command) => {
|
|
895
|
+
cmd.addHelpText('after', envHelp);
|
|
896
|
+
for (const sub of cmd.commands) addEnvHelp(sub);
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
addEnvHelp(program);
|
|
900
|
+
|
|
879
901
|
program.parse();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
|
-
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS } from '../../../src/config.ts';
|
|
4
|
+
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
5
5
|
|
|
6
6
|
export type { AIConfig };
|
|
7
7
|
|
|
@@ -54,8 +54,11 @@ export class ApibotConfigParser {
|
|
|
54
54
|
|
|
55
55
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
56
56
|
if (!resolvedPath) {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
try {
|
|
58
|
+
return await this.loadEnvConfig();
|
|
59
|
+
} finally {
|
|
60
|
+
if (options?.path && originalCwd !== process.cwd()) process.chdir(originalCwd);
|
|
61
|
+
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
try {
|
|
@@ -123,6 +126,45 @@ export class ApibotConfigParser {
|
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
private async loadEnvConfig(): Promise<ApibotConfig> {
|
|
130
|
+
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
131
|
+
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
132
|
+
if (!provider && !modelSpec) {
|
|
133
|
+
throw new Error('No configuration file found. Create apibot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
|
|
134
|
+
}
|
|
135
|
+
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
136
|
+
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
140
|
+
if (!baseEndpoint) {
|
|
141
|
+
throw new Error('No API endpoint to test. Set EXPLORBOT_URL to the API base endpoint');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const outputRoot = resolveOutputRoot();
|
|
145
|
+
materializeKnowledge(outputRoot);
|
|
146
|
+
|
|
147
|
+
const api: ApiConfig = { baseEndpoint };
|
|
148
|
+
if (process.env.EXPLORBOT_API_SPEC) {
|
|
149
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let model: any;
|
|
153
|
+
if (provider && modelSpec) model = await createModel(provider, modelSpec);
|
|
154
|
+
if (provider && !modelSpec) model = await resolveModel(provider, 'model');
|
|
155
|
+
if (!provider) model = await resolveModel(modelSpec!, 'model');
|
|
156
|
+
|
|
157
|
+
this.config = {
|
|
158
|
+
ai: { model },
|
|
159
|
+
api,
|
|
160
|
+
dirs: { output: '.', knowledge: 'knowledge' },
|
|
161
|
+
};
|
|
162
|
+
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
163
|
+
this.validateConfig(this.config);
|
|
164
|
+
|
|
165
|
+
return this.config;
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
private findConfigFile(): string | null {
|
|
127
169
|
const apibotPaths = ['apibot.config.js', 'apibot.config.mjs', 'apibot.config.ts'];
|
|
128
170
|
for (const p of apibotPaths) {
|
|
@@ -2,6 +2,7 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
4
|
import type Explorer from '../../../../src/explorer.ts';
|
|
5
|
+
import type { StateManager } from '../../../../src/state-manager.ts';
|
|
5
6
|
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
6
7
|
import { tag } from '../../../../src/utils/logger.ts';
|
|
7
8
|
import type { DocbotConfig } from '../config.ts';
|
|
@@ -11,11 +12,13 @@ class Documentarian {
|
|
|
11
12
|
private provider: AIProvider;
|
|
12
13
|
private config: DocbotConfig;
|
|
13
14
|
private explorer?: Explorer;
|
|
15
|
+
private stateManager?: StateManager;
|
|
14
16
|
|
|
15
|
-
constructor(provider: AIProvider, config: DocbotConfig = {}, explorer?: Explorer) {
|
|
17
|
+
constructor(provider: AIProvider, config: DocbotConfig = {}, explorer?: Explorer, stateManager?: StateManager) {
|
|
16
18
|
this.provider = provider;
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.explorer = explorer;
|
|
21
|
+
this.stateManager = stateManager;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async document(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
@@ -46,7 +49,7 @@ class Documentarian {
|
|
|
46
49
|
try {
|
|
47
50
|
tag('info').log('Starting interactive exploration...');
|
|
48
51
|
|
|
49
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config, captureState);
|
|
52
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer!, this.stateManager!, state, research, this.config, captureState);
|
|
50
53
|
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
51
54
|
if (meaningfulInteractions.length > 0) {
|
|
52
55
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ResearchElement, parseResearchSections } from '../../../../src/ai/researcher/parser.ts';
|
|
2
2
|
import type Explorer from '../../../../src/explorer.ts';
|
|
3
|
-
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
3
|
+
import type { StateManager, WebPageState } from '../../../../src/state-manager.ts';
|
|
4
4
|
import { detectFocusArea } from '../../../../src/utils/aria.ts';
|
|
5
5
|
import type { DocbotConfig } from '../config.ts';
|
|
6
6
|
|
|
@@ -58,14 +58,14 @@ const DEFAULT_WAIT_MS = 700;
|
|
|
58
58
|
const TAB_WAIT_MS = 500;
|
|
59
59
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
60
60
|
|
|
61
|
-
export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
61
|
+
export async function collectDocInteractions(explorer: Explorer, stateManager: StateManager, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
62
62
|
const sections = parseResearchSections(research);
|
|
63
63
|
const transitions: DocStateTransition[] = [];
|
|
64
64
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
65
65
|
const tabGroup = findTabGroup(sections);
|
|
66
66
|
|
|
67
67
|
if (tabGroup) {
|
|
68
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
|
|
68
|
+
transitions.push(...(await exploreTabGroup(explorer, stateManager, tabGroup, state.url, maxInteractions, captureState)));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
@@ -73,7 +73,7 @@ export async function collectDocInteractions(explorer: Explorer, state: WebPageS
|
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
76
|
+
const transition = await executeInteraction(explorer, stateManager, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
77
77
|
if (!transition) {
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
@@ -92,7 +92,7 @@ export function pickDocActionCandidates(research: string, config: DocbotConfig =
|
|
|
92
92
|
}));
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
95
|
+
async function exploreTabGroup(explorer: Explorer, stateManager: StateManager, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
96
96
|
const transitions: DocStateTransition[] = [];
|
|
97
97
|
|
|
98
98
|
for (const element of tabGroup.elements) {
|
|
@@ -102,6 +102,7 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
102
102
|
|
|
103
103
|
const transition = await executeInteraction(
|
|
104
104
|
explorer,
|
|
105
|
+
stateManager,
|
|
105
106
|
{
|
|
106
107
|
element,
|
|
107
108
|
container: tabGroup.container,
|
|
@@ -123,8 +124,8 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
123
124
|
return transitions;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
127
|
-
const beforeState =
|
|
127
|
+
async function executeInteraction(explorer: Explorer, stateManager: StateManager, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
128
|
+
const beforeState = stateManager.getCurrentState();
|
|
128
129
|
if (!beforeState) {
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
@@ -136,7 +137,7 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
136
137
|
|
|
137
138
|
await wait(waitMs);
|
|
138
139
|
|
|
139
|
-
const afterState =
|
|
140
|
+
const afterState = stateManager.getCurrentState();
|
|
140
141
|
if (!afterState) {
|
|
141
142
|
return null;
|
|
142
143
|
}
|
|
@@ -164,7 +165,7 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
async function attemptInteraction(explorer: Explorer, candidate: InteractionCandidate): Promise<boolean> {
|
|
167
|
-
const action = explorer.
|
|
168
|
+
const action = explorer.action();
|
|
168
169
|
|
|
169
170
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
170
171
|
const success = await action.attempt(command, buildPurpose(candidate));
|
|
@@ -178,7 +179,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
178
179
|
|
|
179
180
|
async function restoreInteractionState(explorer: Explorer, restoreUrl: string, primaryCommand?: string | null): Promise<void> {
|
|
180
181
|
if (primaryCommand) {
|
|
181
|
-
const action = explorer.
|
|
182
|
+
const action = explorer.action();
|
|
182
183
|
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
183
184
|
if (restored) {
|
|
184
185
|
await wait(TAB_WAIT_MS);
|
|
@@ -186,7 +187,7 @@ async function restoreInteractionState(explorer: Explorer, restoreUrl: string, p
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
const action = explorer.
|
|
190
|
+
const action = explorer.action();
|
|
190
191
|
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -5,11 +5,6 @@ import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
|
|
|
5
5
|
import { DocBot, type DocbotOptions } from './docbot.ts';
|
|
6
6
|
|
|
7
7
|
function buildOptions(options: any): DocbotOptions {
|
|
8
|
-
let session = options.session;
|
|
9
|
-
if (options.session === true) {
|
|
10
|
-
session = 'output/session.json';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
return {
|
|
14
9
|
verbose: options.verbose || options.debug,
|
|
15
10
|
config: options.config,
|
|
@@ -17,7 +12,7 @@ function buildOptions(options: any): DocbotOptions {
|
|
|
17
12
|
show: options.show,
|
|
18
13
|
headless: options.headless,
|
|
19
14
|
incognito: options.incognito,
|
|
20
|
-
session,
|
|
15
|
+
session: options.session,
|
|
21
16
|
docsConfig: options.docsConfig,
|
|
22
17
|
};
|
|
23
18
|
}
|
|
@@ -8,10 +8,10 @@ import { sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
|
8
8
|
import { Documentarian, type PageDocumentation } from './ai/documentarian.ts';
|
|
9
9
|
import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
10
10
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
11
|
-
import { renderMermaidBody } from './state-diagram.ts';
|
|
12
11
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
13
12
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
14
13
|
import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { renderMermaidBody } from './state-diagram.ts';
|
|
15
15
|
|
|
16
16
|
class DocBot {
|
|
17
17
|
private explorBot: ExplorBot;
|
|
@@ -43,7 +43,7 @@ class DocBot {
|
|
|
43
43
|
config: this.options.docsConfig,
|
|
44
44
|
path: this.options.path,
|
|
45
45
|
});
|
|
46
|
-
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer());
|
|
46
|
+
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer(), this.explorBot.stateManager());
|
|
47
47
|
this.ensureDirectory(this.configParser.getOutputDir());
|
|
48
48
|
this.ensureDirectory(this.getPagesDir());
|
|
49
49
|
}
|
|
@@ -76,7 +76,7 @@ class DocBot {
|
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const stateManager = this.explorBot.
|
|
79
|
+
const stateManager = this.explorBot.stateManager();
|
|
80
80
|
if (stateManager.hasVisitedState(target)) {
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
@@ -11,7 +11,7 @@ import type { DocbotConfig } from './config.ts';
|
|
|
11
11
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
12
12
|
|
|
13
13
|
export async function captureDocumentationScreenshots(explorer: Explorer, state: WebPageState, research: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot[]> {
|
|
14
|
-
const page = explorer.
|
|
14
|
+
const page = explorer.page;
|
|
15
15
|
if (!page) {
|
|
16
16
|
return [];
|
|
17
17
|
}
|
|
@@ -62,7 +62,7 @@ export function getScreenshotSections(research: string): ScreenshotSection[] {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export async function captureInteractionScreenshot(explorer: Explorer, state: WebPageState, transition: DocStateTransition, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
|
|
65
|
-
const page = explorer.
|
|
65
|
+
const page = explorer.page;
|
|
66
66
|
if (!page) {
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
@@ -9,7 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
-
import { ConfigParser } from '../src/config.js';
|
|
12
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot } from '../src/explorbot.js';
|
|
14
14
|
import { Stats } from '../src/stats.js';
|
|
15
15
|
import { Plan } from '../src/test-plan.js';
|
|
@@ -27,7 +27,6 @@ if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
|
27
27
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
28
28
|
}
|
|
29
29
|
function buildExplorBotOptions(from, options) {
|
|
30
|
-
const sessionFile = options.session === true ? path.join(path.resolve(options.path || process.cwd()), 'output', 'session.json') : options.session;
|
|
31
30
|
return {
|
|
32
31
|
from,
|
|
33
32
|
verbose: options.verbose || options.debug,
|
|
@@ -36,7 +35,7 @@ function buildExplorBotOptions(from, options) {
|
|
|
36
35
|
show: options.show,
|
|
37
36
|
headless: options.headless,
|
|
38
37
|
incognito: options.incognito,
|
|
39
|
-
session:
|
|
38
|
+
session: options.session,
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
41
|
function addCommonOptions(cmd) {
|
|
@@ -623,7 +622,7 @@ program
|
|
|
623
622
|
path: options.path,
|
|
624
623
|
config: options.config,
|
|
625
624
|
headless: true,
|
|
626
|
-
session: options.session
|
|
625
|
+
session: options.session,
|
|
627
626
|
};
|
|
628
627
|
const explorBot = new ExplorBot(mainOptions);
|
|
629
628
|
await explorBot.start();
|
|
@@ -652,10 +651,10 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
652
651
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
653
652
|
await explorBot.start();
|
|
654
653
|
await explorBot.agentNavigator().visit(url);
|
|
655
|
-
const action = explorBot.getExplorer().
|
|
654
|
+
const action = explorBot.getExplorer().action();
|
|
656
655
|
await action.execute(command);
|
|
657
656
|
log('Command executed successfully');
|
|
658
|
-
const state = explorBot.
|
|
657
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
659
658
|
if (state)
|
|
660
659
|
log(`URL: ${state.url}`);
|
|
661
660
|
await explorBot.stop();
|
|
@@ -793,4 +792,25 @@ import { createApiCommands } from "../boat/api-tester/src/cli.js";
|
|
|
793
792
|
import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
|
|
794
793
|
program.addCommand(createApiCommands('api'));
|
|
795
794
|
program.addCommand(createDocsCommands('docs'));
|
|
795
|
+
const envHelp = () => {
|
|
796
|
+
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
797
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
798
|
+
return `
|
|
799
|
+
Environment variables (config-free one-liner mode):
|
|
800
|
+
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
801
|
+
|
|
802
|
+
${rows}
|
|
803
|
+
|
|
804
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
805
|
+
Example:
|
|
806
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
807
|
+
${cli} explore /login --max-tests 3
|
|
808
|
+
`;
|
|
809
|
+
};
|
|
810
|
+
const addEnvHelp = (cmd) => {
|
|
811
|
+
cmd.addHelpText('after', envHelp);
|
|
812
|
+
for (const sub of cmd.commands)
|
|
813
|
+
addEnvHelp(sub);
|
|
814
|
+
};
|
|
815
|
+
addEnvHelp(program);
|
|
796
816
|
program.parse();
|
|
@@ -9,7 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
11
|
import { parseEnv } from 'node:util';
|
|
12
|
-
import { EXPLORBOT_CONFIG_PATHS } from "../../../src/config.js";
|
|
12
|
+
import { EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from "../../../src/config.js";
|
|
13
13
|
export class ApibotConfigParser {
|
|
14
14
|
static instance;
|
|
15
15
|
config = null;
|
|
@@ -37,9 +37,13 @@ export class ApibotConfigParser {
|
|
|
37
37
|
ApibotConfigParser.loadEnv('.env');
|
|
38
38
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
39
39
|
if (!resolvedPath) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
try {
|
|
41
|
+
return await this.loadEnvConfig();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (options?.path && originalCwd !== process.cwd())
|
|
45
|
+
process.chdir(originalCwd);
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
try {
|
|
45
49
|
const configModule = await this.loadConfigModule(resolvedPath);
|
|
@@ -98,6 +102,41 @@ export class ApibotConfigParser {
|
|
|
98
102
|
mkdirSync(dirPath, { recursive: true });
|
|
99
103
|
}
|
|
100
104
|
}
|
|
105
|
+
async loadEnvConfig() {
|
|
106
|
+
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
107
|
+
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
108
|
+
if (!provider && !modelSpec) {
|
|
109
|
+
throw new Error('No configuration file found. Create apibot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
|
|
110
|
+
}
|
|
111
|
+
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
112
|
+
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
113
|
+
}
|
|
114
|
+
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
115
|
+
if (!baseEndpoint) {
|
|
116
|
+
throw new Error('No API endpoint to test. Set EXPLORBOT_URL to the API base endpoint');
|
|
117
|
+
}
|
|
118
|
+
const outputRoot = resolveOutputRoot();
|
|
119
|
+
materializeKnowledge(outputRoot);
|
|
120
|
+
const api = { baseEndpoint };
|
|
121
|
+
if (process.env.EXPLORBOT_API_SPEC) {
|
|
122
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
123
|
+
}
|
|
124
|
+
let model;
|
|
125
|
+
if (provider && modelSpec)
|
|
126
|
+
model = await createModel(provider, modelSpec);
|
|
127
|
+
if (provider && !modelSpec)
|
|
128
|
+
model = await resolveModel(provider, 'model');
|
|
129
|
+
if (!provider)
|
|
130
|
+
model = await resolveModel(modelSpec, 'model');
|
|
131
|
+
this.config = {
|
|
132
|
+
ai: { model },
|
|
133
|
+
api,
|
|
134
|
+
dirs: { output: '.', knowledge: 'knowledge' },
|
|
135
|
+
};
|
|
136
|
+
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
137
|
+
this.validateConfig(this.config);
|
|
138
|
+
return this.config;
|
|
139
|
+
}
|
|
101
140
|
findConfigFile() {
|
|
102
141
|
const apibotPaths = ['apibot.config.js', 'apibot.config.mjs', 'apibot.config.ts'];
|
|
103
142
|
for (const p of apibotPaths) {
|
|
@@ -6,10 +6,12 @@ class Documentarian {
|
|
|
6
6
|
provider;
|
|
7
7
|
config;
|
|
8
8
|
explorer;
|
|
9
|
-
|
|
9
|
+
stateManager;
|
|
10
|
+
constructor(provider, config = {}, explorer, stateManager) {
|
|
10
11
|
this.provider = provider;
|
|
11
12
|
this.config = config;
|
|
12
13
|
this.explorer = explorer;
|
|
14
|
+
this.stateManager = stateManager;
|
|
13
15
|
}
|
|
14
16
|
async document(state, research, captureState) {
|
|
15
17
|
const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
|
|
@@ -35,7 +37,7 @@ class Documentarian {
|
|
|
35
37
|
let meaningfulInteractions = [];
|
|
36
38
|
try {
|
|
37
39
|
tag('info').log('Starting interactive exploration...');
|
|
38
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer, state, research, this.config, captureState);
|
|
40
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer, this.stateManager, state, research, this.config, captureState);
|
|
39
41
|
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
40
42
|
if (meaningfulInteractions.length > 0) {
|
|
41
43
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
@@ -6,19 +6,19 @@ const MAX_LINKS = 15;
|
|
|
6
6
|
const DEFAULT_WAIT_MS = 700;
|
|
7
7
|
const TAB_WAIT_MS = 500;
|
|
8
8
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
9
|
-
export async function collectDocInteractions(explorer, state, research, config = {}, captureState) {
|
|
9
|
+
export async function collectDocInteractions(explorer, stateManager, state, research, config = {}, captureState) {
|
|
10
10
|
const sections = parseResearchSections(research);
|
|
11
11
|
const transitions = [];
|
|
12
12
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
13
13
|
const tabGroup = findTabGroup(sections);
|
|
14
14
|
if (tabGroup) {
|
|
15
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
|
|
15
|
+
transitions.push(...(await exploreTabGroup(explorer, stateManager, tabGroup, state.url, maxInteractions, captureState)));
|
|
16
16
|
}
|
|
17
17
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
18
18
|
if (transitions.length >= maxInteractions) {
|
|
19
19
|
break;
|
|
20
20
|
}
|
|
21
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
21
|
+
const transition = await executeInteraction(explorer, stateManager, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
22
22
|
if (!transition) {
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
@@ -33,13 +33,13 @@ export function pickDocActionCandidates(research, config = {}) {
|
|
|
33
33
|
section: candidate.sectionName,
|
|
34
34
|
}));
|
|
35
35
|
}
|
|
36
|
-
async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions, captureState) {
|
|
36
|
+
async function exploreTabGroup(explorer, stateManager, tabGroup, restoreUrl, maxInteractions, captureState) {
|
|
37
37
|
const transitions = [];
|
|
38
38
|
for (const element of tabGroup.elements) {
|
|
39
39
|
if (transitions.length >= maxInteractions) {
|
|
40
40
|
break;
|
|
41
41
|
}
|
|
42
|
-
const transition = await executeInteraction(explorer, {
|
|
42
|
+
const transition = await executeInteraction(explorer, stateManager, {
|
|
43
43
|
element,
|
|
44
44
|
container: tabGroup.container,
|
|
45
45
|
role: 'tab',
|
|
@@ -53,8 +53,8 @@ async function exploreTabGroup(explorer, tabGroup, restoreUrl, maxInteractions,
|
|
|
53
53
|
await restoreInteractionState(explorer, restoreUrl, buildPrimaryCommand(tabGroup.elements[0], tabGroup.container));
|
|
54
54
|
return transitions;
|
|
55
55
|
}
|
|
56
|
-
async function executeInteraction(explorer, candidate, restoreUrl, waitMs, captureState) {
|
|
57
|
-
const beforeState =
|
|
56
|
+
async function executeInteraction(explorer, stateManager, candidate, restoreUrl, waitMs, captureState) {
|
|
57
|
+
const beforeState = stateManager.getCurrentState();
|
|
58
58
|
if (!beforeState) {
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
@@ -63,7 +63,7 @@ async function executeInteraction(explorer, candidate, restoreUrl, waitMs, captu
|
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
65
|
await wait(waitMs);
|
|
66
|
-
const afterState =
|
|
66
|
+
const afterState = stateManager.getCurrentState();
|
|
67
67
|
if (!afterState) {
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
@@ -86,7 +86,7 @@ async function executeInteraction(explorer, candidate, restoreUrl, waitMs, captu
|
|
|
86
86
|
return transition;
|
|
87
87
|
}
|
|
88
88
|
async function attemptInteraction(explorer, candidate) {
|
|
89
|
-
const action = explorer.
|
|
89
|
+
const action = explorer.action();
|
|
90
90
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
91
91
|
const success = await action.attempt(command, buildPurpose(candidate));
|
|
92
92
|
if (success) {
|
|
@@ -97,14 +97,14 @@ async function attemptInteraction(explorer, candidate) {
|
|
|
97
97
|
}
|
|
98
98
|
async function restoreInteractionState(explorer, restoreUrl, primaryCommand) {
|
|
99
99
|
if (primaryCommand) {
|
|
100
|
-
const action = explorer.
|
|
100
|
+
const action = explorer.action();
|
|
101
101
|
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
102
102
|
if (restored) {
|
|
103
103
|
await wait(TAB_WAIT_MS);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
const action = explorer.
|
|
107
|
+
const action = explorer.action();
|
|
108
108
|
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
109
109
|
}
|
|
110
110
|
function buildTransition(candidate, beforeState, afterState, changes) {
|