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.
Files changed (222) hide show
  1. package/README.md +35 -19
  2. package/bin/explorbot-cli.ts +33 -10
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +26 -28
  5. package/boat/doc-collector/src/ai/tools.ts +74 -16
  6. package/boat/doc-collector/src/cli.ts +1 -6
  7. package/boat/doc-collector/src/docbot.ts +30 -9
  8. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  9. package/boat/doc-collector/src/screenshots.ts +34 -2
  10. package/boat/doc-collector/src/state-diagram.ts +281 -0
  11. package/dist/bin/explorbot-cli.js +29 -9
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
  14. package/dist/boat/doc-collector/src/ai/tools.js +53 -16
  15. package/dist/boat/doc-collector/src/cli.js +1 -5
  16. package/dist/boat/doc-collector/src/docbot.js +27 -8
  17. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  18. package/dist/boat/doc-collector/src/screenshots.js +30 -1
  19. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +23 -3
  22. package/dist/src/action-result.d.ts +8 -10
  23. package/dist/src/action-result.js +72 -164
  24. package/dist/src/action.d.ts +13 -12
  25. package/dist/src/action.js +37 -113
  26. package/dist/src/ai/agent.d.ts +17 -0
  27. package/dist/src/ai/captain/idle-mode.js +1 -1
  28. package/dist/src/ai/captain/test-mode.js +1 -1
  29. package/dist/src/ai/captain/web-mode.js +18 -29
  30. package/dist/src/ai/captain.d.ts +1 -3
  31. package/dist/src/ai/captain.js +17 -45
  32. package/dist/src/ai/conversation.d.ts +2 -5
  33. package/dist/src/ai/conversation.js +9 -28
  34. package/dist/src/ai/driller.d.ts +2 -17
  35. package/dist/src/ai/driller.js +26 -50
  36. package/dist/src/ai/experience-compactor.d.ts +2 -2
  37. package/dist/src/ai/experience-compactor.js +16 -33
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +0 -1
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  42. package/dist/src/ai/historian/codeceptjs.js +4 -3
  43. package/dist/src/ai/historian/playwright.d.ts +4 -1
  44. package/dist/src/ai/historian/playwright.js +4 -3
  45. package/dist/src/ai/historian/screencast.d.ts +2 -1
  46. package/dist/src/ai/historian/screencast.js +2 -2
  47. package/dist/src/ai/historian/utils.d.ts +0 -1
  48. package/dist/src/ai/historian/utils.js +0 -1
  49. package/dist/src/ai/historian.d.ts +10 -8
  50. package/dist/src/ai/historian.js +1 -5
  51. package/dist/src/ai/navigator.d.ts +8 -6
  52. package/dist/src/ai/navigator.js +34 -65
  53. package/dist/src/ai/pilot.d.ts +13 -8
  54. package/dist/src/ai/pilot.js +53 -59
  55. package/dist/src/ai/planner.d.ts +5 -4
  56. package/dist/src/ai/planner.js +66 -52
  57. package/dist/src/ai/provider.d.ts +4 -1
  58. package/dist/src/ai/provider.js +77 -137
  59. package/dist/src/ai/quartermaster.d.ts +22 -22
  60. package/dist/src/ai/quartermaster.js +8 -14
  61. package/dist/src/ai/rerunner.d.ts +3 -13
  62. package/dist/src/ai/rerunner.js +23 -44
  63. package/dist/src/ai/researcher/cache.js +11 -18
  64. package/dist/src/ai/researcher/coordinates.js +4 -3
  65. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  66. package/dist/src/ai/researcher/deep-analysis.js +8 -11
  67. package/dist/src/ai/researcher/locators.d.ts +9 -2
  68. package/dist/src/ai/researcher/locators.js +65 -8
  69. package/dist/src/ai/researcher/parser.js +1 -1
  70. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  71. package/dist/src/ai/researcher/research-result.js +0 -19
  72. package/dist/src/ai/researcher/sections.d.ts +2 -0
  73. package/dist/src/ai/researcher/sections.js +7 -3
  74. package/dist/src/ai/researcher.js +40 -106
  75. package/dist/src/ai/rules.d.ts +1 -0
  76. package/dist/src/ai/rules.js +22 -15
  77. package/dist/src/ai/session-analyst.js +8 -5
  78. package/dist/src/ai/task-agent.d.ts +19 -6
  79. package/dist/src/ai/task-agent.js +37 -33
  80. package/dist/src/ai/tester.d.ts +6 -14
  81. package/dist/src/ai/tester.js +46 -111
  82. package/dist/src/ai/tools.d.ts +26 -14
  83. package/dist/src/ai/tools.js +138 -132
  84. package/dist/src/command-handler.js +1 -1
  85. package/dist/src/commands/compact-command.d.ts +1 -1
  86. package/dist/src/commands/compact-command.js +6 -6
  87. package/dist/src/commands/context-aria-command.js +1 -1
  88. package/dist/src/commands/context-command.js +8 -6
  89. package/dist/src/commands/context-data-command.js +2 -2
  90. package/dist/src/commands/context-experience-command.js +2 -2
  91. package/dist/src/commands/context-html-command.js +2 -2
  92. package/dist/src/commands/context-knowledge-command.js +2 -2
  93. package/dist/src/commands/drill-command.js +1 -1
  94. package/dist/src/commands/experience-command.js +1 -1
  95. package/dist/src/commands/explore-command.js +4 -3
  96. package/dist/src/commands/freesail-command.js +2 -2
  97. package/dist/src/commands/knows-command.js +1 -1
  98. package/dist/src/commands/learn-command.js +5 -3
  99. package/dist/src/commands/path-command.js +1 -1
  100. package/dist/src/commands/research-command.js +1 -1
  101. package/dist/src/commands/test-command.js +1 -1
  102. package/dist/src/components/AddKnowledge.d.ts +2 -0
  103. package/dist/src/components/AddKnowledge.js +3 -7
  104. package/dist/src/components/App.js +3 -3
  105. package/dist/src/config.d.ts +19 -0
  106. package/dist/src/config.js +156 -10
  107. package/dist/src/experience-tracker.d.ts +11 -6
  108. package/dist/src/experience-tracker.js +82 -155
  109. package/dist/src/explorbot.d.ts +21 -13
  110. package/dist/src/explorbot.js +84 -80
  111. package/dist/src/explorer.d.ts +66 -102
  112. package/dist/src/explorer.js +332 -623
  113. package/dist/src/knowledge-tracker.d.ts +2 -2
  114. package/dist/src/knowledge-tracker.js +38 -45
  115. package/dist/src/state-manager.d.ts +10 -30
  116. package/dist/src/state-manager.js +11 -129
  117. package/dist/src/utils/aria.d.ts +6 -1
  118. package/dist/src/utils/aria.js +4 -1
  119. package/dist/src/utils/cache.d.ts +15 -0
  120. package/dist/src/utils/cache.js +34 -0
  121. package/dist/src/utils/context-formatter.d.ts +6 -0
  122. package/dist/src/utils/context-formatter.js +15 -27
  123. package/dist/src/utils/hooks-runner.js +2 -4
  124. package/dist/src/utils/html.d.ts +5 -0
  125. package/dist/src/utils/html.js +71 -0
  126. package/dist/src/utils/markdown-files.d.ts +10 -0
  127. package/dist/src/utils/markdown-files.js +21 -0
  128. package/dist/src/utils/markdown-query.d.ts +6 -0
  129. package/dist/src/utils/markdown-query.js +14 -1
  130. package/dist/src/utils/page-readiness.d.ts +1 -0
  131. package/dist/src/utils/page-readiness.js +1 -1
  132. package/dist/src/utils/secrets.js +2 -2
  133. package/dist/src/utils/strings.d.ts +2 -0
  134. package/dist/src/utils/strings.js +10 -0
  135. package/dist/src/utils/test-files.js +1 -1
  136. package/dist/src/utils/web-annotate.d.ts +5 -0
  137. package/dist/src/utils/web-annotate.js +58 -0
  138. package/dist/src/utils/web-eidx.d.ts +2 -0
  139. package/dist/src/utils/web-eidx.js +20 -0
  140. package/dist/src/utils/web-element.d.ts +3 -1
  141. package/dist/src/utils/web-element.js +32 -0
  142. package/dist/src/utils/web-sandbox.d.ts +4 -0
  143. package/dist/src/utils/web-sandbox.js +37 -0
  144. package/models.json +30 -0
  145. package/package.json +23 -3
  146. package/src/action-result.ts +70 -173
  147. package/src/action.ts +38 -132
  148. package/src/ai/agent.ts +20 -0
  149. package/src/ai/captain/idle-mode.ts +1 -1
  150. package/src/ai/captain/test-mode.ts +1 -1
  151. package/src/ai/captain/web-mode.ts +18 -30
  152. package/src/ai/captain.ts +17 -42
  153. package/src/ai/conversation.ts +9 -32
  154. package/src/ai/driller.ts +53 -83
  155. package/src/ai/experience-compactor.ts +16 -35
  156. package/src/ai/fisherman-tools.ts +1 -1
  157. package/src/ai/fisherman.ts +0 -4
  158. package/src/ai/historian/codeceptjs.ts +5 -3
  159. package/src/ai/historian/playwright.ts +7 -4
  160. package/src/ai/historian/screencast.ts +4 -3
  161. package/src/ai/historian/utils.ts +0 -1
  162. package/src/ai/historian.ts +6 -8
  163. package/src/ai/navigator.ts +39 -70
  164. package/src/ai/pilot.ts +59 -61
  165. package/src/ai/planner.ts +76 -62
  166. package/src/ai/provider.ts +85 -146
  167. package/src/ai/quartermaster.ts +39 -45
  168. package/src/ai/rerunner.ts +27 -50
  169. package/src/ai/researcher/cache.ts +10 -16
  170. package/src/ai/researcher/coordinates.ts +4 -3
  171. package/src/ai/researcher/deep-analysis.ts +10 -11
  172. package/src/ai/researcher/locators.ts +66 -9
  173. package/src/ai/researcher/parser.ts +1 -1
  174. package/src/ai/researcher/research-result.ts +0 -18
  175. package/src/ai/researcher/sections.ts +9 -3
  176. package/src/ai/researcher.ts +42 -119
  177. package/src/ai/rules.ts +24 -13
  178. package/src/ai/session-analyst.ts +8 -5
  179. package/src/ai/task-agent.ts +45 -38
  180. package/src/ai/tester.ts +51 -129
  181. package/src/ai/tools.ts +145 -153
  182. package/src/command-handler.ts +1 -1
  183. package/src/commands/compact-command.ts +7 -8
  184. package/src/commands/context-aria-command.ts +1 -1
  185. package/src/commands/context-command.ts +8 -6
  186. package/src/commands/context-data-command.ts +2 -2
  187. package/src/commands/context-experience-command.ts +2 -2
  188. package/src/commands/context-html-command.ts +2 -2
  189. package/src/commands/context-knowledge-command.ts +2 -2
  190. package/src/commands/drill-command.ts +1 -1
  191. package/src/commands/experience-command.ts +1 -1
  192. package/src/commands/explore-command.ts +4 -3
  193. package/src/commands/freesail-command.ts +2 -2
  194. package/src/commands/knows-command.ts +1 -1
  195. package/src/commands/learn-command.ts +5 -3
  196. package/src/commands/path-command.ts +1 -1
  197. package/src/commands/research-command.ts +1 -1
  198. package/src/commands/test-command.ts +1 -1
  199. package/src/components/AddKnowledge.tsx +5 -7
  200. package/src/components/App.tsx +3 -3
  201. package/src/config.ts +185 -11
  202. package/src/experience-tracker.ts +89 -152
  203. package/src/explorbot.ts +88 -83
  204. package/src/explorer.ts +402 -693
  205. package/src/knowledge-tracker.ts +42 -49
  206. package/src/state-manager.ts +20 -155
  207. package/src/utils/aria.ts +11 -2
  208. package/src/utils/cache.ts +40 -0
  209. package/src/utils/context-formatter.ts +12 -15
  210. package/src/utils/hooks-runner.ts +2 -4
  211. package/src/utils/html.ts +79 -0
  212. package/src/utils/markdown-files.ts +30 -0
  213. package/src/utils/markdown-query.ts +15 -1
  214. package/src/utils/page-readiness.ts +1 -1
  215. package/src/utils/secrets.ts +2 -3
  216. package/src/utils/strings.ts +12 -0
  217. package/src/utils/test-files.ts +1 -1
  218. package/src/utils/web-annotate.ts +64 -0
  219. package/src/utils/web-eidx.ts +21 -0
  220. package/src/utils/web-element.ts +34 -0
  221. package/src/utils/web-sandbox.ts +43 -0
  222. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -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
