explorbot 0.1.32-beta.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/dist/src/explorer.js
CHANGED
|
@@ -8,68 +8,195 @@ import { createTest } from 'codeceptjs/lib/mocha/test';
|
|
|
8
8
|
import dedent from 'dedent';
|
|
9
9
|
import { ActionResult } from "./action-result.js";
|
|
10
10
|
import Action from './action.js';
|
|
11
|
-
import { visuallyAnnotateContainers } from "./ai/researcher/coordinates.js";
|
|
12
|
-
import { RequestStore } from "./api/request-store.js";
|
|
13
11
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
14
12
|
import { ConfigParser } from './config.js';
|
|
15
|
-
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
16
|
-
import { PlaywrightRecorder } from "./playwright-recorder.js";
|
|
17
|
-
import { Reporter } from "./reporter.js";
|
|
18
|
-
import { StateManager } from './state-manager.js';
|
|
19
13
|
import { TestResult } from "./test-plan.js";
|
|
20
|
-
import { BrowserRecoveryError, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
21
|
-
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./utils/html.js";
|
|
14
|
+
import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
22
15
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
23
|
-
import { waitForPageReadiness } from "./utils/page-readiness.js";
|
|
24
|
-
import { WebElement } from "./utils/web-element.js";
|
|
16
|
+
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
25
17
|
const debugLog = createDebug('explorbot:explorer');
|
|
26
18
|
const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
|
|
19
|
+
const RECOVERY_NAVIGATION = { waitUntil: 'domcontentloaded', timeout: 10000 };
|
|
27
20
|
class Explorer {
|
|
28
|
-
aiProvider;
|
|
29
|
-
playwrightHelper;
|
|
30
|
-
isStarted = false;
|
|
31
|
-
isSharedBrowser = false;
|
|
32
|
-
actor;
|
|
33
|
-
stateManager;
|
|
34
|
-
knowledgeTracker;
|
|
35
21
|
config;
|
|
36
|
-
userResolveFn = null;
|
|
37
22
|
options;
|
|
23
|
+
stateManager;
|
|
24
|
+
knowledgeTracker;
|
|
38
25
|
reporter;
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
requestStore;
|
|
27
|
+
playwrightRecorder;
|
|
28
|
+
playwrightHelper;
|
|
29
|
+
_actor;
|
|
30
|
+
started = false;
|
|
31
|
+
isSharedBrowser = false;
|
|
41
32
|
xhrCapture = null;
|
|
42
|
-
|
|
43
|
-
playwrightRecorder = new PlaywrightRecorder();
|
|
33
|
+
_activeTest = null;
|
|
44
34
|
observedTestPages = new Set();
|
|
45
35
|
testPageErrorHandler = null;
|
|
46
36
|
testConsoleHandler = null;
|
|
47
37
|
testDialogHandler = null;
|
|
48
|
-
constructor(config,
|
|
38
|
+
constructor(config, options, deps) {
|
|
49
39
|
this.config = config;
|
|
50
|
-
this.aiProvider = aiProvider;
|
|
51
40
|
this.options = options;
|
|
41
|
+
this.stateManager = deps.stateManager;
|
|
42
|
+
this.knowledgeTracker = deps.knowledgeTracker;
|
|
43
|
+
this.reporter = deps.reporter;
|
|
44
|
+
this.requestStore = deps.requestStore;
|
|
45
|
+
this.playwrightRecorder = deps.playwrightRecorder;
|
|
52
46
|
this.initializeContainer();
|
|
53
|
-
this.stateManager = new StateManager({ incognito: this.options?.incognito });
|
|
54
|
-
this.knowledgeTracker = new KnowledgeTracker();
|
|
55
|
-
this.reporter = new Reporter(config.reporter, this.stateManager);
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
48
|
+
get actor() {
|
|
49
|
+
return this._actor;
|
|
50
|
+
}
|
|
51
|
+
get page() {
|
|
52
|
+
const page = this.playwrightHelper?.page;
|
|
53
|
+
if (!page || page.isClosed?.())
|
|
54
|
+
return null;
|
|
55
|
+
return page;
|
|
56
|
+
}
|
|
57
|
+
get activeTest() {
|
|
58
|
+
return this._activeTest;
|
|
59
|
+
}
|
|
60
|
+
async start() {
|
|
61
|
+
if (this.started)
|
|
62
|
+
return;
|
|
63
|
+
await codeceptjs.recorder.start();
|
|
64
|
+
await codeceptjs.container.started(null);
|
|
65
|
+
storeListener();
|
|
66
|
+
stepsListener();
|
|
67
|
+
codeceptjs.recorder.retry({
|
|
68
|
+
retries: this.config.action?.retries || 3,
|
|
69
|
+
when: (err) => !!err?.message?.includes?.('context'),
|
|
70
|
+
});
|
|
71
|
+
this.playwrightHelper = codeceptjs.container.helpers('Playwright');
|
|
72
|
+
if (!this.playwrightHelper) {
|
|
73
|
+
throw new Error('Playwright helper not available');
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
await this.connectOrLaunchBrowser();
|
|
76
|
+
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
77
|
+
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
78
|
+
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
79
|
+
this.attachXhrCapture();
|
|
80
|
+
if (hasSession) {
|
|
81
|
+
tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options.session)}`);
|
|
82
|
+
}
|
|
83
|
+
this._actor = codeceptjs.container.support('I');
|
|
84
|
+
this.started = true;
|
|
85
|
+
this.listenToStateChanged();
|
|
86
|
+
codeceptjs.event.dispatcher.emit('global.before');
|
|
87
|
+
tag('success').log('Browser started, ready to explore');
|
|
88
|
+
}
|
|
89
|
+
async stop() {
|
|
90
|
+
if (!this.started)
|
|
91
|
+
return;
|
|
92
|
+
this.started = false;
|
|
93
|
+
await this.stopCaptures();
|
|
94
|
+
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
95
|
+
const dir = path.dirname(this.options.session);
|
|
96
|
+
if (!existsSync(dir))
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
await this.playwrightHelper.browserContext.storageState({ path: this.options.session });
|
|
99
|
+
debugLog(`Session saved to ${path.relative(process.cwd(), this.options.session)}`);
|
|
100
|
+
}
|
|
101
|
+
codeceptjs.event.dispatcher.emit('global.after');
|
|
102
|
+
codeceptjs.event.dispatcher.emit('global.result');
|
|
103
|
+
if (!this.isSharedBrowser) {
|
|
104
|
+
await Promise.all([this.reporter.finishRun(), this.playwrightHelper._stopBrowser(), codeceptjs.recorder.stop()]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
tag('info').log('Closing browser context (persistent browser stays running)');
|
|
108
|
+
await this.closeBrowserContext();
|
|
109
|
+
this.playwrightHelper.browser = null;
|
|
110
|
+
this.playwrightHelper.isRunning = false;
|
|
111
|
+
await Promise.all([this.reporter.finishRun(), codeceptjs.recorder.stop()]);
|
|
112
|
+
}
|
|
113
|
+
action() {
|
|
114
|
+
return new Action(this._actor, this.stateManager, this.playwrightRecorder, (fn) => this.runWithRecovery('action', fn));
|
|
115
|
+
}
|
|
116
|
+
async visit(url, opts = {}) {
|
|
117
|
+
return this.runWithRecovery('visit', async () => {
|
|
118
|
+
const action = await this.visitOnce(url);
|
|
119
|
+
if (opts.screenshot)
|
|
120
|
+
return action.capturePageState({ includeScreenshot: true });
|
|
121
|
+
return action.getActionResult() ?? action.capturePageState();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async capture(opts = {}) {
|
|
125
|
+
return this.action().capturePageState({ includeScreenshot: opts.screenshot });
|
|
126
|
+
}
|
|
127
|
+
async withPage(fn) {
|
|
128
|
+
return this.runWithRecovery('page operation', () => fn(this.playwrightHelper.page));
|
|
129
|
+
}
|
|
130
|
+
async recover(error) {
|
|
131
|
+
if (error)
|
|
132
|
+
return this.recoverFromExecutionError(error);
|
|
133
|
+
if (this.page) {
|
|
134
|
+
this.watchActiveTestPage();
|
|
135
|
+
return { ok: true, action: 'continue', message: 'Page is available' };
|
|
136
|
+
}
|
|
137
|
+
if (!(await this.recoverOrRestart())) {
|
|
138
|
+
return { ok: false, action: 'stop', message: 'Browser page could not be recovered' };
|
|
72
139
|
}
|
|
140
|
+
this.watchActiveTestPage();
|
|
141
|
+
return { ok: true, action: 'continue', recovered: true, message: 'Browser page was recovered' };
|
|
142
|
+
}
|
|
143
|
+
async beginTest(test) {
|
|
144
|
+
if (!this.page && !(await this.recoverOrRestart())) {
|
|
145
|
+
return { started: false, stop: async () => { } };
|
|
146
|
+
}
|
|
147
|
+
this._activeTest = test;
|
|
148
|
+
test.start();
|
|
149
|
+
await this.reporter.reportTestStart(test);
|
|
150
|
+
await this.closeOtherTabs();
|
|
151
|
+
this.stateManager.otherTabs = [];
|
|
152
|
+
const codeceptjsTest = toCodeceptjsTest(test);
|
|
153
|
+
const stepHandler = (step, status, error, log) => {
|
|
154
|
+
if (!step.toCode)
|
|
155
|
+
return;
|
|
156
|
+
if (step?.name?.startsWith('grab'))
|
|
157
|
+
return;
|
|
158
|
+
if (step?.name?.startsWith('save'))
|
|
159
|
+
return;
|
|
160
|
+
test.addStep(step.toCode(), step.duration, status, error, log);
|
|
161
|
+
if (!this.stateManager.getCurrentState())
|
|
162
|
+
return;
|
|
163
|
+
const lastScreenshot = ActionResult.fromState(this.stateManager.getCurrentState()).screenshotFile;
|
|
164
|
+
test.setActiveNoteScreenshot(lastScreenshot);
|
|
165
|
+
};
|
|
166
|
+
this.watchActiveTestPage();
|
|
167
|
+
const onStepPassed = (step) => stepHandler(step, 'passed');
|
|
168
|
+
const onStepFailed = (step, error) => {
|
|
169
|
+
stepHandler(step, 'failed', error?.message || String(error), error?.stack);
|
|
170
|
+
};
|
|
171
|
+
const onTestAfter = () => {
|
|
172
|
+
codeceptjs.event.dispatcher.off('step.passed', onStepPassed);
|
|
173
|
+
codeceptjs.event.dispatcher.off('step.failed', onStepFailed);
|
|
174
|
+
codeceptjs.event.dispatcher.off('test.after', onTestAfter);
|
|
175
|
+
this.unwatchActiveTestPages();
|
|
176
|
+
};
|
|
177
|
+
codeceptjs.event.dispatcher.emit('test.before', codeceptjsTest);
|
|
178
|
+
codeceptjs.event.dispatcher.emit('test.start', codeceptjsTest);
|
|
179
|
+
codeceptjs.event.dispatcher.on('step.passed', onStepPassed);
|
|
180
|
+
codeceptjs.event.dispatcher.on('step.failed', onStepFailed);
|
|
181
|
+
codeceptjs.event.dispatcher.on('test.after', onTestAfter);
|
|
182
|
+
return { started: true, stop: (meta) => this.finishTest(test, meta) };
|
|
183
|
+
}
|
|
184
|
+
async openTab() {
|
|
185
|
+
const oldPage = this.playwrightHelper?.page;
|
|
186
|
+
if (!oldPage)
|
|
187
|
+
return;
|
|
188
|
+
await this.activateNewPage(oldPage.context());
|
|
189
|
+
await oldPage.close();
|
|
190
|
+
this.stateManager.otherTabs = [];
|
|
191
|
+
debugLog('Opened fresh tab, closed previous tab');
|
|
192
|
+
}
|
|
193
|
+
initializeContainer() {
|
|
194
|
+
const configParser = ConfigParser.getInstance();
|
|
195
|
+
const projectRoot = configParser.getProjectRoot();
|
|
196
|
+
global.output_dir = configParser.getStatesDir();
|
|
197
|
+
global.codecept_dir = projectRoot;
|
|
198
|
+
configParser.validateConfig(this.config);
|
|
199
|
+
codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
|
|
73
200
|
}
|
|
74
201
|
convertToCodeceptConfig(config) {
|
|
75
202
|
const playwrightConfig = { ...config.playwright };
|
|
@@ -113,105 +240,11 @@ class Explorer {
|
|
|
113
240
|
},
|
|
114
241
|
};
|
|
115
242
|
if (this.config.stepsFile) {
|
|
116
|
-
const
|
|
117
|
-
const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
|
|
243
|
+
const projectRoot = ConfigParser.getInstance().getProjectRoot();
|
|
118
244
|
codeceptConfig.include = { I: path.resolve(projectRoot, this.config.stepsFile) };
|
|
119
245
|
}
|
|
120
246
|
return codeceptConfig;
|
|
121
247
|
}
|
|
122
|
-
getConfig() {
|
|
123
|
-
if (!this.config) {
|
|
124
|
-
throw new Error('Configuration not loaded. Call run() first.');
|
|
125
|
-
}
|
|
126
|
-
return this.config;
|
|
127
|
-
}
|
|
128
|
-
getConfigPath() {
|
|
129
|
-
const configParser = ConfigParser.getInstance();
|
|
130
|
-
return configParser.getConfigPath();
|
|
131
|
-
}
|
|
132
|
-
getAIProvider() {
|
|
133
|
-
return this.aiProvider;
|
|
134
|
-
}
|
|
135
|
-
getStateManager() {
|
|
136
|
-
return this.stateManager;
|
|
137
|
-
}
|
|
138
|
-
getCurrentUrl() {
|
|
139
|
-
return this.stateManager.getCurrentState().url || '?';
|
|
140
|
-
}
|
|
141
|
-
getKnowledgeTracker() {
|
|
142
|
-
return this.knowledgeTracker;
|
|
143
|
-
}
|
|
144
|
-
getReporter() {
|
|
145
|
-
return this.reporter;
|
|
146
|
-
}
|
|
147
|
-
getRequestStore() {
|
|
148
|
-
return this.requestStore;
|
|
149
|
-
}
|
|
150
|
-
async extractCookies() {
|
|
151
|
-
if (!this.playwrightHelper?.browserContext)
|
|
152
|
-
return {};
|
|
153
|
-
try {
|
|
154
|
-
const cookies = await this.playwrightHelper.browserContext.cookies();
|
|
155
|
-
if (!cookies.length)
|
|
156
|
-
return {};
|
|
157
|
-
const cookieString = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
158
|
-
return { Cookie: cookieString };
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return {};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
setupXhrCapture(reuseRequestStore = false) {
|
|
165
|
-
const configParser = ConfigParser.getInstance();
|
|
166
|
-
const outputDir = configParser.getOutputDir();
|
|
167
|
-
if (!reuseRequestStore || !this.requestStore) {
|
|
168
|
-
this.requestStore = new RequestStore(outputDir);
|
|
169
|
-
}
|
|
170
|
-
const baseUrl = this.config.playwright.url;
|
|
171
|
-
this.xhrCapture = new XhrCapture(this.requestStore, baseUrl);
|
|
172
|
-
this.xhrCapture.attach(this.playwrightHelper.page);
|
|
173
|
-
}
|
|
174
|
-
async start() {
|
|
175
|
-
if (this.isStarted) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (!this.config) {
|
|
179
|
-
await this.initializeContainer();
|
|
180
|
-
}
|
|
181
|
-
await codeceptjs.recorder.start();
|
|
182
|
-
await codeceptjs.container.started(null);
|
|
183
|
-
storeListener();
|
|
184
|
-
stepsListener();
|
|
185
|
-
codeceptjs.recorder.retry({
|
|
186
|
-
retries: this.config.action?.retries || 3,
|
|
187
|
-
when: (err) => {
|
|
188
|
-
if (!err || typeof err.message !== 'string') {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
// ignore context errors
|
|
192
|
-
return err.message.includes('context');
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
this.playwrightHelper = codeceptjs.container.helpers('Playwright');
|
|
196
|
-
if (!this.playwrightHelper) {
|
|
197
|
-
throw new Error('Playwright helper not available');
|
|
198
|
-
}
|
|
199
|
-
await this.connectOrLaunchBrowser();
|
|
200
|
-
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
201
|
-
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
202
|
-
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
203
|
-
this.setupXhrCapture();
|
|
204
|
-
if (hasSession) {
|
|
205
|
-
tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options.session)}`);
|
|
206
|
-
}
|
|
207
|
-
const I = codeceptjs.container.support('I');
|
|
208
|
-
this.actor = I;
|
|
209
|
-
this.isStarted = true;
|
|
210
|
-
this.listenToStateChanged();
|
|
211
|
-
codeceptjs.event.dispatcher.emit('global.before');
|
|
212
|
-
tag('success').log('Browser started, ready to explore');
|
|
213
|
-
return I;
|
|
214
|
-
}
|
|
215
248
|
async connectOrLaunchBrowser() {
|
|
216
249
|
const { getAliveEndpoint } = await import('./browser-server.js');
|
|
217
250
|
const endpoint = await getAliveEndpoint();
|
|
@@ -240,11 +273,12 @@ class Explorer {
|
|
|
240
273
|
contextOptions.storageState = this.options.session;
|
|
241
274
|
return contextOptions;
|
|
242
275
|
}
|
|
243
|
-
|
|
244
|
-
|
|
276
|
+
attachXhrCapture() {
|
|
277
|
+
this.xhrCapture = new XhrCapture(this.requestStore, this.config.playwright.url);
|
|
278
|
+
this.xhrCapture.attach(this.playwrightHelper.page);
|
|
245
279
|
}
|
|
246
|
-
async
|
|
247
|
-
if (!(await this.
|
|
280
|
+
async runWithRecovery(label, operation) {
|
|
281
|
+
if (!this.page && !(await this.recoverOrRestart())) {
|
|
248
282
|
throw new Error(`Browser page is unavailable before ${label}`);
|
|
249
283
|
}
|
|
250
284
|
try {
|
|
@@ -259,18 +293,15 @@ class Explorer {
|
|
|
259
293
|
return await operation();
|
|
260
294
|
}
|
|
261
295
|
catch (retryError) {
|
|
262
|
-
if (!isNavigationTransitionError(retryError) && !
|
|
296
|
+
if (!isNavigationTransitionError(retryError) && !isFatalBrowserError(retryError))
|
|
263
297
|
throw retryError;
|
|
264
298
|
recoveryError = retryError;
|
|
265
299
|
}
|
|
266
300
|
}
|
|
267
|
-
if (!
|
|
301
|
+
if (!isFatalBrowserError(recoveryError))
|
|
268
302
|
throw recoveryError;
|
|
269
303
|
tag('warning').log(`${label}: browser page is unavailable, recovering...`);
|
|
270
|
-
|
|
271
|
-
if (!recovered)
|
|
272
|
-
recovered = await this.restartBrowser();
|
|
273
|
-
if (!recovered)
|
|
304
|
+
if (!(await this.recoverOrRestart()))
|
|
274
305
|
throw new BrowserRecoveryError(label, recoveryError, false);
|
|
275
306
|
if (!(await this.waitForPageReadiness()))
|
|
276
307
|
throw new BrowserRecoveryError(label, recoveryError, true);
|
|
@@ -278,38 +309,20 @@ class Explorer {
|
|
|
278
309
|
return await operation();
|
|
279
310
|
}
|
|
280
311
|
catch (retryError) {
|
|
281
|
-
if (
|
|
312
|
+
if (isFatalBrowserError(retryError)) {
|
|
282
313
|
throw new BrowserRecoveryError(label, retryError, true);
|
|
283
314
|
}
|
|
284
315
|
throw retryError;
|
|
285
316
|
}
|
|
286
317
|
}
|
|
287
318
|
}
|
|
288
|
-
async capturePageState(opts = {}) {
|
|
289
|
-
return this.runWithBrowserRecovery('capturePageState', () => this.createAction().capturePageState(opts));
|
|
290
|
-
}
|
|
291
|
-
async capturePageWithScreenshot() {
|
|
292
|
-
return this.capturePageState({ includeScreenshot: true });
|
|
293
|
-
}
|
|
294
|
-
async executeAction(code) {
|
|
295
|
-
return this.runWithBrowserRecovery('executeAction', () => this.createAction().execute(code));
|
|
296
|
-
}
|
|
297
|
-
async attemptAction(code, originalMessage, experience = true) {
|
|
298
|
-
return this.runWithBrowserRecovery('attemptAction', () => this.createAction().attempt(code, originalMessage, experience));
|
|
299
|
-
}
|
|
300
|
-
getPlaywrightRecorder() {
|
|
301
|
-
return this.playwrightRecorder;
|
|
302
|
-
}
|
|
303
|
-
async visit(url) {
|
|
304
|
-
return this.runWithBrowserRecovery('visit', () => this.visitOnce(url));
|
|
305
|
-
}
|
|
306
319
|
async visitOnce(url) {
|
|
307
320
|
await this.closeOtherTabs();
|
|
308
321
|
const serializedUrl = JSON.stringify(url);
|
|
309
322
|
const currentState = this.stateManager.getCurrentState();
|
|
310
323
|
const actionResult = currentState ? ActionResult.fromState(currentState) : new ActionResult({ url });
|
|
311
324
|
const { statePush = false, wait, waitForElement, code } = this.knowledgeTracker.getStateParameters(actionResult, ['statePush', 'wait', 'waitForElement', 'code']);
|
|
312
|
-
const action = this.
|
|
325
|
+
const action = new Action(this._actor, this.stateManager, this.playwrightRecorder);
|
|
313
326
|
if (statePush) {
|
|
314
327
|
await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
|
|
315
328
|
}
|
|
@@ -318,7 +331,7 @@ class Explorer {
|
|
|
318
331
|
await action.execute(`I.amOnPage(${serializedUrl})`);
|
|
319
332
|
}
|
|
320
333
|
catch (err) {
|
|
321
|
-
const msg =
|
|
334
|
+
const msg = browserErrorMessage(err);
|
|
322
335
|
if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg))
|
|
323
336
|
throw err;
|
|
324
337
|
tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
|
|
@@ -339,119 +352,94 @@ class Explorer {
|
|
|
339
352
|
}
|
|
340
353
|
return action;
|
|
341
354
|
}
|
|
342
|
-
async
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
async visuallyAnnotateElements(opts) {
|
|
349
|
-
return this.runWithBrowserRecovery('visuallyAnnotateElements', () => visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []));
|
|
350
|
-
}
|
|
351
|
-
async getEidxInContainer(containerCss) {
|
|
352
|
-
const page = this.playwrightHelper.page;
|
|
353
|
-
try {
|
|
354
|
-
const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`;
|
|
355
|
-
const elements = await page.locator(selector).all();
|
|
356
|
-
const result = [];
|
|
357
|
-
for (const el of elements) {
|
|
358
|
-
const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
359
|
-
if (attr)
|
|
360
|
-
result.push(attr);
|
|
361
|
-
}
|
|
362
|
-
return result;
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
if (this.isFatalBrowserError(error)) {
|
|
366
|
-
tag('warning').log(`getEidxInContainer: ${error instanceof Error ? error.message : error}`);
|
|
367
|
-
await this.recoverFromBrowserError();
|
|
368
|
-
}
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
async getEidxByLocator(locator, container) {
|
|
373
|
-
try {
|
|
374
|
-
const page = this.playwrightHelper.page;
|
|
375
|
-
const base = container ? page.locator(container) : page;
|
|
376
|
-
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
377
|
-
return await el.first().getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
if (this.isFatalBrowserError(error)) {
|
|
381
|
-
tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
|
|
382
|
-
await this.recoverFromBrowserError();
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
355
|
+
async recoverFromExecutionError(error) {
|
|
356
|
+
const message = browserErrorMessage(error);
|
|
357
|
+
tag('error').log(`Browser execution error: ${message}`);
|
|
358
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
359
|
+
return { ok: false, action: 'stop', message };
|
|
385
360
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await this.closeOtherTabs();
|
|
389
|
-
await this.playwrightHelper.page.reload();
|
|
390
|
-
}
|
|
391
|
-
resolveBrowserUrl(url) {
|
|
392
|
-
if (!url)
|
|
393
|
-
return null;
|
|
394
|
-
try {
|
|
395
|
-
return new URL(url).toString();
|
|
361
|
+
if (error instanceof BrowserRecoveryError) {
|
|
362
|
+
return { ok: false, action: 'stop', recovered: error.recovered, message: error.message };
|
|
396
363
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
364
|
+
if (!isFatalBrowserError(error)) {
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
action: 'continue',
|
|
368
|
+
message: `Previous execution error: ${message}. Investigate the current state and choose a different approach.`,
|
|
369
|
+
};
|
|
403
370
|
}
|
|
404
|
-
|
|
405
|
-
return
|
|
371
|
+
if (!(await this.recoverOrRestart())) {
|
|
372
|
+
return { ok: false, action: 'stop', recovered: false, message: `Browser could not be recovered after fatal error: ${message}` };
|
|
406
373
|
}
|
|
374
|
+
this.watchActiveTestPage();
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
action: 'continue',
|
|
378
|
+
recovered: true,
|
|
379
|
+
message: dedent `
|
|
380
|
+
Browser was recovered after a fatal page error.
|
|
381
|
+
Continue from the restored page.
|
|
382
|
+
The interrupted browser action is not product evidence.
|
|
383
|
+
Inspect the restored page and retry the current step when it is still required.
|
|
384
|
+
`,
|
|
385
|
+
};
|
|
407
386
|
}
|
|
408
|
-
|
|
409
|
-
|
|
387
|
+
async recoverOrRestart() {
|
|
388
|
+
if (await this.recoverPage())
|
|
389
|
+
return true;
|
|
390
|
+
return this.restartBrowser();
|
|
410
391
|
}
|
|
411
|
-
async
|
|
392
|
+
async recoverPage() {
|
|
412
393
|
try {
|
|
413
|
-
if (!this.
|
|
394
|
+
if (!this.page) {
|
|
414
395
|
const context = this.playwrightHelper?.browserContext;
|
|
415
396
|
if (!context)
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
await page.bringToFront();
|
|
419
|
-
await this.playwrightHelper._setPage(page);
|
|
420
|
-
this.bindFrameNavigated(page);
|
|
421
|
-
if (this.xhrCapture) {
|
|
422
|
-
this.xhrCapture.attach(this.playwrightHelper.page);
|
|
423
|
-
}
|
|
397
|
+
return false;
|
|
398
|
+
await this.activateNewPage(context);
|
|
424
399
|
}
|
|
425
400
|
const url = this.resolveBrowserUrl(this.stateManager.getCurrentState()?.url);
|
|
426
401
|
if (url) {
|
|
427
402
|
tag('warning').log(`Browser error detected, recovering by navigating to ${url}`);
|
|
428
|
-
await this.playwrightHelper.page.goto(url,
|
|
403
|
+
await this.playwrightHelper.page.goto(url, RECOVERY_NAVIGATION);
|
|
429
404
|
return this.waitForPageReadiness();
|
|
430
405
|
}
|
|
431
406
|
tag('warning').log('Browser error detected, reloading page');
|
|
432
|
-
await this.playwrightHelper.page.reload(
|
|
407
|
+
await this.playwrightHelper.page.reload(RECOVERY_NAVIGATION);
|
|
433
408
|
return this.waitForPageReadiness();
|
|
434
409
|
}
|
|
435
410
|
catch (err) {
|
|
436
|
-
tag('error').log(`Browser recovery failed: ${err
|
|
411
|
+
tag('error').log(`Browser recovery failed: ${browserErrorMessage(err)}`);
|
|
437
412
|
return false;
|
|
438
413
|
}
|
|
439
414
|
}
|
|
415
|
+
async activateNewPage(context) {
|
|
416
|
+
const page = await context.newPage();
|
|
417
|
+
await page.bringToFront();
|
|
418
|
+
await this.playwrightHelper._setPage(page);
|
|
419
|
+
this.bindFrameNavigated(page);
|
|
420
|
+
this.xhrCapture?.attach(page);
|
|
421
|
+
}
|
|
422
|
+
async stopCaptures() {
|
|
423
|
+
if (this.xhrCapture && this.playwrightHelper?.page) {
|
|
424
|
+
this.xhrCapture.detach(this.playwrightHelper.page);
|
|
425
|
+
}
|
|
426
|
+
await this.playwrightRecorder.stop();
|
|
427
|
+
}
|
|
428
|
+
async closeBrowserContext() {
|
|
429
|
+
if (!this.playwrightHelper.browserContext)
|
|
430
|
+
return;
|
|
431
|
+
await this.playwrightHelper.browserContext.close().catch((err) => {
|
|
432
|
+
debugLog('Failed to close browser context:', err);
|
|
433
|
+
});
|
|
434
|
+
this.playwrightHelper.browserContext = null;
|
|
435
|
+
}
|
|
440
436
|
async restartBrowser() {
|
|
441
437
|
if (!this.playwrightHelper)
|
|
442
438
|
return false;
|
|
443
439
|
const url = this.resolveBrowserUrl(this.stateManager.getCurrentState()?.url);
|
|
444
440
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
await this.playwrightRecorder.stop();
|
|
449
|
-
if (this.playwrightHelper.browserContext) {
|
|
450
|
-
await this.playwrightHelper.browserContext.close().catch((err) => {
|
|
451
|
-
debugLog('Failed to close browser context before restart:', err);
|
|
452
|
-
});
|
|
453
|
-
this.playwrightHelper.browserContext = null;
|
|
454
|
-
}
|
|
441
|
+
await this.stopCaptures();
|
|
442
|
+
await this.closeBrowserContext();
|
|
455
443
|
if (!this.isSharedBrowser) {
|
|
456
444
|
await this.playwrightHelper._stopBrowser().catch((err) => {
|
|
457
445
|
debugLog('Failed to stop browser before restart:', err);
|
|
@@ -460,10 +448,10 @@ class Explorer {
|
|
|
460
448
|
await this.connectOrLaunchBrowser();
|
|
461
449
|
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
462
450
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
463
|
-
this.
|
|
451
|
+
this.attachXhrCapture();
|
|
464
452
|
this.listenToStateChanged();
|
|
465
453
|
if (url) {
|
|
466
|
-
await this.playwrightHelper.page.goto(url,
|
|
454
|
+
await this.playwrightHelper.page.goto(url, RECOVERY_NAVIGATION);
|
|
467
455
|
if (!(await this.waitForPageReadiness()))
|
|
468
456
|
return false;
|
|
469
457
|
}
|
|
@@ -471,14 +459,25 @@ class Explorer {
|
|
|
471
459
|
return true;
|
|
472
460
|
}
|
|
473
461
|
catch (err) {
|
|
474
|
-
tag('error').log(`Browser restart failed: ${err
|
|
462
|
+
tag('error').log(`Browser restart failed: ${browserErrorMessage(err)}`);
|
|
475
463
|
return false;
|
|
476
464
|
}
|
|
477
465
|
}
|
|
478
|
-
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
466
|
+
resolveBrowserUrl(url) {
|
|
467
|
+
if (!url)
|
|
468
|
+
return null;
|
|
469
|
+
try {
|
|
470
|
+
return new URL(url).toString();
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
const baseUrl = this.config.playwright?.url || this.config.web?.url;
|
|
474
|
+
if (!baseUrl)
|
|
475
|
+
return null;
|
|
476
|
+
try {
|
|
477
|
+
return new URL(url, baseUrl).toString();
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return null;
|
|
482
481
|
}
|
|
483
482
|
}
|
|
484
483
|
async waitForPageReadiness() {
|
|
@@ -491,219 +490,96 @@ class Explorer {
|
|
|
491
490
|
});
|
|
492
491
|
return true;
|
|
493
492
|
}
|
|
494
|
-
async isInsideIframe() {
|
|
495
|
-
if (this.playwrightHelper.frame)
|
|
496
|
-
return true;
|
|
497
|
-
try {
|
|
498
|
-
const page = this.playwrightHelper.page;
|
|
499
|
-
if (!page)
|
|
500
|
-
return false;
|
|
501
|
-
return await page.evaluate(() => window.top !== window.self);
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
if (this.isFatalBrowserError(error)) {
|
|
505
|
-
tag('warning').log(`isInsideIframe: ${error instanceof Error ? error.message : error}`);
|
|
506
|
-
await this.recoverFromBrowserError();
|
|
507
|
-
}
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
getCurrentIframeInfo() {
|
|
512
|
-
if (!this.playwrightHelper?.frame)
|
|
513
|
-
return null;
|
|
514
|
-
return 'iframe context active';
|
|
515
|
-
}
|
|
516
|
-
hasOtherTabs() {
|
|
517
|
-
return this.otherTabs.length > 0;
|
|
518
|
-
}
|
|
519
|
-
getOtherTabsInfo() {
|
|
520
|
-
return [...this.otherTabs];
|
|
521
|
-
}
|
|
522
|
-
clearOtherTabsInfo() {
|
|
523
|
-
this.otherTabs = [];
|
|
524
|
-
}
|
|
525
|
-
setUserResolve(userResolveFn) {
|
|
526
|
-
this.userResolveFn = userResolveFn;
|
|
527
|
-
}
|
|
528
493
|
listenToStateChanged() {
|
|
529
|
-
|
|
530
|
-
|
|
494
|
+
const page = this.playwrightHelper?.page;
|
|
495
|
+
if (!page) {
|
|
496
|
+
debugLog('Playwright page not available for state monitoring');
|
|
531
497
|
return;
|
|
532
498
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
499
|
+
const initialPage = page;
|
|
500
|
+
const context = page.context();
|
|
501
|
+
context.on('page', async (newPage) => {
|
|
502
|
+
if (newPage === initialPage)
|
|
537
503
|
return;
|
|
538
|
-
}
|
|
539
|
-
const initialPage = page;
|
|
540
|
-
const context = this.playwrightHelper.page.context();
|
|
541
|
-
context.on('page', async (newPage) => {
|
|
542
|
-
if (newPage === initialPage) {
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
try {
|
|
546
|
-
if (newPage.url() === 'about:blank') {
|
|
547
|
-
await newPage.waitForURL(/^(?!about:blank$)/, { timeout: 5000 }).catch(() => { });
|
|
548
|
-
}
|
|
549
|
-
await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
550
|
-
const url = await newPage.url();
|
|
551
|
-
const title = await newPage.title().catch(() => 'Unknown');
|
|
552
|
-
this.otherTabs.push({ url, title });
|
|
553
|
-
if (url !== 'about:blank') {
|
|
554
|
-
tag('info').log(`New browser tab opened: ${url}`);
|
|
555
|
-
}
|
|
556
|
-
debugLog(`New tab detected: ${url} - ${title}`);
|
|
557
|
-
}
|
|
558
|
-
catch (error) {
|
|
559
|
-
debugLog('Failed to get new tab info:', error);
|
|
560
|
-
this.otherTabs.push({ url: 'unknown', title: 'unknown' });
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
this.bindFrameNavigated(page);
|
|
564
|
-
debugLog('Listening for automatic state changes');
|
|
565
|
-
}
|
|
566
|
-
catch (error) {
|
|
567
|
-
debugLog('Failed to set up state change monitoring:', error);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
async stop() {
|
|
571
|
-
if (!this.isStarted) {
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (this.xhrCapture && this.playwrightHelper?.page) {
|
|
575
|
-
this.xhrCapture.detach(this.playwrightHelper.page);
|
|
576
|
-
}
|
|
577
|
-
await this.playwrightRecorder.stop();
|
|
578
|
-
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
579
|
-
const dir = path.dirname(this.options.session);
|
|
580
|
-
if (!existsSync(dir))
|
|
581
|
-
mkdirSync(dir, { recursive: true });
|
|
582
|
-
await this.playwrightHelper.browserContext.storageState({ path: this.options.session });
|
|
583
|
-
debugLog(`Session saved to ${path.relative(process.cwd(), this.options.session)}`);
|
|
584
|
-
}
|
|
585
|
-
codeceptjs.event.dispatcher.emit('global.after');
|
|
586
|
-
codeceptjs.event.dispatcher.emit('global.result');
|
|
587
|
-
if (this.isSharedBrowser) {
|
|
588
|
-
tag('info').log('Closing browser context (persistent browser stays running)');
|
|
589
504
|
try {
|
|
590
|
-
if (
|
|
591
|
-
await
|
|
592
|
-
this.playwrightHelper.browserContext = null;
|
|
505
|
+
if (newPage.url() === 'about:blank') {
|
|
506
|
+
await newPage.waitForURL(/^(?!about:blank$)/, { timeout: 5000 }).catch(() => { });
|
|
593
507
|
}
|
|
508
|
+
await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
509
|
+
const url = await newPage.url();
|
|
510
|
+
const title = await newPage.title().catch(() => 'Unknown');
|
|
511
|
+
this.stateManager.otherTabs.push({ url, title });
|
|
512
|
+
if (url !== 'about:blank') {
|
|
513
|
+
tag('info').log(`New browser tab opened: ${url}`);
|
|
514
|
+
}
|
|
515
|
+
debugLog(`New tab detected: ${url} - ${title}`);
|
|
594
516
|
}
|
|
595
|
-
catch (
|
|
596
|
-
debugLog('Failed to
|
|
517
|
+
catch (error) {
|
|
518
|
+
debugLog('Failed to get new tab info:', error);
|
|
519
|
+
this.stateManager.otherTabs.push({ url: 'unknown', title: 'unknown' });
|
|
597
520
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
await Promise.all([this.reporter.finishRun(), this.playwrightHelper._stopBrowser(), codeceptjs.recorder.stop()]);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
get activeTest() {
|
|
607
|
-
return this._activeTest;
|
|
521
|
+
});
|
|
522
|
+
this.bindFrameNavigated(page);
|
|
523
|
+
debugLog('Listening for automatic state changes');
|
|
608
524
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
await this.reporter.reportTestStart(test);
|
|
613
|
-
await this.closeOtherTabs();
|
|
614
|
-
this.otherTabs = [];
|
|
615
|
-
if (!(await this.ensurePageAvailable()))
|
|
616
|
-
return false;
|
|
617
|
-
const codeceptjsTest = toCodeceptjsTest(test);
|
|
618
|
-
const stepHandler = (step, status, error, log) => {
|
|
619
|
-
if (!step.toCode)
|
|
620
|
-
return;
|
|
621
|
-
if (step?.name?.startsWith('grab'))
|
|
622
|
-
return;
|
|
623
|
-
if (step?.name?.startsWith('save'))
|
|
624
|
-
return;
|
|
625
|
-
test.addStep(step.toCode(), step.duration, status, error, log);
|
|
626
|
-
if (!this.stateManager.getCurrentState())
|
|
525
|
+
bindFrameNavigated(page) {
|
|
526
|
+
page.on('framenavigated', async (frame) => {
|
|
527
|
+
if (frame !== page.mainFrame())
|
|
627
528
|
return;
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
codeceptjs.event.dispatcher.off('test.after', onTestAfter);
|
|
640
|
-
this.unwatchActiveTestPages();
|
|
641
|
-
};
|
|
642
|
-
codeceptjs.event.dispatcher.emit('test.before', codeceptjsTest);
|
|
643
|
-
codeceptjs.event.dispatcher.emit('test.start', codeceptjsTest);
|
|
644
|
-
codeceptjs.event.dispatcher.on('step.passed', onStepPassed);
|
|
645
|
-
codeceptjs.event.dispatcher.on('step.failed', onStepFailed);
|
|
646
|
-
codeceptjs.event.dispatcher.on('test.after', onTestAfter);
|
|
647
|
-
return true;
|
|
529
|
+
const newUrl = await frame.url();
|
|
530
|
+
let newTitle = '';
|
|
531
|
+
try {
|
|
532
|
+
newTitle = await frame.title();
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
debugLog('Failed to get page title:', error);
|
|
536
|
+
}
|
|
537
|
+
this.stateManager.updateStateFromBasic(newUrl, newTitle, 'navigation');
|
|
538
|
+
await sleep(500);
|
|
539
|
+
});
|
|
648
540
|
}
|
|
649
|
-
async
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
541
|
+
async closeOtherTabs() {
|
|
542
|
+
if (!this.playwrightHelper)
|
|
543
|
+
return;
|
|
544
|
+
const context = this.playwrightHelper.page.context();
|
|
545
|
+
const pages = context.pages();
|
|
546
|
+
if (pages.length <= 1)
|
|
547
|
+
return;
|
|
548
|
+
debugLog(`Found ${pages.length} tabs, cleaning up to keep only the first one`);
|
|
549
|
+
const firstPage = pages[0];
|
|
550
|
+
const tabsToClose = pages.slice(1);
|
|
551
|
+
for (const page of tabsToClose) {
|
|
552
|
+
await page.close();
|
|
553
|
+
debugLog(`Closed extra tab: ${await page.url()}`);
|
|
654
554
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
this.watchActiveTestPage();
|
|
659
|
-
return true;
|
|
660
|
-
}
|
|
661
|
-
async ensureActiveTestPageAvailable() {
|
|
662
|
-
return this.ensurePageAvailable();
|
|
555
|
+
await firstPage.bringToFront();
|
|
556
|
+
await this.playwrightHelper._setPage(firstPage);
|
|
557
|
+
debugLog(`Cleaned up tabs, now focused on: ${await firstPage.url()}`);
|
|
663
558
|
}
|
|
664
|
-
async
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
};
|
|
559
|
+
async finishTest(test, meta) {
|
|
560
|
+
this.unwatchActiveTestPages();
|
|
561
|
+
this._activeTest = null;
|
|
562
|
+
const lastScreenshot = this.stateManager.getCurrentState()?.screenshotFile;
|
|
563
|
+
if (lastScreenshot) {
|
|
564
|
+
meta ||= {};
|
|
565
|
+
meta.screenshotFile = lastScreenshot;
|
|
672
566
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
};
|
|
567
|
+
await this.reporter.reportTest(test, meta);
|
|
568
|
+
const codeceptjsTest = toCodeceptjsTest(test);
|
|
569
|
+
if (test.isSuccessful) {
|
|
570
|
+
codeceptjsTest.state = 'passed';
|
|
571
|
+
codeceptjs.event.dispatcher.emit('test.passed', codeceptjsTest);
|
|
679
572
|
}
|
|
680
|
-
if (
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
message: `Previous execution error: ${message}. Investigate the current state and choose a different approach.`,
|
|
684
|
-
};
|
|
573
|
+
else if (test.isSkipped) {
|
|
574
|
+
codeceptjsTest.state = 'skipped';
|
|
575
|
+
codeceptjs.event.dispatcher.emit('test.skipped', codeceptjsTest);
|
|
685
576
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (!recovered) {
|
|
690
|
-
return {
|
|
691
|
-
action: 'stop',
|
|
692
|
-
recovered: false,
|
|
693
|
-
message: `Browser could not be recovered after fatal error: ${message}`,
|
|
694
|
-
};
|
|
577
|
+
else {
|
|
578
|
+
codeceptjsTest.state = 'failed';
|
|
579
|
+
codeceptjs.event.dispatcher.emit('test.failed', codeceptjsTest);
|
|
695
580
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
action: 'continue',
|
|
699
|
-
recovered: true,
|
|
700
|
-
message: dedent `
|
|
701
|
-
Browser was recovered after a fatal page error.
|
|
702
|
-
Continue from the restored page.
|
|
703
|
-
The interrupted browser action is not product evidence.
|
|
704
|
-
Inspect the restored page and retry the current step when it is still required.
|
|
705
|
-
`,
|
|
706
|
-
};
|
|
581
|
+
codeceptjs.event.dispatcher.emit('test.finish', codeceptjsTest);
|
|
582
|
+
codeceptjs.event.dispatcher.emit('test.after', codeceptjsTest);
|
|
707
583
|
}
|
|
708
584
|
watchActiveTestPage(page = this.playwrightHelper?.page) {
|
|
709
585
|
if (!this._activeTest)
|
|
@@ -730,31 +606,6 @@ class Explorer {
|
|
|
730
606
|
page.on('dialog', this.testDialogHandler);
|
|
731
607
|
this.observedTestPages.add(page);
|
|
732
608
|
}
|
|
733
|
-
async stopTest(test, meta) {
|
|
734
|
-
this.unwatchActiveTestPages();
|
|
735
|
-
this._activeTest = null;
|
|
736
|
-
const lastScreenshot = this.stateManager.getCurrentState()?.screenshotFile;
|
|
737
|
-
if (lastScreenshot) {
|
|
738
|
-
meta ||= {};
|
|
739
|
-
meta.screenshotFile = lastScreenshot;
|
|
740
|
-
}
|
|
741
|
-
await this.reporter.reportTest(test, meta);
|
|
742
|
-
const codeceptjsTest = toCodeceptjsTest(test);
|
|
743
|
-
if (test.isSuccessful) {
|
|
744
|
-
codeceptjsTest.state = 'passed';
|
|
745
|
-
codeceptjs.event.dispatcher.emit('test.passed', codeceptjsTest);
|
|
746
|
-
}
|
|
747
|
-
else if (test.isSkipped) {
|
|
748
|
-
codeceptjsTest.state = 'skipped';
|
|
749
|
-
codeceptjs.event.dispatcher.emit('test.skipped', codeceptjsTest);
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
codeceptjsTest.state = 'failed';
|
|
753
|
-
codeceptjs.event.dispatcher.emit('test.failed', codeceptjsTest);
|
|
754
|
-
}
|
|
755
|
-
codeceptjs.event.dispatcher.emit('test.finish', codeceptjsTest);
|
|
756
|
-
codeceptjs.event.dispatcher.emit('test.after', codeceptjsTest);
|
|
757
|
-
}
|
|
758
609
|
unwatchActiveTestPages() {
|
|
759
610
|
for (const page of this.observedTestPages) {
|
|
760
611
|
if (this.testPageErrorHandler)
|
|
@@ -766,94 +617,6 @@ class Explorer {
|
|
|
766
617
|
}
|
|
767
618
|
this.observedTestPages.clear();
|
|
768
619
|
}
|
|
769
|
-
async hasPlaywrightLocator(locatorFn, opts = {}) {
|
|
770
|
-
try {
|
|
771
|
-
const pwLocator = locatorFn(this.playwrightHelper.page);
|
|
772
|
-
const count = await pwLocator.count();
|
|
773
|
-
if (opts.multiple ? count === 0 : count !== 1)
|
|
774
|
-
return false;
|
|
775
|
-
if (opts.contents) {
|
|
776
|
-
const html = await pwLocator.first().innerHTML();
|
|
777
|
-
if (!html?.trim())
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
if (opts.success)
|
|
781
|
-
await opts.success(pwLocator);
|
|
782
|
-
return true;
|
|
783
|
-
}
|
|
784
|
-
catch (error) {
|
|
785
|
-
if (this.isFatalBrowserError(error)) {
|
|
786
|
-
tag('warning').log(`hasPlaywrightLocator: ${error instanceof Error ? error.message : error}`);
|
|
787
|
-
await this.recoverFromBrowserError();
|
|
788
|
-
}
|
|
789
|
-
return false;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
async playwrightLocatorCount(locatorFn) {
|
|
793
|
-
try {
|
|
794
|
-
const pwLocator = locatorFn(this.playwrightHelper.page);
|
|
795
|
-
return await pwLocator.count();
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
if (this.isFatalBrowserError(error)) {
|
|
799
|
-
tag('warning').log(`playwrightLocatorCount: ${error instanceof Error ? error.message : error}`);
|
|
800
|
-
await this.recoverFromBrowserError();
|
|
801
|
-
}
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
bindFrameNavigated(page) {
|
|
806
|
-
page.on('framenavigated', async (frame) => {
|
|
807
|
-
if (frame !== page.mainFrame())
|
|
808
|
-
return;
|
|
809
|
-
const newUrl = await frame.url();
|
|
810
|
-
let newTitle = '';
|
|
811
|
-
try {
|
|
812
|
-
newTitle = await frame.title();
|
|
813
|
-
}
|
|
814
|
-
catch (error) {
|
|
815
|
-
debugLog('Failed to get page title:', error);
|
|
816
|
-
}
|
|
817
|
-
this.stateManager.updateStateFromBasic(newUrl, newTitle, 'navigation');
|
|
818
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
async openFreshTab() {
|
|
822
|
-
if (!this.playwrightHelper?.page)
|
|
823
|
-
return;
|
|
824
|
-
const oldPage = this.playwrightHelper.page;
|
|
825
|
-
const context = oldPage.context();
|
|
826
|
-
const newPage = await context.newPage();
|
|
827
|
-
await oldPage.close();
|
|
828
|
-
await newPage.bringToFront();
|
|
829
|
-
await this.playwrightHelper._setPage(newPage);
|
|
830
|
-
this.otherTabs = [];
|
|
831
|
-
this.bindFrameNavigated(newPage);
|
|
832
|
-
if (this.xhrCapture) {
|
|
833
|
-
this.xhrCapture.attach(newPage);
|
|
834
|
-
}
|
|
835
|
-
debugLog('Opened fresh tab, closed previous tab');
|
|
836
|
-
}
|
|
837
|
-
async closeOtherTabs() {
|
|
838
|
-
if (!this.playwrightHelper) {
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
const context = this.playwrightHelper.page.context();
|
|
842
|
-
const pages = context.pages();
|
|
843
|
-
if (pages.length <= 1) {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
debugLog(`Found ${pages.length} tabs, cleaning up to keep only the first one`);
|
|
847
|
-
const firstPage = pages[0];
|
|
848
|
-
const tabsToClose = pages.slice(1);
|
|
849
|
-
for (const page of tabsToClose) {
|
|
850
|
-
await page.close();
|
|
851
|
-
debugLog(`Closed extra tab: ${await page.url()}`);
|
|
852
|
-
}
|
|
853
|
-
await firstPage.bringToFront();
|
|
854
|
-
await this.playwrightHelper._setPage(firstPage);
|
|
855
|
-
debugLog(`Cleaned up tabs, now focused on: ${await firstPage.url()}`);
|
|
856
|
-
}
|
|
857
620
|
}
|
|
858
621
|
function toCodeceptjsTest(test) {
|
|
859
622
|
const parent = {
|
|
@@ -868,58 +631,4 @@ function toCodeceptjsTest(test) {
|
|
|
868
631
|
codeceptjsTest._explorbotTest = test;
|
|
869
632
|
return codeceptjsTest;
|
|
870
633
|
}
|
|
871
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
872
|
-
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
873
|
-
function parseAriaRefs(ariaSnapshot) {
|
|
874
|
-
const entries = [];
|
|
875
|
-
for (const line of ariaSnapshot.split('\n')) {
|
|
876
|
-
const match = line.match(REF_LINE_PATTERN);
|
|
877
|
-
if (!match)
|
|
878
|
-
continue;
|
|
879
|
-
if (!ANNOTATABLE_ROLES.has(match[2]))
|
|
880
|
-
continue;
|
|
881
|
-
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
882
|
-
}
|
|
883
|
-
return entries;
|
|
884
|
-
}
|
|
885
|
-
export async function annotatePageElements(page) {
|
|
886
|
-
const ariaSnapshot = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
887
|
-
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
888
|
-
const byRole = new Map();
|
|
889
|
-
for (const { role, name, ref } of refEntries) {
|
|
890
|
-
let list = byRole.get(role);
|
|
891
|
-
if (!list) {
|
|
892
|
-
list = [];
|
|
893
|
-
byRole.set(role, list);
|
|
894
|
-
}
|
|
895
|
-
list.push({ name, ref });
|
|
896
|
-
}
|
|
897
|
-
const elements = [];
|
|
898
|
-
for (const [role, entries] of byRole) {
|
|
899
|
-
try {
|
|
900
|
-
const rawList = await page.getByRole(role).evaluateAll((domElements, [data, extractFnStr, config]) => {
|
|
901
|
-
const extract = new Function(`return ${extractFnStr}`)();
|
|
902
|
-
const results = [];
|
|
903
|
-
let ariaIdx = 0;
|
|
904
|
-
for (const el of domElements) {
|
|
905
|
-
if (ariaIdx >= data.length)
|
|
906
|
-
break;
|
|
907
|
-
el.setAttribute(config.attrs.eidx, data[ariaIdx].ref);
|
|
908
|
-
const elData = extract(el, config);
|
|
909
|
-
if (elData)
|
|
910
|
-
results.push(elData);
|
|
911
|
-
ariaIdx++;
|
|
912
|
-
}
|
|
913
|
-
return results;
|
|
914
|
-
}, [entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
915
|
-
for (const raw of rawList) {
|
|
916
|
-
elements.push(WebElement.fromRawData(raw, role));
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
debugLog(`Failed to annotate role=${role}`);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
return { ariaSnapshot, elements };
|
|
924
|
-
}
|
|
925
634
|
export default Explorer;
|