explorbot 0.4.3 → 0.4.5
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/boat/api-tester/src/apibot.ts +8 -13
- package/boat/api-tester/src/cli.ts +7 -3
- package/boat/api-tester/src/config.ts +45 -9
- package/boat/prima/src/cli.ts +33 -99
- package/boat/prima/src/envelope.ts +3 -1
- package/boat/prima/src/help.ts +72 -0
- package/boat/prima/src/prima.ts +41 -46
- package/dist/boat/api-tester/src/apibot.js +7 -6
- package/dist/boat/api-tester/src/cli.js +9 -3
- package/dist/boat/api-tester/src/config.js +32 -6
- package/dist/boat/prima/src/cli.js +30 -86
- package/dist/boat/prima/src/envelope.js +2 -1
- package/dist/boat/prima/src/help.js +63 -0
- package/dist/boat/prima/src/prima.js +39 -44
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +5 -0
- package/dist/src/action.js +12 -1
- package/dist/src/ai/fisherman/tools.js +7 -1
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/pilot.d.ts +0 -1
- package/dist/src/ai/pilot.js +8 -24
- package/dist/src/ai/planner.d.ts +4 -0
- package/dist/src/ai/planner.js +28 -0
- package/dist/src/ai/provider.js +3 -1
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +4 -1
- package/dist/src/ai/researcher/sections.d.ts +1 -1
- package/dist/src/ai/researcher/sections.js +2 -1
- package/dist/src/ai/researcher.js +25 -11
- package/dist/src/ai/rules.js +8 -7
- package/dist/src/ai/scout/tools.d.ts +17 -0
- package/dist/src/ai/scout/tools.js +130 -0
- package/dist/src/ai/scout.d.ts +21 -0
- package/dist/src/ai/scout.js +150 -0
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +27 -33
- package/dist/src/ai/tools.js +61 -30
- package/dist/src/application-spec.d.ts +3 -0
- package/dist/src/application-spec.js +21 -5
- package/dist/src/commands/config-command.js +6 -2
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +14 -0
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +33 -0
- package/dist/src/knowledge-tracker.d.ts +1 -0
- package/dist/src/knowledge-tracker.js +3 -0
- package/dist/src/state-manager.js +5 -1
- package/dist/src/utils/aria-ref.d.ts +16 -0
- package/dist/src/utils/aria-ref.js +47 -0
- package/dist/src/utils/aria.js +3 -3
- package/dist/src/utils/web-annotate.js +3 -15
- package/dist/src/utils/web-element.d.ts +0 -2
- package/dist/src/utils/web-element.js +0 -8
- package/docs/api-testing/basics.md +12 -4
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +28 -1
- package/docs/web-testing/agents.md +9 -1
- package/docs/web-testing/planner.md +5 -0
- package/docs/workflow/agentic-usage.md +3 -1
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +1 -1
- package/src/action-result.ts +7 -0
- package/src/action.ts +14 -2
- package/src/ai/fisherman/tools.ts +8 -1
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/pilot.ts +8 -25
- package/src/ai/planner.ts +33 -0
- package/src/ai/provider.ts +2 -1
- package/src/ai/researcher/deep-analysis.ts +4 -2
- package/src/ai/researcher/sections.ts +2 -2
- package/src/ai/researcher.ts +28 -11
- package/src/ai/rules.ts +8 -7
- package/src/ai/scout/tools.ts +150 -0
- package/src/ai/scout.ts +173 -0
- package/src/ai/tester.ts +25 -30
- package/src/ai/tools.ts +75 -36
- package/src/application-spec.ts +22 -4
- package/src/commands/config-command.ts +4 -1
- package/src/config.ts +23 -0
- package/src/explorbot.ts +36 -0
- package/src/knowledge-tracker.ts +4 -0
- package/src/state-manager.ts +6 -1
- package/src/utils/aria-ref.ts +61 -0
- package/src/utils/aria.ts +3 -3
- package/src/utils/web-annotate.ts +3 -15
- package/src/utils/web-element.ts +0 -9
package/src/application-spec.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class ApplicationSpec {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
renderFor(state: ActionResult): string {
|
|
20
|
-
const relevant = this.
|
|
20
|
+
const relevant = this.relevantPages(state);
|
|
21
21
|
if (relevant.length === 0) return '';
|
|
22
22
|
|
|
23
23
|
tag('operation').log(`Found application specification for ${state.url}`);
|
|
@@ -30,6 +30,10 @@ export class ApplicationSpec {
|
|
|
30
30
|
`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
matchedUrls(state: ActionResult): string[] {
|
|
34
|
+
return this.relevantPages(state).map((page) => page.url);
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
get pageCount(): number {
|
|
34
38
|
return this.pages.length;
|
|
35
39
|
}
|
|
@@ -75,12 +79,26 @@ export class ApplicationSpec {
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
private resolveSourcePath(sourcePath: string): string {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
return resolveSpecSource(sourcePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private relevantPages(state: ActionResult): ApplicationSpecPage[] {
|
|
86
|
+
return this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
export function resolveSpecBundlePath(sourcePath: string): string | null {
|
|
91
|
+
const resolved = resolveSpecSource(sourcePath);
|
|
92
|
+
if (!existsSync(resolved)) return null;
|
|
93
|
+
if (statSync(resolved).isDirectory()) return resolved;
|
|
94
|
+
return path.dirname(resolved);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveSpecSource(sourcePath: string): string {
|
|
98
|
+
if (path.isAbsolute(sourcePath)) return path.resolve(sourcePath);
|
|
99
|
+
return path.resolve(ConfigParser.getInstance().resolveProjectDir(sourcePath));
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
interface ApplicationSpecPage {
|
|
85
103
|
url: string;
|
|
86
104
|
content: string;
|
|
@@ -45,7 +45,10 @@ export class ConfigCommand extends BaseCommand {
|
|
|
45
45
|
const env: Record<string, string> = {};
|
|
46
46
|
for (const variable of EXPLORBOT_ENV_VARS) {
|
|
47
47
|
const value = process.env[variable.name];
|
|
48
|
-
if (value)
|
|
48
|
+
if (!value) continue;
|
|
49
|
+
let shown = value;
|
|
50
|
+
if (variable.secret) shown = 'set';
|
|
51
|
+
env[variable.name] = shown;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
const models: Record<string, string> = {};
|
package/src/config.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const PROVIDERS: Record<string, ProviderInfo> = {
|
|
|
22
22
|
export const MODEL_ROLES: ModelRole[] = ['model', 'visionModel', 'agenticModel'];
|
|
23
23
|
|
|
24
24
|
let cachedOutputRoot: string | null = null;
|
|
25
|
+
let runOutputDir: string | null = null;
|
|
25
26
|
|
|
26
27
|
interface PlaywrightConfig {
|
|
27
28
|
browser: 'chromium' | 'firefox' | 'webkit';
|
|
@@ -129,6 +130,11 @@ interface RerunnerAgentConfig extends AgentConfig {
|
|
|
129
130
|
interface PlannerAgentConfig extends AgentConfig {
|
|
130
131
|
styles?: string[];
|
|
131
132
|
stylesDir?: string;
|
|
133
|
+
docsWeight?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface ScoutAgentConfig extends AgentConfig {
|
|
137
|
+
dirs?: string[];
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
interface ScreencastConfig {
|
|
@@ -153,6 +159,7 @@ interface AgentsConfig {
|
|
|
153
159
|
quartermaster?: AgentConfig;
|
|
154
160
|
historian?: HistorianAgentConfig;
|
|
155
161
|
fisherman?: AgentConfig;
|
|
162
|
+
scout?: ScoutAgentConfig;
|
|
156
163
|
chief?: AgentConfig;
|
|
157
164
|
curler?: AgentConfig;
|
|
158
165
|
rerunner?: RerunnerAgentConfig;
|
|
@@ -275,6 +282,7 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
|
|
|
275
282
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
276
283
|
{ name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
|
|
277
284
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
285
|
+
{ name: 'EXPLORBOT_API_HEADERS', description: 'Headers sent with every API request, one "Name: value" per line', secret: true },
|
|
278
286
|
{ name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
|
|
279
287
|
{ name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
|
|
280
288
|
];
|
|
@@ -291,6 +299,7 @@ export type {
|
|
|
291
299
|
ResearcherAgentConfig,
|
|
292
300
|
NavigatorAgentConfig,
|
|
293
301
|
PlannerAgentConfig,
|
|
302
|
+
ScoutAgentConfig,
|
|
294
303
|
RerunnerAgentConfig,
|
|
295
304
|
HealRecipe,
|
|
296
305
|
Hook,
|
|
@@ -496,6 +505,7 @@ export class ConfigParser {
|
|
|
496
505
|
// For testing purposes only
|
|
497
506
|
public static resetForTesting(): void {
|
|
498
507
|
cachedOutputRoot = null;
|
|
508
|
+
runOutputDir = null;
|
|
499
509
|
if (ConfigParser.instance) {
|
|
500
510
|
ConfigParser.instance.config = null;
|
|
501
511
|
ConfigParser.instance.configPath = null;
|
|
@@ -744,10 +754,22 @@ export class ConfigParser {
|
|
|
744
754
|
}
|
|
745
755
|
}
|
|
746
756
|
|
|
757
|
+
export function setOutputDir(dir: string): void {
|
|
758
|
+
runOutputDir = dir;
|
|
759
|
+
}
|
|
760
|
+
|
|
747
761
|
export function outputPath(...segments: string[]): string {
|
|
762
|
+
if (runOutputDir) return path.join(runOutputDir, ...segments);
|
|
748
763
|
return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
|
|
749
764
|
}
|
|
750
765
|
|
|
766
|
+
export function agentSettings<K extends keyof AgentsConfig>(config: ExplorbotConfig, agent: K): NonNullable<AgentsConfig[K]> {
|
|
767
|
+
const ai = (config.ai ??= { model: null });
|
|
768
|
+
const agents = (ai.agents ??= {}) as Record<K, NonNullable<AgentsConfig[K]>>;
|
|
769
|
+
agents[agent] ??= {} as NonNullable<AgentsConfig[K]>;
|
|
770
|
+
return agents[agent];
|
|
771
|
+
}
|
|
772
|
+
|
|
751
773
|
export async function resolveModel(spec: string, role: ModelRole = 'model'): Promise<any> {
|
|
752
774
|
const separator = spec.indexOf('/');
|
|
753
775
|
if (separator > 0) {
|
|
@@ -912,6 +934,7 @@ interface EnvVar {
|
|
|
912
934
|
name: string;
|
|
913
935
|
description: string;
|
|
914
936
|
required?: boolean;
|
|
937
|
+
secret?: boolean;
|
|
915
938
|
}
|
|
916
939
|
|
|
917
940
|
export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
|
package/src/explorbot.ts
CHANGED
|
@@ -14,12 +14,15 @@ import { AIProvider } from './ai/provider.ts';
|
|
|
14
14
|
import { Quartermaster } from './ai/quartermaster.ts';
|
|
15
15
|
import { Rerunner } from './ai/rerunner.ts';
|
|
16
16
|
import { Researcher } from './ai/researcher.ts';
|
|
17
|
+
import { Scout } from './ai/scout.ts';
|
|
18
|
+
import { loadScoutCorpus } from './ai/scout/tools.ts';
|
|
17
19
|
import { SessionAnalyst } from './ai/session-analyst.ts';
|
|
18
20
|
import { Tester } from './ai/tester.ts';
|
|
19
21
|
import { createAgentTools } from './ai/tools.ts';
|
|
20
22
|
import { ApiClient } from './api/api-client.ts';
|
|
21
23
|
import { RequestStore } from './api/request-store.ts';
|
|
22
24
|
import { loadSpec } from './api/spec-reader.ts';
|
|
25
|
+
import { resolveSpecBundlePath } from './application-spec.ts';
|
|
23
26
|
import type { ExplorbotConfig, ReporterConfig } from './config.js';
|
|
24
27
|
import { ConfigParser } from './config.ts';
|
|
25
28
|
import { ExperienceTracker } from './experience-tracker.ts';
|
|
@@ -231,6 +234,8 @@ export class ExplorBot {
|
|
|
231
234
|
this.agents.planner = this.createAgent((deps) => new Planner(deps, this.agentResearcher()));
|
|
232
235
|
const fisherman = this.agentFisherman();
|
|
233
236
|
if (fisherman) this.agents.planner.setFisherman(fisherman);
|
|
237
|
+
const scout = this.agentScout();
|
|
238
|
+
if (scout) this.agents.planner.setScout(scout);
|
|
234
239
|
}
|
|
235
240
|
return this.agents.planner;
|
|
236
241
|
}
|
|
@@ -367,6 +372,37 @@ export class ExplorBot {
|
|
|
367
372
|
return this.agents.fisherman;
|
|
368
373
|
}
|
|
369
374
|
|
|
375
|
+
agentScout(): Scout | null {
|
|
376
|
+
const scoutConfig = this.config.ai?.agents?.scout;
|
|
377
|
+
if (scoutConfig?.enabled !== true) return null;
|
|
378
|
+
|
|
379
|
+
const dirs = this.scoutCorpusDirs();
|
|
380
|
+
if (dirs.length === 0) {
|
|
381
|
+
tag('warning').log('Scout enabled but no documentation found — set --spec or ai.agents.scout.dirs');
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return (this.agents.scout ||= this.createAgent(({ ai }) => new Scout(ai, loadScoutCorpus(dirs))));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private scoutCorpusDirs(): string[] {
|
|
389
|
+
const dirs: string[] = [];
|
|
390
|
+
|
|
391
|
+
const specPath = this.options.applicationSpec || this.config.dirs?.spec;
|
|
392
|
+
if (specPath) {
|
|
393
|
+
const bundle = resolveSpecBundlePath(specPath);
|
|
394
|
+
const pagesDir = bundle && path.join(bundle, 'pages');
|
|
395
|
+
if (pagesDir && existsSync(pagesDir)) dirs.push(pagesDir);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const dir of this.config.ai?.agents?.scout?.dirs || []) {
|
|
399
|
+
const resolved = this.configParser.resolveProjectDir(dir);
|
|
400
|
+
if (existsSync(resolved)) dirs.push(resolved);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return [...new Set(dirs)];
|
|
404
|
+
}
|
|
405
|
+
|
|
370
406
|
getCurrentPlan(): Plan | undefined {
|
|
371
407
|
return this.currentPlan;
|
|
372
408
|
}
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -102,6 +102,10 @@ export class KnowledgeTracker {
|
|
|
102
102
|
return this.applicationSpec?.renderFor(state) || '';
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
applicationSpecUrls(state: ActionResult): string[] {
|
|
106
|
+
return this.applicationSpec?.matchedUrls(state) || [];
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
addKnowledge(urlPattern: string, description: string, opts?: { replace?: boolean }): { filename: string; filePath: string; isNewFile: boolean } {
|
|
106
110
|
const configParser = ConfigParser.getInstance();
|
|
107
111
|
const configPath = configParser.getConfigPath();
|
package/src/state-manager.ts
CHANGED
|
@@ -143,13 +143,18 @@ export class StateManager {
|
|
|
143
143
|
updateState(actionResult: ActionResult, codeBlock?: string, trigger: 'manual' | 'navigation' | 'automatic' = 'manual'): WebPageState {
|
|
144
144
|
const previousState = this.currentState;
|
|
145
145
|
const previousHash = previousState?.hash;
|
|
146
|
+
const hashChanged = actionResult.hash !== previousHash;
|
|
147
|
+
|
|
148
|
+
if (!hashChanged && previousState?.verifications) {
|
|
149
|
+
const stillTrue = Object.entries(previousState.verifications).filter(([, passed]) => passed);
|
|
150
|
+
actionResult.verifications = { ...Object.fromEntries(stillTrue), ...actionResult.verifications };
|
|
151
|
+
}
|
|
146
152
|
|
|
147
153
|
const newState = actionResult;
|
|
148
154
|
this.currentState = newState;
|
|
149
155
|
this.currentState.id = this.nextStateId++;
|
|
150
156
|
if (newState.url) this.allVisitedUrls.add(normalizeUrl(newState.url));
|
|
151
157
|
|
|
152
|
-
const hashChanged = actionResult.hash !== previousHash;
|
|
153
158
|
const regionOpened = !hashChanged && this.regionOpened(previousState, newState);
|
|
154
159
|
|
|
155
160
|
if (hashChanged || regionOpened) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { WebElement } from './web-element.ts';
|
|
2
|
+
|
|
3
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
4
|
+
const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
|
|
5
|
+
|
|
6
|
+
const REF_ROLES: Record<string, string> = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
7
|
+
|
|
8
|
+
export function ariaRefSelector(ref: string): string {
|
|
9
|
+
return `aria-ref=${ref}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isAriaRef(ref: string): boolean {
|
|
13
|
+
return ARIA_REF_PATTERN.test(ref);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ariaRefSnapshot(page: any): Promise<string> {
|
|
17
|
+
return page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseAriaRefs(ariaSnapshot: string): AriaRefEntry[] {
|
|
21
|
+
const entries: AriaRefEntry[] = [];
|
|
22
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
23
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
24
|
+
if (!match) continue;
|
|
25
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
26
|
+
}
|
|
27
|
+
return entries;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function elementFromAriaRef(page: any, ref: string): Promise<WebElement | null> {
|
|
31
|
+
if (!isAriaRef(ref)) return null;
|
|
32
|
+
return WebElement.fromPlaywrightLocator(page.locator(ariaRefSelector(ref)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function refIsGone(explorer: any, ref: string): Promise<boolean> {
|
|
36
|
+
const count = () => Promise.resolve(explorer?.withPage?.((page: any) => page.locator(ariaRefSelector(ref)).count())).catch(() => undefined);
|
|
37
|
+
if ((await count()) !== 0) return false;
|
|
38
|
+
await Promise.resolve(explorer?.withPage?.(ariaRefSnapshot)).catch(() => null);
|
|
39
|
+
return (await count()) === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function describeRef(explorer: any, ref: string): Promise<{ role: string; text: string } | null> {
|
|
43
|
+
return Promise.resolve(
|
|
44
|
+
explorer?.withPage?.((page: any) =>
|
|
45
|
+
page.locator(ariaRefSelector(ref)).evaluate((el: any, roles: Record<string, string>) => {
|
|
46
|
+
const tag = el.tagName.toLowerCase();
|
|
47
|
+
const role = el.getAttribute('role') || roles[tag];
|
|
48
|
+
if (!role) return null;
|
|
49
|
+
const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
|
|
50
|
+
if (!text) return null;
|
|
51
|
+
return { role, text };
|
|
52
|
+
}, REF_ROLES)
|
|
53
|
+
)
|
|
54
|
+
).catch(() => null);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AriaRefEntry {
|
|
58
|
+
role: string;
|
|
59
|
+
name: string;
|
|
60
|
+
ref: string;
|
|
61
|
+
}
|
package/src/utils/aria.ts
CHANGED
|
@@ -431,9 +431,9 @@ const formatDiffSection = (label: string, items: string[]): string[] => {
|
|
|
431
431
|
const summary = countBy(items);
|
|
432
432
|
if (summary.size === 0) return [` ${label}: []`];
|
|
433
433
|
|
|
434
|
-
const
|
|
435
|
-
const top =
|
|
436
|
-
const rest =
|
|
434
|
+
const ordered = Array.from(summary.entries());
|
|
435
|
+
const top = ordered.slice(0, TOP_DIFF_ITEMS);
|
|
436
|
+
const rest = ordered.slice(TOP_DIFF_ITEMS);
|
|
437
437
|
|
|
438
438
|
const lines = [` ${label}:`];
|
|
439
439
|
for (const [item, count] of top) {
|
|
@@ -1,27 +1,15 @@
|
|
|
1
|
+
import { ariaRefSnapshot, parseAriaRefs } from './aria-ref.ts';
|
|
1
2
|
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from './html.ts';
|
|
2
3
|
import { createDebug } from './logger.js';
|
|
3
4
|
import { WebElement } from './web-element.ts';
|
|
4
5
|
|
|
5
6
|
const debugLog = createDebug('explorbot:web-annotate');
|
|
6
7
|
|
|
7
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
8
|
-
|
|
9
8
|
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
10
9
|
|
|
11
|
-
function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string; ref: string }> {
|
|
12
|
-
const entries: Array<{ role: string; name: string; ref: string }> = [];
|
|
13
|
-
for (const line of ariaSnapshot.split('\n')) {
|
|
14
|
-
const match = line.match(REF_LINE_PATTERN);
|
|
15
|
-
if (!match) continue;
|
|
16
|
-
if (!ANNOTATABLE_ROLES.has(match[2])) continue;
|
|
17
|
-
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
18
|
-
}
|
|
19
|
-
return entries;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
23
|
-
const ariaSnapshot: string = await page
|
|
24
|
-
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
11
|
+
const ariaSnapshot: string = await ariaRefSnapshot(page);
|
|
12
|
+
const refEntries = parseAriaRefs(ariaSnapshot).filter((entry) => ANNOTATABLE_ROLES.has(entry.role));
|
|
25
13
|
|
|
26
14
|
const byRole = new Map<string, Array<{ name: string; ref: string }>>();
|
|
27
15
|
for (const { role, name, ref } of refEntries) {
|
package/src/utils/web-element.ts
CHANGED
|
@@ -125,15 +125,6 @@ export class WebElement {
|
|
|
125
125
|
return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
static isAriaRef(ref: string): boolean {
|
|
129
|
-
return /^(f\d+)?e\d+$/i.test(ref);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
static async fromAriaRef(page: any, ref: string): Promise<WebElement | null> {
|
|
133
|
-
if (!WebElement.isAriaRef(ref)) return null;
|
|
134
|
-
return WebElement.fromPlaywrightLocator(page.locator(`aria-ref=${ref}`));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
128
|
static async fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]> {
|
|
138
129
|
const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
|
|
139
130
|
if (validEidxList.length === 0) return [];
|