explorbot 0.2.0 → 0.2.2

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