- otherTabs = [];
40
- _activeTest = null;
26
+ requestStore;
27
+ playwrightRecorder;
28
+ playwrightHelper;
29
+ _actor;
30
+ started = false;
31
+ isSharedBrowser = false;
41
32
  xhrCapture = null;
42
- requestStore = null;
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, aiProvider, options) {
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
- initializeContainer() {
58
- try {
59
- // Use project root for output directory, not current working directory
60
- const configParser = ConfigParser.getInstance();
61
- const configPath = configParser.getConfigPath();
62
- const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
63
- global.output_dir = path.join(projectRoot, 'output', 'states');
64
- global.codecept_dir = projectRoot;
65
- configParser.validateConfig(this.config);
66
- const codeceptConfig = this.convertToCodeceptConfig(this.config);
67
- codeceptjs.container.create(codeceptConfig, {});
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
- catch (error) {
70
- log(`❌ Failed to initialize container: ${error}`);
71
- throw error;
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 configPath = ConfigParser.getInstance().getConfigPath();
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
- createAction() {
244
- return new Action(this.actor, this.stateManager, this.playwrightRecorder);
276
+ attachXhrCapture() {
277
+ this.xhrCapture = new XhrCapture(this.requestStore, this.config.playwright.url);
278
+ this.xhrCapture.attach(this.playwrightHelper.page);
245
279
  }
246
- async runWithBrowserRecovery(label, operation) {
247
- if (!(await this.ensurePageAvailable())) {
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) && !this.isFatalBrowserError(retryError))
296
+ if (!isNavigationTransitionError(retryError) && !isFatalBrowserError(retryError))
263
297
  throw retryError;
264
298
  recoveryError = retryError;
265
299
  }
266
300
  }
267
- if (!this.isFatalBrowserError(recoveryError))
301
+ if (!isFatalBrowserError(recoveryError))
268
302
  throw recoveryError;
269
303
  tag('warning').log(`${label}: browser page is unavailable, recovering...`);
270
- let recovered = await this.recoverFromBrowserError();
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 (this.isFatalBrowserError(retryError)) {
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.createAction();
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 = err instanceof Error ? err.message : String(err);
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 annotateElements() {
343
- return this.runWithBrowserRecovery('annotateElements', async () => {
344
- const { elements } = await annotatePageElements(this.playwrightHelper.page);
345
- return elements;
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
- async reload() {
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
- catch { }
398
- const baseUrl = this.config.playwright?.url || this.config.web?.url;
399
- if (!baseUrl)
400
- return null;
401
- try {
402
- return new URL(url, baseUrl).toString();
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
- catch {
405
- return null;
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
- isFatalBrowserError(error) {
409
- return isFatalBrowserError(error);
387
+ async recoverOrRestart() {
388
+ if (await this.recoverPage())
389
+ return true;
390
+ return this.restartBrowser();
410
391
  }
411
- async recoverFromBrowserError() {
392
+ async recoverPage() {
412
393
  try {
413
- if (!this.playwrightHelper?.page || this.playwrightHelper.page.isClosed?.()) {
394
+ if (!this.page) {
414
395
  const context = this.playwrightHelper?.browserContext;
415
396
  if (!context)
416
- return await this.restartBrowser();
417
- const page = await context.newPage();
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, { waitUntil: 'domcontentloaded', timeout: 10000 });
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({ waitUntil: 'domcontentloaded', timeout: 10000 });
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 instanceof Error ? err.message : 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
- if (this.xhrCapture && this.playwrightHelper.page) {
446
- this.xhrCapture.detach(this.playwrightHelper.page);
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.setupXhrCapture(true);
451
+ this.attachXhrCapture();
464
452
  this.listenToStateChanged();
465
453
  if (url) {
466
- await this.playwrightHelper.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
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 instanceof Error ? err.message : err}`);
462
+ tag('error').log(`Browser restart failed: ${browserErrorMessage(err)}`);
475
463
  return false;
476
464
  }
477
465
  }
478
- async switchToMainFrame() {
479
- if (this.playwrightHelper.frame) {
480
- debugLog('Switching to main frame');
481
- await this.playwrightHelper.switchTo();
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
- if (!this.playwrightHelper) {
530
- debugLog('Playwright helper not available for state monitoring');
494
+ const page = this.playwrightHelper?.page;
495
+ if (!page) {
496
+ debugLog('Playwright page not available for state monitoring');
531
497
  return;
532
498
  }
533
- try {
534
- const page = this.playwrightHelper.page;
535
- if (!page) {
536
- debugLog('Playwright page not available for state monitoring');
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 (this.playwrightHelper.browserContext) {
591
- await this.playwrightHelper.browserContext.close();
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 (err) {
596
- debugLog('Failed to close browser context:', err);
517
+ catch (error) {
518
+ debugLog('Failed to get new tab info:', error);
519
+ this.stateManager.otherTabs.push({ url: 'unknown', title: 'unknown' });
597
520
  }
598
- this.playwrightHelper.browser = null;
599
- this.playwrightHelper.isRunning = false;
600
- await Promise.all([this.reporter.finishRun(), codeceptjs.recorder.stop()]);
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
- async startTest(test) {
610
- this._activeTest = test;
611
- test.start();
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 lastScreenshot = ActionResult.fromState(this.stateManager.getCurrentState()).screenshotFile;
629
- test.setActiveNoteScreenshot(lastScreenshot);
630
- };
631
- this.watchActiveTestPage();
632
- const onStepPassed = (step) => stepHandler(step, 'passed');
633
- const onStepFailed = (step, error) => {
634
- stepHandler(step, 'failed', error?.message || String(error), error?.stack);
635
- };
636
- const onTestAfter = () => {
637
- codeceptjs.event.dispatcher.off('step.passed', onStepPassed);
638
- codeceptjs.event.dispatcher.off('step.failed', onStepFailed);
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 ensurePageAvailable() {
650
- const page = this.playwrightHelper?.page;
651
- if (page && !page.isClosed?.()) {
652
- this.watchActiveTestPage(page);
653
- return true;
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
- const recovered = await this.recoverFromBrowserError();
656
- if (!recovered)
657
- return false;
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 handleExecutionError(error) {
665
- const message = error instanceof Error ? error.message : String(error);
666
- tag('error').log(`Browser execution error: ${message}`);
667
- if (error instanceof Error && error.name === 'AbortError') {
668
- return {
669
- action: 'stop',
670
- message,
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
- if (error instanceof BrowserRecoveryError) {
674
- return {
675
- action: 'stop',
676
- recovered: error.recovered,
677
- message: error.message,
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 (!this.isFatalBrowserError(error)) {
681
- return {
682
- action: 'continue',
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
- let recovered = await this.recoverFromBrowserError();
687
- if (!recovered)
688
- recovered = await this.restartBrowser();
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
- this.watchActiveTestPage();
697
- return {
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;