explorbot 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/explorbot-cli.ts +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/src/action-result.ts
CHANGED
|
@@ -30,6 +30,7 @@ interface ActionResultData extends WebPageState {
|
|
|
30
30
|
iframeSnapshots?: Array<{ src: string; html: string; id?: string }>;
|
|
31
31
|
ariaSnapshot?: string | null;
|
|
32
32
|
ariaSnapshotFile?: string;
|
|
33
|
+
focusedElement?: FocusedElement | null;
|
|
33
34
|
iframeURL?: string;
|
|
34
35
|
links?: Link[];
|
|
35
36
|
}
|
|
@@ -74,6 +75,7 @@ export class ActionResult implements ActionResultData {
|
|
|
74
75
|
private snapshotCache = new TTLCache<string>();
|
|
75
76
|
readonly logFile: string | undefined = undefined;
|
|
76
77
|
readonly ariaSnapshotFile: string | undefined = undefined;
|
|
78
|
+
readonly focusedElement: FocusedElement | null = null;
|
|
77
79
|
private _ariaSnapshot: string | null | undefined = undefined;
|
|
78
80
|
private _lastExtractedHtml: string | undefined = undefined;
|
|
79
81
|
notes: string[] = [];
|
|
@@ -107,6 +109,9 @@ export class ActionResult implements ActionResultData {
|
|
|
107
109
|
if (data.ariaSnapshotFile !== undefined) {
|
|
108
110
|
this.ariaSnapshotFile = data.ariaSnapshotFile;
|
|
109
111
|
}
|
|
112
|
+
if (data.focusedElement !== undefined) {
|
|
113
|
+
this.focusedElement = data.focusedElement;
|
|
114
|
+
}
|
|
110
115
|
|
|
111
116
|
// Store HTML in a private property if provided
|
|
112
117
|
if (data.html !== undefined) {
|
|
@@ -643,3 +648,9 @@ export class Diff {
|
|
|
643
648
|
this._ariaChangeCount = ariaDiff.count;
|
|
644
649
|
}
|
|
645
650
|
}
|
|
651
|
+
|
|
652
|
+
export interface FocusedElement {
|
|
653
|
+
role: string;
|
|
654
|
+
name: string;
|
|
655
|
+
value?: string;
|
|
656
|
+
}
|
package/src/action.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { context, trace } from '@opentelemetry/api';
|
|
4
4
|
import { container, recorder } from 'codeceptjs';
|
|
5
5
|
import * as codeceptjs from 'codeceptjs';
|
|
6
|
-
import { ActionResult } from './action-result.js';
|
|
6
|
+
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
7
7
|
import { clearActivity, setActivity } from './activity.ts';
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import type { ExplorbotConfig } from './config.js';
|
|
@@ -19,6 +19,8 @@ import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCo
|
|
|
19
19
|
|
|
20
20
|
const debugLog = createDebug('explorbot:action');
|
|
21
21
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
22
|
+
const DEFAULT_ACTION_TIMEOUT = 3000;
|
|
23
|
+
const DEFAULT_PAGE_TIMEOUT = 3000;
|
|
22
24
|
|
|
23
25
|
class Action {
|
|
24
26
|
private actor: CodeceptJS.I;
|
|
@@ -32,6 +34,7 @@ class Action {
|
|
|
32
34
|
public playwrightHelper: any;
|
|
33
35
|
public playwrightGroupId: string | null = null;
|
|
34
36
|
public assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
37
|
+
public lastValue: unknown;
|
|
35
38
|
private recorder?: PlaywrightRecorder;
|
|
36
39
|
private recovery: RecoveryRunner;
|
|
37
40
|
private mainDocumentStatus: number | undefined = undefined;
|
|
@@ -65,8 +68,8 @@ class Action {
|
|
|
65
68
|
return this.recovery(() => this.captureOnce(opts));
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
async execute(code: string): Promise<Action> {
|
|
69
|
-
return this.recovery(() => this.executeOnce(code));
|
|
71
|
+
async execute(code: string, opts: ExecuteOptions = {}): Promise<Action> {
|
|
72
|
+
return this.recovery(() => this.executeOnce(code, opts.verbatim));
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
private async captureOnce({ includeScreenshot = false, codeBlock }: { includeScreenshot?: boolean; codeBlock?: string } = {}): Promise<ActionResult> {
|
|
@@ -133,10 +136,12 @@ class Action {
|
|
|
133
136
|
|
|
134
137
|
let ariaSnapshot: string | null = null;
|
|
135
138
|
let ariaSnapshotFile: string | undefined = undefined;
|
|
139
|
+
let focusedElement: FocusedElement | null = null;
|
|
136
140
|
|
|
137
141
|
try {
|
|
138
142
|
const page = this.playwrightHelper.page;
|
|
139
143
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
144
|
+
focusedElement = await page.evaluate(readFocusedElement);
|
|
140
145
|
} catch (err) {
|
|
141
146
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
142
147
|
}
|
|
@@ -160,6 +165,7 @@ class Action {
|
|
|
160
165
|
iframeSnapshots,
|
|
161
166
|
ariaSnapshot,
|
|
162
167
|
ariaSnapshotFile,
|
|
168
|
+
focusedElement,
|
|
163
169
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
164
170
|
});
|
|
165
171
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -270,7 +276,7 @@ class Action {
|
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
278
|
|
|
273
|
-
private async executeOnce(code: string): Promise<Action> {
|
|
279
|
+
private async executeOnce(code: string, verbatim = false): Promise<Action> {
|
|
274
280
|
let error: Error | null = null;
|
|
275
281
|
|
|
276
282
|
setActivity('🔎 Browsing...', 'action');
|
|
@@ -287,8 +293,8 @@ class Action {
|
|
|
287
293
|
const tracer = trace.getTracer('ai');
|
|
288
294
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
289
295
|
setStepSpanParent(stepSpan);
|
|
290
|
-
const sanitizedCode = sanitizeCodeBlock(codeString);
|
|
291
|
-
const isPlaywright = hasPlaywrightCommands(sanitizedCode);
|
|
296
|
+
const sanitizedCode = verbatim ? codeString : sanitizeCodeBlock(codeString);
|
|
297
|
+
const isPlaywright = !verbatim && hasPlaywrightCommands(sanitizedCode);
|
|
292
298
|
|
|
293
299
|
try {
|
|
294
300
|
debugLog('Executing action:', codeString);
|
|
@@ -297,16 +303,21 @@ class Action {
|
|
|
297
303
|
throw new Error('No valid I.* or page.* commands found in code block');
|
|
298
304
|
}
|
|
299
305
|
|
|
306
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
|
|
307
|
+
|
|
300
308
|
if (isPlaywright) {
|
|
301
309
|
const page = this.playwrightHelper.page;
|
|
302
310
|
await playwrightSandbox(page, sanitizedCode);
|
|
303
311
|
await sleep(this.config.action?.delay || 500);
|
|
304
312
|
} else {
|
|
305
|
-
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
313
|
+
const returned = codeceptJSSandbox(this.actor, sanitizedCode);
|
|
306
314
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
307
315
|
await recorder.promise();
|
|
316
|
+
this.lastValue = await returned;
|
|
308
317
|
}
|
|
309
318
|
|
|
319
|
+
this.restorePageTimeout();
|
|
320
|
+
|
|
310
321
|
if (executedSteps.length > 0) {
|
|
311
322
|
codeString = executedSteps.join('\n');
|
|
312
323
|
}
|
|
@@ -325,6 +336,7 @@ class Action {
|
|
|
325
336
|
this.assertionSteps = [];
|
|
326
337
|
throw err;
|
|
327
338
|
} finally {
|
|
339
|
+
this.restorePageTimeout();
|
|
328
340
|
detachMainDocumentResponse();
|
|
329
341
|
if (groupId) await this.recorder!.endAction();
|
|
330
342
|
detachStepLogger(stepListener);
|
|
@@ -374,6 +386,10 @@ class Action {
|
|
|
374
386
|
return this.actionResult;
|
|
375
387
|
}
|
|
376
388
|
|
|
389
|
+
private restorePageTimeout(): void {
|
|
390
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
|
|
391
|
+
}
|
|
392
|
+
|
|
377
393
|
private async waitForPageReadiness(page: any): Promise<void> {
|
|
378
394
|
await waitForPageReadiness(page, {
|
|
379
395
|
timeout: this.config.playwright.waitForTimeout,
|
|
@@ -386,6 +402,10 @@ export default Action;
|
|
|
386
402
|
|
|
387
403
|
export type RecoveryRunner = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
388
404
|
|
|
405
|
+
export interface ExecuteOptions {
|
|
406
|
+
verbatim?: boolean;
|
|
407
|
+
}
|
|
408
|
+
|
|
389
409
|
function errorToString(error: any): string {
|
|
390
410
|
if (error.cliMessage) {
|
|
391
411
|
return error.cliMessage();
|
|
@@ -438,3 +458,23 @@ const detachStepLogger = (listener: StepListener) => {
|
|
|
438
458
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
439
459
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
440
460
|
};
|
|
461
|
+
|
|
462
|
+
const readFocusedElement = () => {
|
|
463
|
+
const el = document.activeElement as any;
|
|
464
|
+
if (!el || el === document.body) return null;
|
|
465
|
+
|
|
466
|
+
const tag = el.tagName.toLowerCase();
|
|
467
|
+
const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
|
|
468
|
+
let role = el.getAttribute('role') || tag;
|
|
469
|
+
if (tag === 'textarea' || el.isContentEditable) role = 'textbox';
|
|
470
|
+
if (tag === 'input' && textish.has(el.type)) role = 'textbox';
|
|
471
|
+
if (tag === 'select') role = 'combobox';
|
|
472
|
+
if (tag === 'a') role = 'link';
|
|
473
|
+
|
|
474
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
|
|
475
|
+
const focused: { role: string; name: string; value?: string } = { role, name: label.trim().slice(0, 80) };
|
|
476
|
+
|
|
477
|
+
const value = el.value ?? el.textContent;
|
|
478
|
+
if (typeof value === 'string' && value) focused.value = value.slice(0, 200);
|
|
479
|
+
return focused;
|
|
480
|
+
};
|
package/src/ai/captain/mixin.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ModeContext {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function resolveProjectRoot(): string | null {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
return
|
|
19
|
+
const configParser = ConfigParser.getInstance();
|
|
20
|
+
if (!configParser.getConfigPath()) return null;
|
|
21
|
+
return configParser.getProjectRoot();
|
|
22
22
|
}
|
|
@@ -18,7 +18,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
18
18
|
});
|
|
19
19
|
const { see, context, visualClick, learnExperience } = agentTools;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const tools: Record<string, any> = {
|
|
22
22
|
navigate: tool({
|
|
23
23
|
description: 'Navigate to a URL or page description using AI-powered navigation.',
|
|
24
24
|
inputSchema: z.object({
|
|
@@ -27,7 +27,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
27
27
|
execute: async ({ destination }) => {
|
|
28
28
|
try {
|
|
29
29
|
debugLog('navigate', destination);
|
|
30
|
-
await ctx.explorBot.
|
|
30
|
+
await ctx.explorBot.visit(destination);
|
|
31
31
|
const stateManager = ctx.explorBot.stateManager();
|
|
32
32
|
const state = stateManager.getCurrentState();
|
|
33
33
|
return { success: true, url: state?.url, title: state?.title };
|
|
@@ -123,11 +123,14 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
123
123
|
}),
|
|
124
124
|
|
|
125
125
|
...codeceptTools,
|
|
126
|
-
see,
|
|
127
126
|
context,
|
|
128
|
-
visualClick,
|
|
129
127
|
learnExperience,
|
|
130
128
|
};
|
|
129
|
+
|
|
130
|
+
if (see) tools.see = see;
|
|
131
|
+
if (visualClick) tools.visualClick = visualClick;
|
|
132
|
+
|
|
133
|
+
return tools;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
webModePrompt(): string {
|
package/src/ai/captain.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
21
21
|
import type { Provider } from './provider.ts';
|
|
22
22
|
import { Researcher } from './researcher.ts';
|
|
23
23
|
import { TaskAgent } from './task-agent.ts';
|
|
24
|
+
import { withdrawVisionTools } from './tools.ts';
|
|
24
25
|
|
|
25
26
|
const MAX_STEPS = 15;
|
|
26
27
|
|
|
@@ -437,6 +438,8 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
437
438
|
return;
|
|
438
439
|
}
|
|
439
440
|
|
|
441
|
+
withdrawVisionTools(tools);
|
|
442
|
+
|
|
440
443
|
const currentState = stateManager.getCurrentState();
|
|
441
444
|
if (!currentState && this.getMode() !== 'idle') {
|
|
442
445
|
stop();
|
package/src/ai/navigator.ts
CHANGED
|
@@ -8,12 +8,13 @@ import type { ExperienceTracker } from '../experience-tracker.js';
|
|
|
8
8
|
import Explorer from '../explorer.ts';
|
|
9
9
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
10
10
|
import { type StateManager, normalizeUrl } from '../state-manager.js';
|
|
11
|
+
import { renderAssertion } from '../playwright-recorder.ts';
|
|
11
12
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
12
13
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
13
14
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
14
15
|
import { loop, pause } from '../utils/loop.js';
|
|
15
16
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
16
|
-
import { extractStatePath } from '../utils/url-matcher.js';
|
|
17
|
+
import { extractStatePath, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
17
18
|
import type { Agent, AgentDeps } from './agent.js';
|
|
18
19
|
import type { Conversation } from './conversation.js';
|
|
19
20
|
import type { Provider } from './provider.js';
|
|
@@ -134,7 +135,7 @@ class Navigator implements Agent {
|
|
|
134
135
|
return false;
|
|
135
136
|
}
|
|
136
137
|
const currentUrl = this.getComparableCurrentUrl(stateManager, expectedUrl);
|
|
137
|
-
return
|
|
138
|
+
return matchesNavigationUrl(expectedUrl, currentUrl);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
async visit(url: string): Promise<void> {
|
|
@@ -189,14 +190,16 @@ class Navigator implements Agent {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
async resolveState(message: string, actionResult: ActionResult, opts?: { action?: Action; expectedUrl?: string }): Promise<boolean> {
|
|
193
|
+
async resolveState(message: string, actionResult: ActionResult, opts?: { action?: Action; expectedUrl?: string; onAttempt?: (attempt: { code: string; error?: string }) => void }): Promise<boolean> {
|
|
194
|
+
if (!this.provider) throw new Error('AI-assisted recovery is unavailable: no AI model is configured.');
|
|
195
|
+
|
|
193
196
|
tag('info').log('AI Navigator resolving state at', actionResult.url);
|
|
194
197
|
debugLog('Resolution message:', message);
|
|
195
198
|
|
|
196
199
|
const action = opts?.action ?? this.explorer.action();
|
|
197
200
|
const expectedUrl = opts?.expectedUrl;
|
|
198
201
|
|
|
199
|
-
const knowledge = this.knowledgeTracker.
|
|
202
|
+
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
200
203
|
let experience = '';
|
|
201
204
|
|
|
202
205
|
if (!actionResult.isInsideIframe) {
|
|
@@ -363,24 +366,27 @@ class Navigator implements Agent {
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
|
|
369
|
+
if (attemptOk) opts?.onAttempt?.({ code: codeBlock });
|
|
370
|
+
|
|
366
371
|
if (!attemptOk) {
|
|
367
372
|
const raw = action.lastError?.message || 'attempt failed';
|
|
368
373
|
const firstMeaningful = raw.split('\n').find((l) => l.trim() && !l.trim().startsWith('at ')) || raw;
|
|
369
374
|
const shortErr = firstMeaningful.replace(/\s+/g, ' ').trim().slice(0, 220);
|
|
370
375
|
batchFailures.push({ code: codeBlock, error: shortErr });
|
|
376
|
+
opts?.onAttempt?.({ code: codeBlock, error: shortErr });
|
|
371
377
|
}
|
|
372
378
|
|
|
373
379
|
if (expectedUrl) {
|
|
374
380
|
if (page) {
|
|
375
381
|
try {
|
|
376
|
-
await page.waitForURL((url: URL) =>
|
|
382
|
+
await page.waitForURL((url: URL) => matchesNavigationUrl(expectedUrl, `${url.pathname}${url.search}${url.hash}`), { timeout: 5000 });
|
|
377
383
|
} catch {
|
|
378
384
|
// URL did not transition to expectedUrl within timeout
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
const freshState = await this.explorer.capture();
|
|
382
388
|
const currentUrl = /^https?:\/\//i.test(expectedUrl) ? freshState.fullUrl || freshState.url || '' : freshState.url || '';
|
|
383
|
-
const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) &&
|
|
389
|
+
const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) && matchesNavigationUrl(expectedUrl, currentUrl);
|
|
384
390
|
const stateChanged = freshState.getStateHash() !== actionResult.getStateHash();
|
|
385
391
|
resolved = urlMatches && stateChanged;
|
|
386
392
|
|
|
@@ -615,7 +621,7 @@ class Navigator implements Agent {
|
|
|
615
621
|
return suggestion;
|
|
616
622
|
}
|
|
617
623
|
|
|
618
|
-
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
|
|
624
|
+
async verifyState(message: string, actionResult: ActionResult): Promise<{ verified: boolean; inexpressible: boolean; results: AssertionResult[]; successfulCodes: string[]; assertionSteps: Array<{ name: string; args: any[] }>; totalAttempted: number }> {
|
|
619
625
|
tag('info').log('AI Navigator verifying state at', actionResult.url);
|
|
620
626
|
debugLog('Verification message:', message);
|
|
621
627
|
|
|
@@ -625,7 +631,7 @@ class Navigator implements Agent {
|
|
|
625
631
|
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
626
632
|
}
|
|
627
633
|
|
|
628
|
-
const knowledge = this.knowledgeTracker.
|
|
634
|
+
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
629
635
|
let experience = '';
|
|
630
636
|
|
|
631
637
|
if (!actionResult.isInsideIframe) {
|
|
@@ -693,6 +699,7 @@ class Navigator implements Agent {
|
|
|
693
699
|
|
|
694
700
|
let codeBlocks: string[] = [];
|
|
695
701
|
const successfulCodes: string[] = [];
|
|
702
|
+
const results: AssertionResult[] = [];
|
|
696
703
|
const assertionSteps: Array<{ name: string; args: any[] }> = [];
|
|
697
704
|
|
|
698
705
|
const action = this.explorer.action();
|
|
@@ -734,6 +741,8 @@ class Navigator implements Agent {
|
|
|
734
741
|
await action.exitIframe();
|
|
735
742
|
|
|
736
743
|
const verified = await action.attempt(codeBlock, message);
|
|
744
|
+
const proof = action.assertionSteps.map(renderAssertion).filter(Boolean);
|
|
745
|
+
results.push({ code: codeBlock, passed: verified, proof });
|
|
737
746
|
|
|
738
747
|
if (verified) {
|
|
739
748
|
tag('success').log('Verification passed');
|
|
@@ -742,12 +751,6 @@ class Navigator implements Agent {
|
|
|
742
751
|
} else {
|
|
743
752
|
failures++;
|
|
744
753
|
}
|
|
745
|
-
|
|
746
|
-
const target = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
747
|
-
const majorityNeeded = Math.floor(target / 2) + 1;
|
|
748
|
-
if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
|
|
749
|
-
stop();
|
|
750
|
-
}
|
|
751
754
|
},
|
|
752
755
|
{
|
|
753
756
|
maxAttempts: this.verifyAttempts,
|
|
@@ -768,10 +771,16 @@ class Navigator implements Agent {
|
|
|
768
771
|
let verified = successfulCodes.length >= majorityNeeded;
|
|
769
772
|
if (alreadyVerified) verified = true;
|
|
770
773
|
|
|
774
|
+
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
775
|
+
if (inexpressible) {
|
|
776
|
+
tag('warning').log('No assertion could express this claim');
|
|
777
|
+
return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
|
|
778
|
+
}
|
|
779
|
+
|
|
771
780
|
actionResult.addVerification(message, verified);
|
|
772
781
|
this.stateManager.updateState(actionResult);
|
|
773
782
|
|
|
774
|
-
return { verified, successfulCodes, assertionSteps, totalAttempted };
|
|
783
|
+
return { verified, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
|
|
775
784
|
}
|
|
776
785
|
|
|
777
786
|
private checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean {
|
|
@@ -782,4 +791,6 @@ class Navigator implements Agent {
|
|
|
782
791
|
}
|
|
783
792
|
}
|
|
784
793
|
|
|
794
|
+
export type AssertionResult = { code: string; passed: boolean; proof: string[] };
|
|
795
|
+
|
|
785
796
|
export { Navigator };
|
package/src/ai/pilot.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type Explorer from '../explorer.ts';
|
|
|
8
8
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
10
|
import { type Test, TestResult } from '../test-plan.ts';
|
|
11
|
-
import { collectInteractiveNodes, detectFocusArea
|
|
11
|
+
import { collectInteractiveNodes, detectFocusArea } from '../utils/aria.ts';
|
|
12
12
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
13
13
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
14
14
|
|
|
@@ -22,6 +22,7 @@ import type { Provider } from './provider.ts';
|
|
|
22
22
|
import type { Researcher } from './researcher.ts';
|
|
23
23
|
import { capabilityGroundingRule, dataProtectionRules } from './rules.ts';
|
|
24
24
|
import { isInteractive } from './task-agent.ts';
|
|
25
|
+
import { withdrawVisionTools } from './tools.ts';
|
|
25
26
|
|
|
26
27
|
const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
|
|
27
28
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
@@ -561,6 +562,55 @@ export class Pilot implements Agent {
|
|
|
561
562
|
return text;
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
async settleExpectations(task: Test): Promise<Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>> {
|
|
566
|
+
const undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
567
|
+
const decided = (text: string): 'passed' | 'failed' => {
|
|
568
|
+
if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text)) return 'passed';
|
|
569
|
+
return 'failed';
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
if (!undecided.length) return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
573
|
+
|
|
574
|
+
const schema = z.object({
|
|
575
|
+
outcomes: z.array(
|
|
576
|
+
z.object({
|
|
577
|
+
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
578
|
+
status: z.enum(['passed', 'failed', 'unverified']).describe('passed = the log shows it happened, failed = the log shows it did not, unverified = the run never established either way'),
|
|
579
|
+
})
|
|
580
|
+
),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const userContent = dedent`
|
|
584
|
+
A test run has finished. Decide, for each expected outcome, what the run established about it.
|
|
585
|
+
|
|
586
|
+
<expected_outcomes>
|
|
587
|
+
${undecided.map((text) => `- ${text}`).join('\n')}
|
|
588
|
+
</expected_outcomes>
|
|
589
|
+
|
|
590
|
+
<run_log>
|
|
591
|
+
${task.notesToString() || 'No steps recorded.'}
|
|
592
|
+
</run_log>
|
|
593
|
+
|
|
594
|
+
The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
|
|
595
|
+
differently. Judge by what the steps show happened, not by whether the wording matches.
|
|
596
|
+
Choose "unverified" only when the log neither shows the outcome happening nor shows it failing —
|
|
597
|
+
that is a statement about the run, not about the application.
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
const response = await this.provider
|
|
601
|
+
.generateObject([{ role: 'user' as const, content: userContent }], schema, this.provider.getAgenticModel('pilot'), {
|
|
602
|
+
agentName: 'pilot',
|
|
603
|
+
telemetry: { functionId: 'pilot.settleExpectations' },
|
|
604
|
+
})
|
|
605
|
+
.catch(() => null);
|
|
606
|
+
|
|
607
|
+
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome.status]));
|
|
608
|
+
return task.expected.map((text) => {
|
|
609
|
+
if (!undecided.includes(text)) return { text, status: decided(text) };
|
|
610
|
+
return { text, status: (judged.get(text) as 'passed' | 'failed' | 'unverified') || 'unverified' };
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
564
614
|
private formatExpectations(task: Test): string {
|
|
565
615
|
const checked = task.getCheckedExpectations();
|
|
566
616
|
const remaining = task.getRemainingExpectations();
|
|
@@ -620,6 +670,7 @@ export class Pilot implements Agent {
|
|
|
620
670
|
if (xpathCheck) planning.xpathCheck = xpathCheck;
|
|
621
671
|
if (learnExperience) planning.learnExperience = learnExperience;
|
|
622
672
|
if (askUser) planning.askUser = askUser;
|
|
673
|
+
withdrawVisionTools(planning);
|
|
623
674
|
return planning;
|
|
624
675
|
}
|
|
625
676
|
|
|
@@ -703,7 +754,7 @@ export class Pilot implements Agent {
|
|
|
703
754
|
lines.push(`url: ${state.url}`);
|
|
704
755
|
lines.push(`title: ${state.title || 'unknown'}`);
|
|
705
756
|
|
|
706
|
-
const focused =
|
|
757
|
+
const focused = state.focusedElement;
|
|
707
758
|
if (focused) {
|
|
708
759
|
const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
|
|
709
760
|
lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
|
package/src/ai/planner.ts
CHANGED
|
@@ -53,6 +53,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
53
53
|
provider: Provider;
|
|
54
54
|
stateManager: StateManager;
|
|
55
55
|
private experienceTracker: ExperienceTracker;
|
|
56
|
+
private knowledgeTracker: AgentDeps['knowledgeTracker'];
|
|
56
57
|
|
|
57
58
|
MIN_TASKS = 3;
|
|
58
59
|
MAX_TASKS = 12;
|
|
@@ -70,6 +71,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
70
71
|
this.researcher = researcher;
|
|
71
72
|
this.stateManager = deps.stateManager;
|
|
72
73
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
74
|
+
this.knowledgeTracker = deps.knowledgeTracker;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
setFisherman(fisherman: Fisherman): void {
|
|
@@ -411,6 +413,11 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
411
413
|
</page_research>
|
|
412
414
|
`);
|
|
413
415
|
|
|
416
|
+
const applicationContext = this.knowledgeTracker.renderApplicationSpec(state);
|
|
417
|
+
if (applicationContext) {
|
|
418
|
+
conversation.addUserText(applicationContext);
|
|
419
|
+
}
|
|
420
|
+
|
|
414
421
|
conversation.addUserText(dedent`
|
|
415
422
|
${this.buildApproach(style)}
|
|
416
423
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -326,6 +326,7 @@ export class Provider {
|
|
|
326
326
|
try {
|
|
327
327
|
const response = await withRetry(async () => {
|
|
328
328
|
const result = await generateText({ messages, ...config });
|
|
329
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
329
330
|
if (!result.text) {
|
|
330
331
|
debugLog(result);
|
|
331
332
|
if (result.finishReason === 'length') {
|
|
@@ -342,8 +343,6 @@ export class Provider {
|
|
|
342
343
|
clearActivity();
|
|
343
344
|
responseLog(response.text);
|
|
344
345
|
|
|
345
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
346
|
-
|
|
347
346
|
return response;
|
|
348
347
|
} catch (error: any) {
|
|
349
348
|
clearActivity();
|
|
@@ -378,6 +377,7 @@ export class Provider {
|
|
|
378
377
|
try {
|
|
379
378
|
const response = await withRetry(async () => {
|
|
380
379
|
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
380
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
381
381
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
382
382
|
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
383
383
|
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
@@ -397,8 +397,6 @@ export class Provider {
|
|
|
397
397
|
|
|
398
398
|
responseLog(response.text);
|
|
399
399
|
|
|
400
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
401
|
-
|
|
402
400
|
return response;
|
|
403
401
|
} catch (error: any) {
|
|
404
402
|
clearActivity();
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
24
24
|
import { Provider } from './provider.ts';
|
|
25
25
|
import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
|
|
26
26
|
import { TaskAgent } from './task-agent.ts';
|
|
27
|
-
import { createCodeceptJSTools } from './tools.ts';
|
|
27
|
+
import { createCodeceptJSTools, withdrawVisionTools } from './tools.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:rerunner');
|
|
30
30
|
|
|
@@ -361,6 +361,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
361
361
|
},
|
|
362
362
|
}),
|
|
363
363
|
};
|
|
364
|
+
withdrawVisionTools(tools);
|
|
364
365
|
|
|
365
366
|
const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
|
|
366
367
|
conversation.addUserText(this.getHealUserPrompt(failedCode, error));
|
package/src/ai/researcher.ts
CHANGED
|
@@ -102,7 +102,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
102
102
|
const cached = getCachedResearch(stateHash);
|
|
103
103
|
if (cached) {
|
|
104
104
|
debugLog('Previous research result found');
|
|
105
|
-
return
|
|
105
|
+
return cached;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -438,7 +438,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
438
438
|
if (!this.actionResult) throw new Error('actionResult is not set');
|
|
439
439
|
|
|
440
440
|
const html = await this.actionResult.combinedHtml();
|
|
441
|
-
const knowledge = this.knowledgeTracker.
|
|
441
|
+
const knowledge = this.knowledgeTracker.renderRelevantContext(this.actionResult);
|
|
442
442
|
|
|
443
443
|
const ariaSnapshot = this.actionResult.getCompactARIA();
|
|
444
444
|
|
package/src/ai/rules.ts
CHANGED
|
@@ -4,13 +4,18 @@ export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField',
|
|
|
4
4
|
|
|
5
5
|
const locatorPriorityRule = dedent`
|
|
6
6
|
<locator_priority>
|
|
7
|
+
When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
|
|
8
|
+
and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
|
|
9
|
+
below is for elements the context gives no ref for.
|
|
10
|
+
|
|
7
11
|
Use the following priority when selecting locators:
|
|
8
12
|
|
|
9
13
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
10
14
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
11
|
-
|
|
15
|
+
Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
|
|
16
|
+
If the element is absent from the snapshot, do not invent one; use text or CSS instead.
|
|
12
17
|
|
|
13
|
-
2. Text locators (second choice) - use only when
|
|
18
|
+
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
14
19
|
Example: 'Login', 'Submit', 'Username'
|
|
15
20
|
Skip if the same text appears multiple times on the page
|
|
16
21
|
|
|
@@ -26,10 +31,10 @@ const locatorPriorityRule = dedent`
|
|
|
26
31
|
|
|
27
32
|
const contextSimplificationRule = dedent`
|
|
28
33
|
<context_simplification>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
34
|
+
- Add a container when the target may match several elements, or the UI map gives a verified
|
|
35
|
+
section container: I.click('Save', '.modal')
|
|
36
|
+
- Skip the container when the locator is already unique (XPath, unique ID, unique text)
|
|
37
|
+
- A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
|
|
33
38
|
- No need for complex ARIA when container narrows scope sufficiently
|
|
34
39
|
</context_simplification>
|
|
35
40
|
`;
|
|
@@ -59,7 +64,7 @@ const locatorStrategyRule = dedent`
|
|
|
59
64
|
|
|
60
65
|
<good_aria_locator_example>
|
|
61
66
|
{ "role": "button", "text": "Login" },
|
|
62
|
-
{ "role": "
|
|
67
|
+
{ "role": "textbox", "text": "Name" },
|
|
63
68
|
{ "role": "link", "text": "Forgot your password?" },
|
|
64
69
|
{ "role": "link", "text": "Sign Up" },
|
|
65
70
|
{ "role": "button", "text": "Sign In" },
|
|
@@ -105,7 +110,7 @@ const locatorStrategyRule = dedent`
|
|
|
105
110
|
</xpath_rules>
|
|
106
111
|
|
|
107
112
|
<good locator example>
|
|
108
|
-
'
|
|
113
|
+
'input[placeholder="Name"]'
|
|
109
114
|
'[aria-label="Name"]'
|
|
110
115
|
'form#user_form input[name="name"]'
|
|
111
116
|
'#content-top #user_name'
|
|
@@ -245,7 +250,8 @@ export const unexpectedPopupRule = dedent`
|
|
|
245
250
|
|
|
246
251
|
export const sectionContextRule = dedent`
|
|
247
252
|
<section_context_rule>
|
|
248
|
-
|
|
253
|
+
Use a container from UI map sections when the target may match several elements. A container that is
|
|
254
|
+
wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
|
|
249
255
|
|
|
250
256
|
1. Identify which section contains the target element
|
|
251
257
|
2. Get the Context Locator from that section in the UI map
|
|
@@ -263,9 +269,10 @@ export const sectionContextRule = dedent`
|
|
|
263
269
|
For CSS locators - prepend section context:
|
|
264
270
|
- I.click('.main button.submit') // instead of I.click('button.submit')
|
|
265
271
|
|
|
266
|
-
|
|
272
|
+
Omit context when the locator already resolves to one element:
|
|
267
273
|
- Locator is XPath (already includes path context)
|
|
268
274
|
- Locator is a unique ID (#specific-element)
|
|
275
|
+
- Text or ARIA that appears only once on the page
|
|
269
276
|
</section_context_rule>
|
|
270
277
|
|
|
271
278
|
${unexpectedPopupRule}
|