explorbot 0.2.3 → 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 +7 -1
- package/boat/api-tester/src/config.ts +4 -2
- 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/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +115 -86
- package/boat/prima/src/envelope.ts +80 -52
- package/boat/prima/src/prima.ts +547 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +7 -1
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- 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/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +97 -77
- package/dist/boat/prima/src/envelope.js +70 -44
- package/dist/boat/prima/src/prima.js +515 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- 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 +2 -0
- package/dist/src/action.js +36 -1
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +7 -0
- package/dist/src/ai/navigator.js +10 -6
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +33 -33
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +13 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +3 -1
- package/dist/src/explorer.js +2 -3
- 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/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/url-matcher.js +3 -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/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +38 -2
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +14 -8
- package/src/ai/pilot.ts +53 -2
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +32 -33
- package/src/ai/tools.ts +132 -36
- package/src/config.ts +15 -4
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +4 -2
- package/src/explorer.ts +2 -2
- 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/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/dist/src/ai/rules.js
CHANGED
|
@@ -2,13 +2,18 @@ import dedent from 'dedent';
|
|
|
2
2
|
export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'];
|
|
3
3
|
const locatorPriorityRule = dedent `
|
|
4
4
|
<locator_priority>
|
|
5
|
+
When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
|
|
6
|
+
and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
|
|
7
|
+
below is for elements the context gives no ref for.
|
|
8
|
+
|
|
5
9
|
Use the following priority when selecting locators:
|
|
6
10
|
|
|
7
11
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
8
12
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
9
|
-
|
|
13
|
+
Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
|
|
14
|
+
If the element is absent from the snapshot, do not invent one; use text or CSS instead.
|
|
10
15
|
|
|
11
|
-
2. Text locators (second choice) - use only when
|
|
16
|
+
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
12
17
|
Example: 'Login', 'Submit', 'Username'
|
|
13
18
|
Skip if the same text appears multiple times on the page
|
|
14
19
|
|
|
@@ -23,10 +28,10 @@ const locatorPriorityRule = dedent `
|
|
|
23
28
|
`;
|
|
24
29
|
const contextSimplificationRule = dedent `
|
|
25
30
|
<context_simplification>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
31
|
+
- Add a container when the target may match several elements, or the UI map gives a verified
|
|
32
|
+
section container: I.click('Save', '.modal')
|
|
33
|
+
- Skip the container when the locator is already unique (XPath, unique ID, unique text)
|
|
34
|
+
- A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
|
|
30
35
|
- No need for complex ARIA when container narrows scope sufficiently
|
|
31
36
|
</context_simplification>
|
|
32
37
|
`;
|
|
@@ -55,7 +60,7 @@ const locatorStrategyRule = dedent `
|
|
|
55
60
|
|
|
56
61
|
<good_aria_locator_example>
|
|
57
62
|
{ "role": "button", "text": "Login" },
|
|
58
|
-
{ "role": "
|
|
63
|
+
{ "role": "textbox", "text": "Name" },
|
|
59
64
|
{ "role": "link", "text": "Forgot your password?" },
|
|
60
65
|
{ "role": "link", "text": "Sign Up" },
|
|
61
66
|
{ "role": "button", "text": "Sign In" },
|
|
@@ -101,7 +106,7 @@ const locatorStrategyRule = dedent `
|
|
|
101
106
|
</xpath_rules>
|
|
102
107
|
|
|
103
108
|
<good locator example>
|
|
104
|
-
'
|
|
109
|
+
'input[placeholder="Name"]'
|
|
105
110
|
'[aria-label="Name"]'
|
|
106
111
|
'form#user_form input[name="name"]'
|
|
107
112
|
'#content-top #user_name'
|
|
@@ -231,7 +236,8 @@ export const unexpectedPopupRule = dedent `
|
|
|
231
236
|
`;
|
|
232
237
|
export const sectionContextRule = dedent `
|
|
233
238
|
<section_context_rule>
|
|
234
|
-
|
|
239
|
+
Use a container from UI map sections when the target may match several elements. A container that is
|
|
240
|
+
wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
|
|
235
241
|
|
|
236
242
|
1. Identify which section contains the target element
|
|
237
243
|
2. Get the Context Locator from that section in the UI map
|
|
@@ -249,9 +255,10 @@ export const sectionContextRule = dedent `
|
|
|
249
255
|
For CSS locators - prepend section context:
|
|
250
256
|
- I.click('.main button.submit') // instead of I.click('button.submit')
|
|
251
257
|
|
|
252
|
-
|
|
258
|
+
Omit context when the locator already resolves to one element:
|
|
253
259
|
- Locator is XPath (already includes path context)
|
|
254
260
|
- Locator is a unique ID (#specific-element)
|
|
261
|
+
- Text or ARIA that appears only once on the page
|
|
255
262
|
</section_context_rule>
|
|
256
263
|
|
|
257
264
|
${unexpectedPopupRule}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { executionController } from "../execution-controller.js";
|
|
1
2
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
2
3
|
export function isInteractive() {
|
|
3
|
-
|
|
4
|
+
if (process.env.INK_RUNNING === 'true')
|
|
5
|
+
return true;
|
|
6
|
+
return executionController.hasInputCallback();
|
|
4
7
|
}
|
|
5
8
|
function createNullProxy() {
|
|
6
9
|
return new Proxy({}, {
|
package/dist/src/ai/tester.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
31
31
|
seenUiMapUrls: Set<string>;
|
|
32
32
|
lastAnalyzedStateHash: string | null;
|
|
33
33
|
stalledIterations: number;
|
|
34
|
-
hasSuccessfulAssertion: boolean;
|
|
35
34
|
readonly MAX_STALLED_ITERATIONS = 3;
|
|
36
35
|
constructor(deps: AgentDeps, researcher: Researcher, navigator: Navigator, agentTools?: any);
|
|
37
36
|
getNavigator(): Navigator;
|
|
@@ -50,6 +49,7 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
50
49
|
shouldStopForStalledExecution(task: Test, previousState: ActionResult, toolExecutions: any[]): boolean;
|
|
51
50
|
prepareInstructionsForNextStep(task: Test): Promise<string>;
|
|
52
51
|
reinjectContextIfNeeded(iteration: number, currentState: ActionResult): Promise<string>;
|
|
52
|
+
interactiveAriaWithRefs(state: ActionResult): Promise<string>;
|
|
53
53
|
finishTest(task: Test): void;
|
|
54
54
|
abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{
|
|
55
55
|
success: boolean;
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -8,13 +8,13 @@ import { clearActivity, setActivity } from "../activity.js";
|
|
|
8
8
|
import { Observability } from "../observability.js";
|
|
9
9
|
import { Stats } from "../stats.js";
|
|
10
10
|
import { TestResult } from "../test-plan.js";
|
|
11
|
-
import {
|
|
11
|
+
import { compactAriaSnapshot, detectFocusArea } from "../utils/aria.js";
|
|
12
12
|
import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
|
|
13
13
|
import { createDebug, tag } from "../utils/logger.js";
|
|
14
14
|
import { loop } from "../utils/loop.js";
|
|
15
15
|
import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from "./rules.js";
|
|
16
16
|
import { TaskAgent } from "./task-agent.js";
|
|
17
|
-
import { createCodeceptJSTools, createIframeTools } from "./tools.js";
|
|
17
|
+
import { createCodeceptJSTools, createIframeTools, withdrawVisionTools } from "./tools.js";
|
|
18
18
|
const debugLog = createDebug('explorbot:tester');
|
|
19
19
|
const SAMPLE_FILES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../assets/sample-files');
|
|
20
20
|
const SAMPLE_FILES = {
|
|
@@ -48,7 +48,6 @@ export class Tester extends TaskAgent {
|
|
|
48
48
|
seenUiMapUrls = new Set();
|
|
49
49
|
lastAnalyzedStateHash = null;
|
|
50
50
|
stalledIterations = 0;
|
|
51
|
-
hasSuccessfulAssertion = false;
|
|
52
51
|
MAX_STALLED_ITERATIONS = 3;
|
|
53
52
|
constructor(deps, researcher, navigator, agentTools) {
|
|
54
53
|
super(deps);
|
|
@@ -88,14 +87,13 @@ export class Tester extends TaskAgent {
|
|
|
88
87
|
this.seenUiMapUrls.clear();
|
|
89
88
|
this.lastAnalyzedStateHash = null;
|
|
90
89
|
this.stalledIterations = 0;
|
|
91
|
-
this.hasSuccessfulAssertion = false;
|
|
92
90
|
this.stateManager.clearHistory();
|
|
93
91
|
this.resetFailureCount();
|
|
94
92
|
this.pilot?.reset();
|
|
95
93
|
const requestStore = this.requestStore;
|
|
96
94
|
requestStore.clear();
|
|
97
95
|
const offFailedRequest = requestStore.onFailedRequest((r) => {
|
|
98
|
-
task.
|
|
96
|
+
task.addObservation(`Network error: ${r.method} ${r.path} → ${r.status}`);
|
|
99
97
|
});
|
|
100
98
|
const initialState = ActionResult.fromState(state);
|
|
101
99
|
if (isErrorPage(initialState)) {
|
|
@@ -208,6 +206,7 @@ export class Tester extends TaskAgent {
|
|
|
208
206
|
if (currentState.isInsideIframe) {
|
|
209
207
|
Object.assign(tools, createIframeTools(this.toolDeps));
|
|
210
208
|
}
|
|
209
|
+
withdrawVisionTools(tools);
|
|
211
210
|
debugLog(`Test ${task.scenario} iteration ${iteration}`);
|
|
212
211
|
if (this.stateManager.isInDeadLoop()) {
|
|
213
212
|
task.addNote('Dead loop detected. Stopped');
|
|
@@ -268,15 +267,8 @@ export class Tester extends TaskAgent {
|
|
|
268
267
|
const allToolNames = result?.toolExecutions?.map((execution) => execution.toolName) || [];
|
|
269
268
|
const successfulToolNames = result?.toolExecutions?.filter((execution) => execution.wasSuccessful)?.map((execution) => execution.toolName) || [];
|
|
270
269
|
const actionPerformed = !!allToolNames.find((toolName) => this.ACTION_TOOLS.includes(toolName));
|
|
271
|
-
const successfulActionPerformed = !!successfulToolNames.find((toolName) => this.ACTION_TOOLS.includes(toolName));
|
|
272
270
|
assertionPerformed = !!successfulToolNames.find((toolName) => this.ASSERTION_TOOLS.includes(toolName));
|
|
273
271
|
const wasSuccessful = result?.toolExecutions?.every((execution) => execution.wasSuccessful);
|
|
274
|
-
if (successfulActionPerformed) {
|
|
275
|
-
this.hasSuccessfulAssertion = false;
|
|
276
|
-
}
|
|
277
|
-
if (assertionPerformed) {
|
|
278
|
-
this.hasSuccessfulAssertion = true;
|
|
279
|
-
}
|
|
280
272
|
this.trackToolExecutions(result?.toolExecutions || []);
|
|
281
273
|
if (this.consecutiveEmptyResults >= 5) {
|
|
282
274
|
task.addNote('AI model is not responding with actions. Stopped');
|
|
@@ -360,6 +352,7 @@ export class Tester extends TaskAgent {
|
|
|
360
352
|
if (extensions >= this.MAX_EXTENSIONS)
|
|
361
353
|
break;
|
|
362
354
|
extensions++;
|
|
355
|
+
this.stalledIterations = 0;
|
|
363
356
|
tag('info').log(`Pilot extending test (${extensions}/${this.MAX_EXTENSIONS})`);
|
|
364
357
|
conversation.cleanupTag('page_aria', '...trimmed...', 1);
|
|
365
358
|
conversation.cleanupTag('page_html', '...trimmed...', 0);
|
|
@@ -414,12 +407,7 @@ export class Tester extends TaskAgent {
|
|
|
414
407
|
this.stalledIterations++;
|
|
415
408
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS)
|
|
416
409
|
return false;
|
|
417
|
-
|
|
418
|
-
task.addNote('No further browser progress after successful verification; requesting final review');
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
task.addNote('No browser progress after repeated attempts on unchanged page', TestResult.FAILED);
|
|
422
|
-
task.finish(TestResult.FAILED);
|
|
410
|
+
task.addNote('No further browser progress on unchanged page; requesting final review');
|
|
423
411
|
return true;
|
|
424
412
|
}
|
|
425
413
|
async prepareInstructionsForNextStep(task) {
|
|
@@ -437,7 +425,8 @@ export class Tester extends TaskAgent {
|
|
|
437
425
|
</rules>
|
|
438
426
|
`;
|
|
439
427
|
if (task.getPrintableNotes()) {
|
|
440
|
-
outcomeStatus
|
|
428
|
+
outcomeStatus += dedent `
|
|
429
|
+
|
|
441
430
|
Your current log:
|
|
442
431
|
<notes>
|
|
443
432
|
${task.notesToString()}
|
|
@@ -458,7 +447,7 @@ export class Tester extends TaskAgent {
|
|
|
458
447
|
this.previousStateHash = currentStateHash;
|
|
459
448
|
let context = '';
|
|
460
449
|
const focusArea = detectFocusArea(currentState.ariaSnapshot);
|
|
461
|
-
const focusedElement =
|
|
450
|
+
const focusedElement = currentState.focusedElement;
|
|
462
451
|
if (focusedElement) {
|
|
463
452
|
const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
|
|
464
453
|
context += dedent `
|
|
@@ -539,7 +528,7 @@ export class Tester extends TaskAgent {
|
|
|
539
528
|
</page>
|
|
540
529
|
|
|
541
530
|
<page_aria>
|
|
542
|
-
${
|
|
531
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
543
532
|
</page_aria>
|
|
544
533
|
${uiMapSection}
|
|
545
534
|
|
|
@@ -574,26 +563,32 @@ export class Tester extends TaskAgent {
|
|
|
574
563
|
</page>
|
|
575
564
|
|
|
576
565
|
<page_aria>
|
|
577
|
-
${
|
|
566
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
578
567
|
</page_aria>
|
|
579
568
|
`;
|
|
580
569
|
}
|
|
570
|
+
async interactiveAriaWithRefs(state) {
|
|
571
|
+
const withRefs = await Promise.resolve(this.explorer?.withPage?.((page) => page.locator('body').ariaSnapshot({ mode: 'ai' }))).catch(() => null);
|
|
572
|
+
if (!withRefs)
|
|
573
|
+
return state.getInteractiveARIA();
|
|
574
|
+
return compactAriaSnapshot(withRefs, false);
|
|
575
|
+
}
|
|
581
576
|
finishTest(task) {
|
|
582
|
-
if (!task.
|
|
583
|
-
task.
|
|
577
|
+
if (!task.result) {
|
|
578
|
+
if (task.hasAchievedAll())
|
|
579
|
+
task.finish(TestResult.PASSED);
|
|
580
|
+
else
|
|
581
|
+
task.finish(TestResult.FAILED);
|
|
584
582
|
}
|
|
585
583
|
if (task.isSuccessful) {
|
|
586
584
|
tag('success').log(`Successful test: ${task.scenario}`);
|
|
585
|
+
return;
|
|
587
586
|
}
|
|
588
|
-
|
|
587
|
+
if (task.isSkipped) {
|
|
589
588
|
tag('warning').log(`Skipped test: ${task.scenario}`);
|
|
589
|
+
return;
|
|
590
590
|
}
|
|
591
|
-
|
|
592
|
-
tag('error').log(`Failed test: ${task.scenario}`);
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
tag('warning').log(`Test with no result: ${task.scenario}`);
|
|
596
|
-
}
|
|
591
|
+
tag('error').log(`Failed test: ${task.scenario}`);
|
|
597
592
|
}
|
|
598
593
|
async abortStartedTestOnErrorPage(task, actionResult) {
|
|
599
594
|
const error = new ErrorPageError(actionResult.url || task.startUrl || '', actionResult.title, actionResult.httpStatus);
|
|
@@ -734,6 +729,9 @@ export class Tester extends TaskAgent {
|
|
|
734
729
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
735
730
|
</expected_results>
|
|
736
731
|
|
|
732
|
+
An expected result counts as settled only when you record it back word for word as it is written above.
|
|
733
|
+
A note in your own wording is a general note and leaves that result unsettled.
|
|
734
|
+
|
|
737
735
|
Your goal is to perform actions on the web page and verify the expected outcomes.
|
|
738
736
|
Try to achieve as many goals as possible.
|
|
739
737
|
If goal is not achievable, log that and skip to next one.
|
|
@@ -968,6 +966,9 @@ export class Tester extends TaskAgent {
|
|
|
968
966
|
- You unsuccessfully tried multiple iterations and failed
|
|
969
967
|
- If the expected result was expected to fail, use status="success" instead
|
|
970
968
|
|
|
969
|
+
When a note settles one of the expected results, that note must repeat the expected result word
|
|
970
|
+
for word. Paraphrasing it leaves the expected result unsettled and it is reported as unverified.
|
|
971
|
+
|
|
971
972
|
Example:
|
|
972
973
|
- record({ notes: ["clicked login button", "login form appeared", "fill credentials"], status: "success" })
|
|
973
974
|
`,
|
|
@@ -1045,8 +1046,7 @@ export class Tester extends TaskAgent {
|
|
|
1045
1046
|
this.stalledIterations++;
|
|
1046
1047
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS)
|
|
1047
1048
|
return false;
|
|
1048
|
-
task.addNote('No browser progress after repeated execution errors'
|
|
1049
|
-
task.finish(TestResult.FAILED);
|
|
1049
|
+
task.addNote('No browser progress after repeated execution errors; requesting final review');
|
|
1050
1050
|
return true;
|
|
1051
1051
|
}
|
|
1052
1052
|
async cleanupStartedTest(task) {
|
package/dist/src/ai/tools.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export declare function createCodeceptJSTools({ explorer, stateManager, ai }: To
|
|
|
16
16
|
commands: any;
|
|
17
17
|
explanation: any;
|
|
18
18
|
}, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
|
|
19
|
+
clickRef: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
20
|
+
ref: any;
|
|
21
|
+
element: any;
|
|
22
|
+
}, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
|
|
19
23
|
hover: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
20
24
|
commands: any;
|
|
21
25
|
explanation: any;
|
|
@@ -49,5 +53,15 @@ export declare function createLearnExperienceTool({ getExperienceTracker, getSta
|
|
|
49
53
|
error: string;
|
|
50
54
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
51
55
|
export declare function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any;
|
|
56
|
+
export declare function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void>;
|
|
57
|
+
export declare function successToolResult(action: string, data?: Record<string, any>, source?: {
|
|
58
|
+
playwrightGroupId?: string | null;
|
|
59
|
+
assertionSteps?: any[];
|
|
60
|
+
}): Record<string, any>;
|
|
52
61
|
export declare function isMajorPageChange(pageDiff: PageDiff): boolean;
|
|
62
|
+
export declare function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null): Promise<Record<string, any>>;
|
|
63
|
+
export declare function withdrawVisionTools(tools: Record<string, any>): void;
|
|
64
|
+
export declare function clickFailureSuggestion(attempts: Array<{
|
|
65
|
+
error?: string;
|
|
66
|
+
}>): string;
|
|
53
67
|
export {};
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -2,8 +2,9 @@ import { tool } from 'ai';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
|
+
import { Stats } from "../stats.js";
|
|
5
6
|
import { TestResult } from '../test-plan.js';
|
|
6
|
-
import { LARGE_ARIA_CHANGE_THRESHOLD
|
|
7
|
+
import { LARGE_ARIA_CHANGE_THRESHOLD } from "../utils/aria.js";
|
|
7
8
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
8
9
|
import { createDebug, tag } from '../utils/logger.js';
|
|
9
10
|
import { pause } from '../utils/loop.js';
|
|
@@ -18,6 +19,10 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
18
19
|
description: dedent `
|
|
19
20
|
Click an element by trying multiple CodeceptJS commands in order until one succeeds.
|
|
20
21
|
|
|
22
|
+
Use this only for elements the page context gives you no ref for. When the element shows a ref such as [ref=e14],
|
|
23
|
+
call clickRef with that ref instead — composing a locator for an element that already has a ref is wasted work,
|
|
24
|
+
and a locator can match several elements where a ref cannot.
|
|
25
|
+
|
|
21
26
|
Follow <locator_priority> from system prompt for locator selection.
|
|
22
27
|
|
|
23
28
|
I.click(locator) - click element matching locator
|
|
@@ -36,14 +41,13 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
36
41
|
commands: z.array(z.string()).describe(dedent `
|
|
37
42
|
FALLBACK LOCATORS for ONE element. All commands must click the SAME element.
|
|
38
43
|
Never mix different elements — use separate click() calls instead.
|
|
44
|
+
REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
|
|
39
45
|
Order by reliability:
|
|
40
|
-
1. I.click(text, container) -
|
|
46
|
+
1. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
|
|
41
47
|
2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
42
48
|
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
43
49
|
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
44
50
|
5. I.clickXY(x, y) - coordinates fallback
|
|
45
|
-
IMPORTANT: Always include at least one command WITHOUT a container as fallback,
|
|
46
|
-
in case the element moved to a different section (e.g. I.click("Save") without container).
|
|
47
51
|
`),
|
|
48
52
|
explanation: z.string().describe('Why you are clicking this element'),
|
|
49
53
|
}),
|
|
@@ -106,14 +110,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
106
110
|
}
|
|
107
111
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
|
|
108
112
|
await commitNote(activeNote, TestResult.FAILED, toolResult, action);
|
|
109
|
-
|
|
110
|
-
const lastError = attempts[attempts.length - 1]?.error || '';
|
|
111
|
-
if (lastError.includes('was not found') || lastError.includes('not found by text')) {
|
|
112
|
-
suggestion = 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
113
|
-
}
|
|
114
|
-
else if (lastError.includes('Timeout') || lastError.includes('intercept')) {
|
|
115
|
-
suggestion = 'Element exists but could not be clicked (possibly covered by overlay or not interactable). Try closing overlapping panels first, or use visualClick().';
|
|
116
|
-
}
|
|
113
|
+
const suggestion = clickFailureSuggestion(attempts);
|
|
117
114
|
return failedToolResult('click', 'All click commands failed', {
|
|
118
115
|
...toolResult,
|
|
119
116
|
attempts,
|
|
@@ -121,6 +118,38 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
121
118
|
}, action.lastError);
|
|
122
119
|
},
|
|
123
120
|
}),
|
|
121
|
+
clickRef: tool({
|
|
122
|
+
description: dedent `
|
|
123
|
+
Click an element by the ref the page context gave it, e.g. [ref=e14].
|
|
124
|
+
|
|
125
|
+
Prefer this over click() whenever the element you want carries a ref. A ref names one exact element, so it
|
|
126
|
+
cannot match several by mistake and never needs disambiguating — it is the fastest way to click.
|
|
127
|
+
Only pass a ref that appears in the page context you were given. Never invent or guess one.
|
|
128
|
+
If it reports the ref is gone, the page has been rebuilt: get fresh context and use the new ref.
|
|
129
|
+
`,
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
ref: z.string().describe('The ref exactly as it appears in the page context, e.g. "e14"'),
|
|
132
|
+
element: z.string().describe('Role and name of the element you are clicking, for the record'),
|
|
133
|
+
}),
|
|
134
|
+
execute: async ({ ref, element }) => {
|
|
135
|
+
const activeNote = task.startNote(`Click ${element}`);
|
|
136
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
137
|
+
const action = explorer.action();
|
|
138
|
+
const named = await describeRef(explorer, ref);
|
|
139
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(`click ${element}`)}, async ({ page }) => page.locator(${JSON.stringify(`aria-ref=${ref}`)}).click())`;
|
|
140
|
+
if (!(await action.attempt(run, `Click ${element}`))) {
|
|
141
|
+
activeNote.commit(TestResult.FAILED);
|
|
142
|
+
return failedToolResult('clickRef', `Ref ${ref} could not be clicked: ${errorText(action.lastError)}`, {
|
|
143
|
+
suggestion: 'The ref may belong to an older version of the page. Get fresh context and use the ref it gives, or fall back to click() with a locator.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// a ref belongs to this session only, so the run is reported as the locator a later test can replay
|
|
147
|
+
const code = named ? `I.click(${JSON.stringify(named)})` : run;
|
|
148
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, code);
|
|
149
|
+
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
150
|
+
return successToolResult('clickRef', { ...toolResult, code }, action);
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
124
153
|
hover: tool({
|
|
125
154
|
description: dedent `
|
|
126
155
|
Move the mouse cursor to an element to reveal hover-only controls.
|
|
@@ -245,15 +274,11 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
245
274
|
}
|
|
246
275
|
const focusFreeKeys = new Set(['Escape', 'Esc', 'Tab', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']);
|
|
247
276
|
const needsFocus = !focusFreeKeys.has(keyToUse) && !modifier;
|
|
248
|
-
if (needsFocus) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
254
|
-
suggestion: 'Click the target element first, then press the key.',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
277
|
+
if (needsFocus && !(await hasFocusedElement(explorer))) {
|
|
278
|
+
activeNote.commit(TestResult.FAILED);
|
|
279
|
+
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
280
|
+
suggestion: 'Click the target element first, then press the key.',
|
|
281
|
+
});
|
|
257
282
|
}
|
|
258
283
|
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
259
284
|
const action = explorer.action();
|
|
@@ -296,6 +321,8 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
296
321
|
Execute raw CodeceptJS code block with multiple commands.
|
|
297
322
|
USE THIS TOOL for typing text into fields: I.fillField, I.type
|
|
298
323
|
|
|
324
|
+
Do not put a click on a ref-bearing element in here — clickRef with its ref is cheaper and cannot mis-target.
|
|
325
|
+
|
|
299
326
|
Follow <actions> from system prompt for available commands.
|
|
300
327
|
Follow <locator_priority> from system prompt for locator selection.
|
|
301
328
|
|
|
@@ -456,25 +483,25 @@ export function createLearnExperienceTool({ getExperienceTracker, getState }) {
|
|
|
456
483
|
});
|
|
457
484
|
}
|
|
458
485
|
export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }) {
|
|
459
|
-
let visionDisabled = false;
|
|
460
486
|
const tools = {
|
|
461
487
|
see: tool({
|
|
462
488
|
description: dedent `
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
489
|
+
Answer a question about the page from a screenshot, for things its structure cannot express:
|
|
490
|
+
layout and position, what an image or canvas depicts, colour, and whether something is covered or cut off.
|
|
491
|
+
This runs a second model and is the slowest tool here, so reach for it only when the question is genuinely visual.
|
|
492
|
+
Do NOT use it to confirm an action landed — every action already reports what changed on the page.
|
|
466
493
|
Input schema has exactly one field: request. Do not pass text, reason, assertion, or other fields.
|
|
467
494
|
|
|
468
495
|
<example>
|
|
469
|
-
request: "
|
|
470
|
-
result: "
|
|
496
|
+
request: "Is the save button covered by anything, and does the chart show any plotted data?"
|
|
497
|
+
result: "The save button is partly behind a cookie banner at the bottom. The chart area is empty apart from its axes."
|
|
471
498
|
</example>
|
|
472
499
|
`,
|
|
473
500
|
inputSchema: z.object({
|
|
474
501
|
request: z.string().describe('LLM-friendly description of the page contents to look for. 1-3 sentences. No more than 100 words.'),
|
|
475
502
|
}),
|
|
476
503
|
execute: async ({ request }) => {
|
|
477
|
-
if (visionDisabled) {
|
|
504
|
+
if (Stats.visionDisabled) {
|
|
478
505
|
return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
|
|
479
506
|
}
|
|
480
507
|
try {
|
|
@@ -495,8 +522,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
495
522
|
catch (error) {
|
|
496
523
|
throwIfFatalBrowserError(error);
|
|
497
524
|
const errorMessage = errorText(error);
|
|
498
|
-
|
|
499
|
-
tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
|
|
525
|
+
disableVision();
|
|
500
526
|
return failedToolResult('see', `See tool failed: ${errorMessage}`, {
|
|
501
527
|
suggestion: 'Vision is now disabled. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.',
|
|
502
528
|
});
|
|
@@ -576,6 +602,12 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
576
602
|
code: result.successfulCodes.join('\n'),
|
|
577
603
|
}, { assertionSteps: result.assertionSteps });
|
|
578
604
|
}
|
|
605
|
+
if (result.inexpressible) {
|
|
606
|
+
return failedToolResult('verify', `No assertion could express this claim: ${assertion}`, {
|
|
607
|
+
inexpressible: true,
|
|
608
|
+
suggestion: 'This is not evidence the page is wrong — the claim could not be turned into an assertion. Restate it in terms of what is visible or of a control state, or check it with see().',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
579
611
|
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
580
612
|
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
581
613
|
});
|
|
@@ -696,7 +728,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
696
728
|
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
697
729
|
}),
|
|
698
730
|
execute: async ({ element, context }) => {
|
|
699
|
-
if (visionDisabled) {
|
|
731
|
+
if (Stats.visionDisabled) {
|
|
700
732
|
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
701
733
|
}
|
|
702
734
|
try {
|
|
@@ -741,8 +773,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
741
773
|
catch (error) {
|
|
742
774
|
throwIfFatalBrowserError(error);
|
|
743
775
|
const errorMessage = errorText(error);
|
|
744
|
-
|
|
745
|
-
tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
|
|
776
|
+
disableVision();
|
|
746
777
|
return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
|
|
747
778
|
suggestion: 'Vision is now disabled. Use xpathCheck() to find the element, then click() with the discovered locator.',
|
|
748
779
|
});
|
|
@@ -875,6 +906,11 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
875
906
|
},
|
|
876
907
|
}),
|
|
877
908
|
};
|
|
909
|
+
const disableVision = () => {
|
|
910
|
+
Stats.visionDisabled = true;
|
|
911
|
+
withdrawVisionTools(tools);
|
|
912
|
+
tag('warning').log('⚠️ Vision model is not available. Visual tools are disabled for this session.');
|
|
913
|
+
};
|
|
878
914
|
if (withExperience !== false) {
|
|
879
915
|
tools.learnExperience = createLearnExperienceTool({
|
|
880
916
|
getExperienceTracker: () => stateManager.getExperienceTracker(),
|
|
@@ -921,6 +957,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
921
957
|
},
|
|
922
958
|
});
|
|
923
959
|
}
|
|
960
|
+
withdrawVisionTools(tools);
|
|
924
961
|
return tools;
|
|
925
962
|
}
|
|
926
963
|
const PAGE_DIFF_SUGGESTION = 'Analyze page diff. htmlParts shows what changed and WHERE — each part has a container selector. Use the container as context when clicking elements from the diff.';
|
|
@@ -964,13 +1001,27 @@ function errorText(error) {
|
|
|
964
1001
|
return error.toString();
|
|
965
1002
|
return 'Unknown error occurred';
|
|
966
1003
|
}
|
|
967
|
-
async function commitNote(activeNote, result, toolResult, action) {
|
|
1004
|
+
export async function commitNote(activeNote, result, toolResult, action) {
|
|
968
1005
|
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
969
1006
|
activeNote.screenshot = await action.saveScreenshot();
|
|
970
1007
|
}
|
|
971
1008
|
activeNote.commit(result);
|
|
972
1009
|
}
|
|
973
|
-
function
|
|
1010
|
+
async function describeRef(explorer, ref) {
|
|
1011
|
+
return Promise.resolve(explorer?.withPage?.((page) => page.locator(`aria-ref=${ref}`).evaluate((el) => {
|
|
1012
|
+
const tag = el.tagName.toLowerCase();
|
|
1013
|
+
const roles = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
1014
|
+
const role = el.getAttribute('role') || roles[tag] || tag;
|
|
1015
|
+
const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
|
|
1016
|
+
if (!text)
|
|
1017
|
+
return null;
|
|
1018
|
+
return { role, text };
|
|
1019
|
+
}))).catch(() => null);
|
|
1020
|
+
}
|
|
1021
|
+
async function hasFocusedElement(explorer) {
|
|
1022
|
+
return explorer.withPage((page) => page.evaluate(() => !!document.activeElement && document.activeElement !== document.body)).catch(() => true);
|
|
1023
|
+
}
|
|
1024
|
+
export function successToolResult(action, data, source) {
|
|
974
1025
|
const result = { success: true, action, ...data };
|
|
975
1026
|
if (source?.playwrightGroupId) {
|
|
976
1027
|
result.playwrightGroupId = source.playwrightGroupId;
|
|
@@ -1008,7 +1059,7 @@ function hasObservablePageChange(data) {
|
|
|
1008
1059
|
return true;
|
|
1009
1060
|
return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
1010
1061
|
}
|
|
1011
|
-
async function failedToolResult(action, message, data, error) {
|
|
1062
|
+
export async function failedToolResult(action, message, data, error) {
|
|
1012
1063
|
const result = { success: false, action, message, ...data };
|
|
1013
1064
|
if (data?.pageDiff) {
|
|
1014
1065
|
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
@@ -1040,6 +1091,32 @@ function getMultipleElementsSuggestion() {
|
|
|
1040
1091
|
6. Use visualClick() to click the right element by visual appearance
|
|
1041
1092
|
`;
|
|
1042
1093
|
}
|
|
1094
|
+
export function withdrawVisionTools(tools) {
|
|
1095
|
+
if (!Stats.visionDisabled)
|
|
1096
|
+
return;
|
|
1097
|
+
Reflect.deleteProperty(tools, 'see');
|
|
1098
|
+
Reflect.deleteProperty(tools, 'visualClick');
|
|
1099
|
+
}
|
|
1100
|
+
export function clickFailureSuggestion(attempts) {
|
|
1101
|
+
const errors = attempts.map((a) => a.error || '');
|
|
1102
|
+
if (errors.some((e) => e.includes('not enabled'))) {
|
|
1103
|
+
return 'Element exists but is DISABLED — clicking it again cannot work. A precondition is unmet: a required field is empty, nothing is selected, or a dialog is blocking. Satisfy it, then retry.';
|
|
1104
|
+
}
|
|
1105
|
+
if (errors.some((e) => e.includes('intercepts pointer events'))) {
|
|
1106
|
+
return 'Element exists but another element covers it. Close the overlapping panel or dialog, then retry.';
|
|
1107
|
+
}
|
|
1108
|
+
if (errors.some((e) => e.includes('is not visible'))) {
|
|
1109
|
+
return 'Element is in the DOM but not visible. Reveal it first — scroll to it, expand its section, or open the panel holding it.';
|
|
1110
|
+
}
|
|
1111
|
+
const notFound = errors.filter((e) => e.includes('was not found'));
|
|
1112
|
+
if (notFound.length && notFound.every((e) => e.includes('was not found inside element'))) {
|
|
1113
|
+
return 'Element was not found inside that container — the container is wrong or stale, and the element may exist elsewhere on the page. Retry the same locator WITHOUT a container, or verify the container with xpathCheck().';
|
|
1114
|
+
}
|
|
1115
|
+
if (notFound.length) {
|
|
1116
|
+
return 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
1117
|
+
}
|
|
1118
|
+
return "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
|
|
1119
|
+
}
|
|
1043
1120
|
const MAX_DISAMBIGUATE_ELEMENTS = 10;
|
|
1044
1121
|
const MULTIPLE_ELEMENTS_PATTERN = 'multiple elements';
|
|
1045
1122
|
async function extractWebElements(error) {
|