explorbot 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -26
- package/bin/explorbot-cli.ts +680 -0
- package/boat/api-tester/src/ai/chief/styles.ts +15 -0
- package/boat/api-tester/src/ai/chief.ts +335 -0
- package/boat/api-tester/src/ai/curler-tools.ts +278 -0
- package/boat/api-tester/src/ai/curler.ts +306 -0
- package/boat/api-tester/src/api-client.ts +28 -0
- package/boat/api-tester/src/apibot.ts +203 -0
- package/boat/api-tester/src/cli.ts +301 -0
- package/boat/api-tester/src/config.ts +190 -0
- package/dist/bin/explorbot-cli.js +23 -101
- package/dist/boat/api-tester/bin/apibot-cli.js +0 -1
- package/dist/boat/api-tester/src/ai/chief/styles.js +0 -1
- package/dist/boat/api-tester/src/ai/chief.js +0 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +0 -1
- package/dist/boat/api-tester/src/ai/curler.js +0 -1
- package/dist/boat/api-tester/src/api-client.js +0 -1
- package/dist/boat/api-tester/src/apibot.js +0 -1
- package/dist/boat/api-tester/src/cli.js +0 -1
- package/dist/boat/api-tester/src/config.js +0 -1
- package/dist/src/action-result.js +0 -1
- package/dist/src/action.js +14 -12
- package/dist/src/activity.js +0 -1
- package/dist/src/ai/agent.js +0 -1
- package/dist/src/ai/bosun.js +0 -1
- package/dist/src/ai/captain/idle-mode.js +0 -1
- package/dist/src/ai/captain/mixin.js +0 -1
- package/dist/src/ai/captain/test-mode.js +0 -1
- package/dist/src/ai/captain/web-mode.js +0 -1
- package/dist/src/ai/captain.js +0 -1
- package/dist/src/ai/conversation.js +0 -1
- package/dist/src/ai/experience-compactor.js +0 -1
- package/dist/src/ai/fisherman-tools.js +0 -1
- package/dist/src/ai/fisherman.js +0 -1
- package/dist/src/ai/historian.js +0 -1
- package/dist/src/ai/navigator.js +0 -1
- package/dist/src/ai/pilot.js +0 -1
- package/dist/src/ai/planner/session-dedup.js +0 -1
- package/dist/src/ai/planner/styles.js +0 -1
- package/dist/src/ai/planner/subpages.js +42 -7
- package/dist/src/ai/planner.js +15 -4
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.js +0 -1
- package/dist/src/ai/researcher/cache.js +13 -9
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.js +16 -20
- package/dist/src/ai/researcher/fingerprint-worker.js +0 -1
- package/dist/src/ai/researcher/focus.js +0 -1
- package/dist/src/ai/researcher/locators.js +1 -2
- package/dist/src/ai/researcher/mixin.js +0 -1
- package/dist/src/ai/researcher/parser.js +4 -4
- package/dist/src/ai/researcher/research-result.js +2 -1
- package/dist/src/ai/researcher.js +6 -6
- package/dist/src/ai/rules.js +0 -1
- package/dist/src/ai/task-agent.js +0 -1
- package/dist/src/ai/tester.js +0 -1
- package/dist/src/ai/tools.js +4 -1
- package/dist/src/api/api-client.js +0 -1
- package/dist/src/api/request-result.js +0 -1
- package/dist/src/api/request-store.js +0 -1
- package/dist/src/api/spec-reader.js +0 -1
- package/dist/src/api/xhr-capture.js +0 -1
- package/dist/src/browser-server.js +0 -1
- package/dist/src/command-handler.js +0 -1
- package/dist/src/commands/add-rule-command.js +0 -1
- package/dist/src/commands/base-command.js +0 -1
- package/dist/src/commands/clean-command.js +0 -1
- package/dist/src/commands/context-aria-command.js +0 -1
- package/dist/src/commands/context-command.js +2 -3
- package/dist/src/commands/context-data-command.js +0 -1
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-html-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/debug-command.js +0 -1
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/exit-command.js +0 -1
- package/dist/src/commands/explore-command.js +3 -3
- package/dist/src/commands/freesail-command.js +0 -1
- package/dist/src/commands/help-command.js +0 -1
- package/dist/src/commands/index.js +0 -1
- package/dist/src/commands/init-command.js +117 -0
- package/dist/src/commands/knows-command.js +0 -1
- package/dist/src/commands/learn-command.js +0 -1
- package/dist/src/commands/navigate-command.js +0 -1
- package/dist/src/commands/path-command.js +0 -1
- package/dist/src/commands/plan-clear-command.js +0 -1
- package/dist/src/commands/plan-command.js +6 -2
- package/dist/src/commands/plan-edit-command.js +0 -1
- package/dist/src/commands/plan-load-command.js +0 -1
- package/dist/src/commands/plan-reload-command.js +0 -1
- package/dist/src/commands/plan-save-command.js +0 -1
- package/dist/src/commands/research-command.js +0 -1
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/status-command.js +0 -1
- package/dist/src/commands/test-command.js +0 -1
- package/dist/src/components/ActivityPane.js +0 -1
- package/dist/src/components/AddKnowledge.js +0 -1
- package/dist/src/components/AddRule.js +0 -1
- package/dist/src/components/App.js +0 -1
- package/dist/src/components/Autocomplete.js +0 -1
- package/dist/src/components/InputPane.js +0 -1
- package/dist/src/components/InputReadline.js +0 -1
- package/dist/src/components/LogPane.js +0 -1
- package/dist/src/components/PlanEditor.js +0 -1
- package/dist/src/components/PlanPane.js +0 -1
- package/dist/src/components/SessionTimer.js +0 -1
- package/dist/src/components/StateTransitionPane.js +0 -1
- package/dist/src/components/StatusPane.js +0 -1
- package/dist/src/components/TaskPane.js +0 -1
- package/dist/src/components/Welcome.js +0 -1
- package/dist/src/components/WelcomeChecklist.js +0 -1
- package/dist/src/components/WelcomeCommands.js +0 -1
- package/dist/src/components/autocomplete-store.js +0 -1
- package/dist/src/components/parse-keypress.js +0 -1
- package/dist/src/config.js +0 -1
- package/dist/src/execution-controller.js +0 -1
- package/dist/src/experience-tracker.js +0 -1
- package/dist/src/explorbot.js +1 -2
- package/dist/src/explorer.js +58 -17
- package/dist/src/index.js +0 -1
- package/dist/src/knowledge-tracker.js +2 -2
- package/dist/src/observability.js +0 -1
- package/dist/src/reporter.js +0 -1
- package/dist/src/state-manager.js +0 -1
- package/dist/src/stats.js +0 -1
- package/dist/src/test-plan.js +0 -1
- package/dist/src/utils/aria.js +0 -1
- package/dist/src/utils/cli-name.js +16 -0
- package/dist/src/utils/code-extractor.js +0 -1
- package/dist/src/utils/context-formatter.js +0 -1
- package/dist/src/utils/error-page.js +0 -1
- package/dist/src/utils/expandable.js +0 -1
- package/dist/src/utils/hooks-runner.js +0 -1
- package/dist/src/utils/html-diff.js +0 -1
- package/dist/src/utils/html.js +0 -1
- package/dist/src/utils/logger.js +0 -1
- package/dist/src/utils/loop.js +0 -1
- package/dist/src/utils/markdown-parser.js +0 -1
- package/dist/src/utils/markdown-query.js +0 -1
- package/dist/src/utils/markdown-terminal.js +0 -1
- package/dist/src/utils/research-parser.js +0 -1
- package/dist/src/utils/retry.js +0 -1
- package/dist/src/utils/rules-loader.js +0 -1
- package/dist/src/utils/strings.js +0 -1
- package/dist/src/utils/test-plan-markdown.js +0 -1
- package/dist/src/utils/throttle.js +0 -1
- package/dist/src/utils/unique-names.js +0 -1
- package/dist/src/utils/url-matcher.js +0 -1
- package/dist/src/utils/web-element.js +6 -5
- package/dist/src/utils/xpath.js +0 -1
- package/package.json +28 -4
- package/src/action-result.ts +694 -0
- package/src/action.ts +449 -0
- package/src/activity.ts +111 -0
- package/src/ai/agent.ts +3 -0
- package/src/ai/bosun.ts +557 -0
- package/src/ai/captain/idle-mode.ts +116 -0
- package/src/ai/captain/mixin.ts +22 -0
- package/src/ai/captain/test-mode.ts +262 -0
- package/src/ai/captain/web-mode.ts +136 -0
- package/src/ai/captain.ts +504 -0
- package/src/ai/conversation.ts +205 -0
- package/src/ai/experience-compactor.ts +284 -0
- package/src/ai/fisherman-tools.ts +181 -0
- package/src/ai/fisherman.ts +223 -0
- package/src/ai/historian.ts +457 -0
- package/src/ai/navigator.ts +572 -0
- package/src/ai/pilot.ts +776 -0
- package/src/ai/planner/session-dedup.ts +35 -0
- package/src/ai/planner/styles.ts +17 -0
- package/src/ai/planner/subpages.ts +171 -0
- package/src/ai/planner.ts +549 -0
- package/src/ai/provider.ts +613 -0
- package/src/ai/quartermaster.ts +286 -0
- package/src/ai/researcher/cache.ts +109 -0
- package/src/ai/researcher/coordinates.ts +239 -0
- package/src/ai/researcher/deep-analysis.ts +412 -0
- package/src/ai/researcher/fingerprint-worker.ts +59 -0
- package/src/ai/researcher/focus.ts +42 -0
- package/src/ai/researcher/locators.ts +282 -0
- package/src/ai/researcher/mixin.ts +4 -0
- package/src/ai/researcher/parser.ts +186 -0
- package/src/ai/researcher/research-result.ts +116 -0
- package/src/ai/researcher.ts +858 -0
- package/src/ai/rules.ts +376 -0
- package/src/ai/task-agent.ts +141 -0
- package/src/ai/tester.ts +939 -0
- package/src/ai/tools.ts +1122 -0
- package/src/api/api-client.ts +109 -0
- package/src/api/request-result.ts +212 -0
- package/src/api/request-store.ts +130 -0
- package/src/api/spec-reader.ts +174 -0
- package/src/api/xhr-capture.ts +100 -0
- package/src/browser-server.ts +74 -0
- package/src/command-handler.ts +454 -0
- package/src/commands/add-rule-command.ts +63 -0
- package/src/commands/base-command.ts +27 -0
- package/src/commands/clean-command.ts +73 -0
- package/src/commands/context-aria-command.ts +22 -0
- package/src/commands/context-command.ts +67 -0
- package/src/commands/context-data-command.ts +30 -0
- package/src/commands/context-experience-command.ts +48 -0
- package/src/commands/context-html-command.ts +33 -0
- package/src/commands/context-knowledge-command.ts +43 -0
- package/src/commands/debug-command.ts +13 -0
- package/src/commands/drill-command.ts +34 -0
- package/src/commands/exit-command.ts +32 -0
- package/src/commands/explore-command.ts +129 -0
- package/src/commands/freesail-command.ts +95 -0
- package/src/commands/help-command.ts +8 -0
- package/src/commands/index.ts +69 -0
- package/src/commands/init-command.ts +131 -0
- package/src/commands/knows-command.ts +68 -0
- package/src/commands/learn-command.ts +44 -0
- package/src/commands/navigate-command.ts +18 -0
- package/src/commands/path-command.ts +83 -0
- package/src/commands/plan-clear-command.ts +14 -0
- package/src/commands/plan-command.ts +46 -0
- package/src/commands/plan-edit-command.ts +9 -0
- package/src/commands/plan-load-command.ts +18 -0
- package/src/commands/plan-reload-command.ts +28 -0
- package/src/commands/plan-save-command.ts +25 -0
- package/src/commands/research-command.ts +45 -0
- package/src/commands/start-command.ts +13 -0
- package/src/commands/status-command.tsx +23 -0
- package/src/commands/test-command.ts +84 -0
- package/src/components/ActivityPane.tsx +80 -0
- package/src/components/AddKnowledge.tsx +169 -0
- package/src/components/AddRule.tsx +174 -0
- package/src/components/App.tsx +377 -0
- package/src/components/Autocomplete.tsx +63 -0
- package/src/components/InputPane.tsx +259 -0
- package/src/components/InputReadline.tsx +704 -0
- package/src/components/LogPane.tsx +187 -0
- package/src/components/PlanEditor.tsx +150 -0
- package/src/components/PlanPane.tsx +71 -0
- package/src/components/SessionTimer.tsx +35 -0
- package/src/components/StateTransitionPane.tsx +149 -0
- package/src/components/StatusPane.tsx +62 -0
- package/src/components/TaskPane.tsx +119 -0
- package/src/components/Welcome.tsx +83 -0
- package/src/components/WelcomeChecklist.tsx +118 -0
- package/src/components/WelcomeCommands.tsx +102 -0
- package/src/components/autocomplete-store.ts +35 -0
- package/src/components/parse-keypress.ts +170 -0
- package/src/config.ts +491 -0
- package/src/execution-controller.ts +109 -0
- package/src/experience-tracker.ts +350 -0
- package/src/explorbot.ts +405 -0
- package/src/explorer.ts +760 -0
- package/src/index.tsx +62 -0
- package/src/knowledge-tracker.ts +230 -0
- package/src/observability.ts +150 -0
- package/src/reporter.ts +224 -0
- package/src/state-manager.ts +556 -0
- package/src/stats.ts +53 -0
- package/src/test-plan.ts +432 -0
- package/src/utils/aria.ts +629 -0
- package/src/utils/cli-name.ts +13 -0
- package/src/utils/code-extractor.ts +22 -0
- package/src/utils/context-formatter.ts +239 -0
- package/src/utils/error-page.ts +23 -0
- package/src/utils/expandable.ts +38 -0
- package/src/utils/hooks-runner.ts +79 -0
- package/src/utils/html-diff.ts +918 -0
- package/src/utils/html.ts +1316 -0
- package/src/utils/logger.ts +534 -0
- package/src/utils/loop.ts +176 -0
- package/src/utils/markdown-parser.ts +127 -0
- package/src/utils/markdown-query.ts +466 -0
- package/src/utils/markdown-terminal.ts +43 -0
- package/src/utils/research-parser.ts +11 -0
- package/src/utils/retry.ts +73 -0
- package/src/utils/rules-loader.ts +118 -0
- package/src/utils/strings.ts +13 -0
- package/src/utils/test-plan-markdown.ts +332 -0
- package/src/utils/throttle.ts +18 -0
- package/src/utils/unique-names.ts +14 -0
- package/src/utils/url-matcher.ts +45 -0
- package/src/utils/web-element.ts +147 -0
- package/src/utils/xpath.ts +129 -0
- package/dist/bin/explorbot-cli.js.map +0 -1
- package/dist/boat/api-tester/bin/apibot-cli.js.map +0 -1
- package/dist/boat/api-tester/example/apibot.config.js +0 -31
- package/dist/boat/api-tester/example/apibot.config.js.map +0 -1
- package/dist/boat/api-tester/src/ai/chief/styles.js.map +0 -1
- package/dist/boat/api-tester/src/ai/chief.js.map +0 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js.map +0 -1
- package/dist/boat/api-tester/src/ai/curler.js.map +0 -1
- package/dist/boat/api-tester/src/api-client.js.map +0 -1
- package/dist/boat/api-tester/src/apibot.js.map +0 -1
- package/dist/boat/api-tester/src/cli.js.map +0 -1
- package/dist/boat/api-tester/src/config.js.map +0 -1
- package/dist/prompts/audit-rules.md +0 -124
- package/dist/src/action-result.js.map +0 -1
- package/dist/src/action.js.map +0 -1
- package/dist/src/activity.js.map +0 -1
- package/dist/src/ai/agent.js.map +0 -1
- package/dist/src/ai/bosun.js.map +0 -1
- package/dist/src/ai/captain/idle-mode.js.map +0 -1
- package/dist/src/ai/captain/mixin.js.map +0 -1
- package/dist/src/ai/captain/test-mode.js.map +0 -1
- package/dist/src/ai/captain/web-mode.js.map +0 -1
- package/dist/src/ai/captain.js.map +0 -1
- package/dist/src/ai/conversation.js.map +0 -1
- package/dist/src/ai/experience-compactor.js.map +0 -1
- package/dist/src/ai/fisherman-tools.js.map +0 -1
- package/dist/src/ai/fisherman.js.map +0 -1
- package/dist/src/ai/historian.js.map +0 -1
- package/dist/src/ai/navigator.js.map +0 -1
- package/dist/src/ai/pilot.js.map +0 -1
- package/dist/src/ai/planner/session-dedup.js.map +0 -1
- package/dist/src/ai/planner/styles.js.map +0 -1
- package/dist/src/ai/planner/subpages.js.map +0 -1
- package/dist/src/ai/planner.js.map +0 -1
- package/dist/src/ai/provider.js.map +0 -1
- package/dist/src/ai/quartermaster.js.map +0 -1
- package/dist/src/ai/researcher/cache.js.map +0 -1
- package/dist/src/ai/researcher/coordinates.js.map +0 -1
- package/dist/src/ai/researcher/deep-analysis.js.map +0 -1
- package/dist/src/ai/researcher/fingerprint-worker.js.map +0 -1
- package/dist/src/ai/researcher/focus.js.map +0 -1
- package/dist/src/ai/researcher/locators.js.map +0 -1
- package/dist/src/ai/researcher/mixin.js.map +0 -1
- package/dist/src/ai/researcher/parser.js.map +0 -1
- package/dist/src/ai/researcher/research-result.js.map +0 -1
- package/dist/src/ai/researcher.js.map +0 -1
- package/dist/src/ai/rules.js.map +0 -1
- package/dist/src/ai/task-agent.js.map +0 -1
- package/dist/src/ai/tester.js.map +0 -1
- package/dist/src/ai/tools.js.map +0 -1
- package/dist/src/api/api-client.js.map +0 -1
- package/dist/src/api/request-result.js.map +0 -1
- package/dist/src/api/request-store.js.map +0 -1
- package/dist/src/api/spec-reader.js.map +0 -1
- package/dist/src/api/xhr-capture.js.map +0 -1
- package/dist/src/browser-server.js.map +0 -1
- package/dist/src/command-handler.js.map +0 -1
- package/dist/src/commands/add-rule-command.js.map +0 -1
- package/dist/src/commands/base-command.js.map +0 -1
- package/dist/src/commands/clean-command.js.map +0 -1
- package/dist/src/commands/context-aria-command.js.map +0 -1
- package/dist/src/commands/context-command.js.map +0 -1
- package/dist/src/commands/context-data-command.js.map +0 -1
- package/dist/src/commands/context-experience-command.js.map +0 -1
- package/dist/src/commands/context-html-command.js.map +0 -1
- package/dist/src/commands/context-knowledge-command.js.map +0 -1
- package/dist/src/commands/debug-command.js.map +0 -1
- package/dist/src/commands/drill-command.js.map +0 -1
- package/dist/src/commands/exit-command.js.map +0 -1
- package/dist/src/commands/explore-command.js.map +0 -1
- package/dist/src/commands/freesail-command.js.map +0 -1
- package/dist/src/commands/help-command.js.map +0 -1
- package/dist/src/commands/index.js.map +0 -1
- package/dist/src/commands/knows-command.js.map +0 -1
- package/dist/src/commands/learn-command.js.map +0 -1
- package/dist/src/commands/navigate-command.js.map +0 -1
- package/dist/src/commands/path-command.js.map +0 -1
- package/dist/src/commands/plan-clear-command.js.map +0 -1
- package/dist/src/commands/plan-command.js.map +0 -1
- package/dist/src/commands/plan-edit-command.js.map +0 -1
- package/dist/src/commands/plan-load-command.js.map +0 -1
- package/dist/src/commands/plan-reload-command.js.map +0 -1
- package/dist/src/commands/plan-save-command.js.map +0 -1
- package/dist/src/commands/research-command.js.map +0 -1
- package/dist/src/commands/start-command.js.map +0 -1
- package/dist/src/commands/status-command.js.map +0 -1
- package/dist/src/commands/test-command.js.map +0 -1
- package/dist/src/components/ActivityPane.js.map +0 -1
- package/dist/src/components/AddKnowledge.js.map +0 -1
- package/dist/src/components/AddRule.js.map +0 -1
- package/dist/src/components/App.js.map +0 -1
- package/dist/src/components/Autocomplete.js.map +0 -1
- package/dist/src/components/InputPane.js.map +0 -1
- package/dist/src/components/InputReadline.js.map +0 -1
- package/dist/src/components/LogPane.js.map +0 -1
- package/dist/src/components/PlanEditor.js.map +0 -1
- package/dist/src/components/PlanPane.js.map +0 -1
- package/dist/src/components/SessionTimer.js.map +0 -1
- package/dist/src/components/StateTransitionPane.js.map +0 -1
- package/dist/src/components/StatusPane.js.map +0 -1
- package/dist/src/components/TaskPane.js.map +0 -1
- package/dist/src/components/Welcome.js.map +0 -1
- package/dist/src/components/WelcomeChecklist.js.map +0 -1
- package/dist/src/components/WelcomeCommands.js.map +0 -1
- package/dist/src/components/autocomplete-store.js.map +0 -1
- package/dist/src/components/parse-keypress.js.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/execution-controller.js.map +0 -1
- package/dist/src/experience-tracker.js.map +0 -1
- package/dist/src/explorbot.js.map +0 -1
- package/dist/src/explorer.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/knowledge-tracker.js.map +0 -1
- package/dist/src/observability.js.map +0 -1
- package/dist/src/reporter.js.map +0 -1
- package/dist/src/state-manager.js.map +0 -1
- package/dist/src/stats.js.map +0 -1
- package/dist/src/test-plan.js.map +0 -1
- package/dist/src/utils/aria.js.map +0 -1
- package/dist/src/utils/code-extractor.js.map +0 -1
- package/dist/src/utils/context-formatter.js.map +0 -1
- package/dist/src/utils/error-page.js.map +0 -1
- package/dist/src/utils/expandable.js.map +0 -1
- package/dist/src/utils/hooks-runner.js.map +0 -1
- package/dist/src/utils/html-diff.js.map +0 -1
- package/dist/src/utils/html.js.map +0 -1
- package/dist/src/utils/logger.js.map +0 -1
- package/dist/src/utils/loop.js.map +0 -1
- package/dist/src/utils/markdown-parser.js.map +0 -1
- package/dist/src/utils/markdown-query.js.map +0 -1
- package/dist/src/utils/markdown-terminal.js.map +0 -1
- package/dist/src/utils/research-parser.js.map +0 -1
- package/dist/src/utils/retry.js.map +0 -1
- package/dist/src/utils/rules-loader.js.map +0 -1
- package/dist/src/utils/strings.js.map +0 -1
- package/dist/src/utils/test-plan-markdown.js.map +0 -1
- package/dist/src/utils/throttle.js.map +0 -1
- package/dist/src/utils/unique-names.js.map +0 -1
- package/dist/src/utils/url-matcher.js.map +0 -1
- package/dist/src/utils/web-element.js.map +0 -1
- package/dist/src/utils/xpath.js.map +0 -1
- package/prompts/audit-rules.md +0 -124
package/src/explorer.ts
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import path, { join } from 'node:path';
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import * as codeceptjs from 'codeceptjs';
|
|
5
|
+
import stepsListener from 'codeceptjs/lib/listener/steps';
|
|
6
|
+
import storeListener from 'codeceptjs/lib/listener/store';
|
|
7
|
+
import { createTest } from 'codeceptjs/lib/mocha/test';
|
|
8
|
+
import { ActionResult } from './action-result.ts';
|
|
9
|
+
import Action from './action.js';
|
|
10
|
+
import { AIProvider } from './ai/provider.js';
|
|
11
|
+
import { visuallyAnnotateContainers } from './ai/researcher/coordinates.ts';
|
|
12
|
+
import type { ExplorbotConfig } from './config.js';
|
|
13
|
+
import { ConfigParser, outputPath } from './config.js';
|
|
14
|
+
import type { UserResolveFunction } from './explorbot.js';
|
|
15
|
+
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
16
|
+
import { Reporter } from './reporter.ts';
|
|
17
|
+
import { StateManager } from './state-manager.js';
|
|
18
|
+
import { Test } from './test-plan.ts';
|
|
19
|
+
import { RequestStore } from './api/request-store.ts';
|
|
20
|
+
import { XhrCapture } from './api/xhr-capture.ts';
|
|
21
|
+
import { createDebug, log, tag } from './utils/logger.js';
|
|
22
|
+
import { WebElement, extractElementData } from './utils/web-element.ts';
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
namespace NodeJS {
|
|
26
|
+
interface Global {
|
|
27
|
+
output_dir: string;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare namespace CodeceptJS {
|
|
33
|
+
interface I {
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const debugLog = createDebug('explorbot:explorer');
|
|
39
|
+
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
40
|
+
|
|
41
|
+
interface TabInfo {
|
|
42
|
+
url: string;
|
|
43
|
+
title: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Explorer {
|
|
47
|
+
private aiProvider: AIProvider;
|
|
48
|
+
playwrightHelper: any;
|
|
49
|
+
public isStarted = false;
|
|
50
|
+
private isSharedBrowser = false;
|
|
51
|
+
actor!: CodeceptJS.I;
|
|
52
|
+
private stateManager!: StateManager;
|
|
53
|
+
private knowledgeTracker!: KnowledgeTracker;
|
|
54
|
+
config: ExplorbotConfig;
|
|
55
|
+
private userResolveFn: UserResolveFunction | null = null;
|
|
56
|
+
private options?: { show?: boolean; headless?: boolean; incognito?: boolean; session?: string };
|
|
57
|
+
private reporter!: Reporter;
|
|
58
|
+
private otherTabs: TabInfo[] = [];
|
|
59
|
+
private _activeTest: Test | null = null;
|
|
60
|
+
private xhrCapture: XhrCapture | null = null;
|
|
61
|
+
private requestStore: RequestStore | null = null;
|
|
62
|
+
|
|
63
|
+
constructor(config: ExplorbotConfig, aiProvider: AIProvider, options?: { show?: boolean; headless?: boolean; incognito?: boolean; session?: string }) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.aiProvider = aiProvider;
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.initializeContainer();
|
|
68
|
+
this.stateManager = new StateManager({ incognito: this.options?.incognito });
|
|
69
|
+
this.knowledgeTracker = new KnowledgeTracker();
|
|
70
|
+
this.reporter = new Reporter(config.reporter);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private initializeContainer() {
|
|
74
|
+
try {
|
|
75
|
+
// Use project root for output directory, not current working directory
|
|
76
|
+
const configParser = ConfigParser.getInstance();
|
|
77
|
+
const configPath = configParser.getConfigPath();
|
|
78
|
+
const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
|
|
79
|
+
(global as any).output_dir = path.join(projectRoot, 'output', 'states');
|
|
80
|
+
(global as any).codecept_dir = projectRoot;
|
|
81
|
+
|
|
82
|
+
configParser.validateConfig(this.config);
|
|
83
|
+
|
|
84
|
+
const codeceptConfig = this.convertToCodeceptConfig(this.config);
|
|
85
|
+
|
|
86
|
+
codeceptjs.container.create(codeceptConfig, {});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
log(`❌ Failed to initialize container: ${error}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private convertToCodeceptConfig(config: ExplorbotConfig): any {
|
|
94
|
+
const playwrightConfig = { ...config.playwright };
|
|
95
|
+
|
|
96
|
+
if (this.options?.show !== undefined) {
|
|
97
|
+
playwrightConfig.show = this.options.show;
|
|
98
|
+
}
|
|
99
|
+
if (this.options?.headless !== undefined) {
|
|
100
|
+
playwrightConfig.show = !this.options.headless;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let debugInfo = '';
|
|
104
|
+
|
|
105
|
+
if (!playwrightConfig.show && !process.env.CI) {
|
|
106
|
+
if (config.playwright.browser === 'chromium') {
|
|
107
|
+
const debugPort = 9222;
|
|
108
|
+
playwrightConfig.chromium ||= {};
|
|
109
|
+
playwrightConfig.chromium.args = [...(config.playwright.args || []), `--remote-debugging-port=${debugPort}`, '--remote-debugging-address=0.0.0.0'];
|
|
110
|
+
|
|
111
|
+
debugInfo = `Enabling debug protocol for Chromium at http://localhost:${debugPort}`;
|
|
112
|
+
} else if (config.playwright.browser === 'firefox') {
|
|
113
|
+
const debugPort = 9222;
|
|
114
|
+
playwrightConfig.firefox ||= {};
|
|
115
|
+
playwrightConfig.firefox.args = [...(config.playwright.args || []), `--remote-debugging-port=${debugPort}`];
|
|
116
|
+
debugInfo = `Enabling debug protocol for Firefox at http://localhost:${debugPort}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
log(`${playwrightConfig.browser} starting in ${playwrightConfig.show ? 'headed' : 'headless'} mode`);
|
|
121
|
+
if (debugInfo) {
|
|
122
|
+
tag('substep').log(debugInfo);
|
|
123
|
+
}
|
|
124
|
+
const PlaywrightConfig = {
|
|
125
|
+
timeout: 1000,
|
|
126
|
+
highlightElement: true,
|
|
127
|
+
waitForAction: 500,
|
|
128
|
+
...playwrightConfig,
|
|
129
|
+
strict: true,
|
|
130
|
+
fullPageScreenshots: true,
|
|
131
|
+
};
|
|
132
|
+
tag('debug').log(JSON.stringify(PlaywrightConfig, null, 2));
|
|
133
|
+
|
|
134
|
+
const codeceptConfig: any = {
|
|
135
|
+
helpers: {
|
|
136
|
+
Playwright: PlaywrightConfig,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (this.config.stepsFile) {
|
|
141
|
+
const configPath = ConfigParser.getInstance().getConfigPath();
|
|
142
|
+
const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
|
|
143
|
+
codeceptConfig.include = { I: path.resolve(projectRoot, this.config.stepsFile) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return codeceptConfig;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public getConfig(): ExplorbotConfig {
|
|
150
|
+
if (!this.config) {
|
|
151
|
+
throw new Error('Configuration not loaded. Call run() first.');
|
|
152
|
+
}
|
|
153
|
+
return this.config;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public getConfigPath(): string | null {
|
|
157
|
+
const configParser = ConfigParser.getInstance();
|
|
158
|
+
return configParser.getConfigPath();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public getAIProvider(): AIProvider {
|
|
162
|
+
return this.aiProvider;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public getStateManager(): StateManager {
|
|
166
|
+
return this.stateManager;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public getCurrentUrl(): string {
|
|
170
|
+
return this.stateManager.getCurrentState()!.url || '?';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public getKnowledgeTracker(): KnowledgeTracker {
|
|
174
|
+
return this.knowledgeTracker;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public getReporter(): Reporter {
|
|
178
|
+
return this.reporter;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getRequestStore(): RequestStore | null {
|
|
182
|
+
return this.requestStore;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async extractCookies(): Promise<Record<string, string>> {
|
|
186
|
+
if (!this.playwrightHelper?.browserContext) return {};
|
|
187
|
+
try {
|
|
188
|
+
const cookies = await this.playwrightHelper.browserContext.cookies();
|
|
189
|
+
if (!cookies.length) return {};
|
|
190
|
+
const cookieString = cookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
|
|
191
|
+
return { Cookie: cookieString };
|
|
192
|
+
} catch {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private setupXhrCapture(): void {
|
|
198
|
+
const configParser = ConfigParser.getInstance();
|
|
199
|
+
const outputDir = configParser.getOutputDir();
|
|
200
|
+
this.requestStore = new RequestStore(outputDir);
|
|
201
|
+
const baseUrl = this.config.playwright.url;
|
|
202
|
+
this.xhrCapture = new XhrCapture(this.requestStore, baseUrl);
|
|
203
|
+
this.xhrCapture.attach(this.playwrightHelper.page);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async start() {
|
|
207
|
+
if (this.isStarted) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!this.config) {
|
|
212
|
+
await this.initializeContainer();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await codeceptjs.recorder.start();
|
|
216
|
+
await codeceptjs.container.started(null);
|
|
217
|
+
storeListener();
|
|
218
|
+
stepsListener();
|
|
219
|
+
|
|
220
|
+
codeceptjs.recorder.retry({
|
|
221
|
+
retries: this.config.action?.retries || 3,
|
|
222
|
+
when: (err: any) => {
|
|
223
|
+
if (!err || typeof err.message !== 'string') {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
// ignore context errors
|
|
227
|
+
return err.message.includes('context');
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.playwrightHelper = codeceptjs.container.helpers('Playwright');
|
|
232
|
+
if (!this.playwrightHelper) {
|
|
233
|
+
throw new Error('Playwright helper not available');
|
|
234
|
+
}
|
|
235
|
+
await this.connectOrLaunchBrowser();
|
|
236
|
+
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
237
|
+
const contextOptions = hasSession ? { storageState: this.options!.session } : undefined;
|
|
238
|
+
await this.playwrightHelper._createContextPage(contextOptions);
|
|
239
|
+
this.setupXhrCapture();
|
|
240
|
+
if (hasSession) {
|
|
241
|
+
tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options!.session!)}`);
|
|
242
|
+
}
|
|
243
|
+
const I = codeceptjs.container.support('I');
|
|
244
|
+
|
|
245
|
+
this.actor = I;
|
|
246
|
+
this.isStarted = true;
|
|
247
|
+
|
|
248
|
+
this.listenToStateChanged();
|
|
249
|
+
|
|
250
|
+
codeceptjs.event.dispatcher.emit('global.before');
|
|
251
|
+
tag('success').log('Browser started, ready to explore');
|
|
252
|
+
|
|
253
|
+
return I;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async connectOrLaunchBrowser(): Promise<void> {
|
|
257
|
+
const { getAliveEndpoint } = await import('./browser-server.js');
|
|
258
|
+
const endpoint = await getAliveEndpoint();
|
|
259
|
+
|
|
260
|
+
if (endpoint) {
|
|
261
|
+
const browserName = this.config.playwright.browser || 'chromium';
|
|
262
|
+
this.playwrightHelper.options[browserName] ||= {};
|
|
263
|
+
this.playwrightHelper.options[browserName].browserWSEndpoint = endpoint;
|
|
264
|
+
this.playwrightHelper._setConfig(this.playwrightHelper.options);
|
|
265
|
+
await this.playwrightHelper._startBrowser();
|
|
266
|
+
this.isSharedBrowser = true;
|
|
267
|
+
tag('success').log('Connected to persistent browser server');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await this.playwrightHelper._startBrowser();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
createAction() {
|
|
275
|
+
return new Action(this.actor, this.stateManager);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async visit(url: string) {
|
|
279
|
+
await this.closeOtherTabs();
|
|
280
|
+
|
|
281
|
+
const serializedUrl = JSON.stringify(url);
|
|
282
|
+
const currentState = this.stateManager.getCurrentState();
|
|
283
|
+
const actionResult = currentState ? ActionResult.fromState(currentState) : new ActionResult({ url });
|
|
284
|
+
|
|
285
|
+
const { statePush = false, wait, waitForElement, code } = this.knowledgeTracker.getStateParameters(actionResult, ['statePush', 'wait', 'waitForElement', 'code']);
|
|
286
|
+
|
|
287
|
+
const action = this.createAction();
|
|
288
|
+
|
|
289
|
+
if (statePush) {
|
|
290
|
+
await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
|
|
291
|
+
} else {
|
|
292
|
+
await action.execute(`I.amOnPage(${serializedUrl})`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (wait !== undefined) {
|
|
296
|
+
debugLog('Waiting for', wait);
|
|
297
|
+
await action.execute(`I.wait(${wait})`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (waitForElement) {
|
|
301
|
+
await action.execute(`I.waitForElement(${JSON.stringify(waitForElement)})`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (code) {
|
|
305
|
+
debugLog('Executing knowledge code:', code);
|
|
306
|
+
await action.execute(code);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return action;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async annotateElements(): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
313
|
+
return annotatePageElements(this.playwrightHelper.page);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
|
|
317
|
+
return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async getEidxInContainer(containerCss: string | null): Promise<string[]> {
|
|
321
|
+
const page = this.playwrightHelper.page;
|
|
322
|
+
try {
|
|
323
|
+
const selector = containerCss ? `${containerCss} [data-explorbot-eidx]` : '[data-explorbot-eidx]';
|
|
324
|
+
const elements = await page.locator(selector).all();
|
|
325
|
+
const result: string[] = [];
|
|
326
|
+
for (const el of elements) {
|
|
327
|
+
const attr = await el.getAttribute('data-explorbot-eidx');
|
|
328
|
+
if (attr) result.push(attr);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (this.isFatalBrowserError(error)) {
|
|
333
|
+
tag('warning').log(`getEidxInContainer: ${error instanceof Error ? error.message : error}`);
|
|
334
|
+
await this.recoverFromBrowserError();
|
|
335
|
+
}
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getEidxByLocator(locator: string, container?: string | null): Promise<string | null> {
|
|
341
|
+
try {
|
|
342
|
+
const page = this.playwrightHelper.page;
|
|
343
|
+
const base = container ? page.locator(container) : page;
|
|
344
|
+
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
345
|
+
return await el.first().getAttribute('data-explorbot-eidx');
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (this.isFatalBrowserError(error)) {
|
|
348
|
+
tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
|
|
349
|
+
await this.recoverFromBrowserError();
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async reload() {
|
|
356
|
+
await this.closeOtherTabs();
|
|
357
|
+
await this.playwrightHelper.page.reload();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
isFatalBrowserError(error: unknown): boolean {
|
|
361
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
362
|
+
return FATAL_BROWSER_ERRORS.test(msg);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async recoverFromBrowserError(): Promise<boolean> {
|
|
366
|
+
try {
|
|
367
|
+
const url = this.stateManager.getCurrentState()?.url;
|
|
368
|
+
if (url) {
|
|
369
|
+
tag('warning').log(`Browser error detected, recovering by navigating to ${url}`);
|
|
370
|
+
await this.playwrightHelper.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
tag('warning').log('Browser error detected, reloading page');
|
|
374
|
+
await this.playwrightHelper.page.reload({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
375
|
+
return true;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
tag('error').log(`Browser recovery failed: ${err instanceof Error ? err.message : err}`);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async switchToMainFrame() {
|
|
383
|
+
if (this.playwrightHelper.frame) {
|
|
384
|
+
debugLog('Switching to main frame');
|
|
385
|
+
await this.playwrightHelper.switchTo();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async isInsideIframe(): Promise<boolean> {
|
|
390
|
+
if (this.playwrightHelper.frame) return true;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const page = this.playwrightHelper.page;
|
|
394
|
+
if (!page) return false;
|
|
395
|
+
return await page.evaluate(() => window.top !== window.self);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (this.isFatalBrowserError(error)) {
|
|
398
|
+
tag('warning').log(`isInsideIframe: ${error instanceof Error ? error.message : error}`);
|
|
399
|
+
await this.recoverFromBrowserError();
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getCurrentIframeInfo(): string | null {
|
|
406
|
+
if (!this.playwrightHelper?.frame) return null;
|
|
407
|
+
return 'iframe context active';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
hasOtherTabs(): boolean {
|
|
411
|
+
return this.otherTabs.length > 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getOtherTabsInfo(): TabInfo[] {
|
|
415
|
+
return [...this.otherTabs];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
clearOtherTabsInfo(): void {
|
|
419
|
+
this.otherTabs = [];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
setUserResolve(userResolveFn: UserResolveFunction): void {
|
|
423
|
+
this.userResolveFn = userResolveFn;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private listenToStateChanged(): void {
|
|
427
|
+
if (!this.playwrightHelper) {
|
|
428
|
+
debugLog('Playwright helper not available for state monitoring');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const page = this.playwrightHelper.page;
|
|
434
|
+
if (!page) {
|
|
435
|
+
debugLog('Playwright page not available for state monitoring');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const initialPage = page;
|
|
439
|
+
const context = this.playwrightHelper.page.context();
|
|
440
|
+
|
|
441
|
+
context.on('page', async (newPage: any) => {
|
|
442
|
+
if (newPage === initialPage) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
if (newPage.url() === 'about:blank') {
|
|
448
|
+
await newPage.waitForURL(/^(?!about:blank$)/, { timeout: 5000 }).catch(() => {});
|
|
449
|
+
}
|
|
450
|
+
await newPage.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
451
|
+
const url = await newPage.url();
|
|
452
|
+
const title = await newPage.title().catch(() => 'Unknown');
|
|
453
|
+
|
|
454
|
+
this.otherTabs.push({ url, title });
|
|
455
|
+
if (url !== 'about:blank') {
|
|
456
|
+
tag('info').log(`New browser tab opened: ${url}`);
|
|
457
|
+
}
|
|
458
|
+
debugLog(`New tab detected: ${url} - ${title}`);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
debugLog('Failed to get new tab info:', error);
|
|
461
|
+
this.otherTabs.push({ url: 'unknown', title: 'unknown' });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
this.bindFrameNavigated(page);
|
|
466
|
+
|
|
467
|
+
debugLog('Listening for automatic state changes');
|
|
468
|
+
} catch (error) {
|
|
469
|
+
debugLog('Failed to set up state change monitoring:', error);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async stop(): Promise<void> {
|
|
474
|
+
if (!this.isStarted) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (this.xhrCapture && this.playwrightHelper?.page) {
|
|
479
|
+
this.xhrCapture.detach(this.playwrightHelper.page);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
483
|
+
const dir = path.dirname(this.options.session);
|
|
484
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
485
|
+
await this.playwrightHelper.browserContext.storageState({ path: this.options.session });
|
|
486
|
+
debugLog(`Session saved to ${path.relative(process.cwd(), this.options.session)}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
codeceptjs.event.dispatcher.emit('global.after');
|
|
490
|
+
codeceptjs.event.dispatcher.emit('global.result');
|
|
491
|
+
|
|
492
|
+
if (this.isSharedBrowser) {
|
|
493
|
+
tag('info').log('Closing browser context (persistent browser stays running)');
|
|
494
|
+
try {
|
|
495
|
+
if (this.playwrightHelper.browserContext) {
|
|
496
|
+
await this.playwrightHelper.browserContext.close();
|
|
497
|
+
this.playwrightHelper.browserContext = null;
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
debugLog('Failed to close browser context:', err);
|
|
501
|
+
}
|
|
502
|
+
this.playwrightHelper.browser = null;
|
|
503
|
+
this.playwrightHelper.isRunning = false;
|
|
504
|
+
await Promise.all([this.reporter.finishRun(), codeceptjs.recorder.stop()]);
|
|
505
|
+
} else {
|
|
506
|
+
await Promise.all([this.reporter.finishRun(), this.playwrightHelper._stopBrowser(), codeceptjs.recorder.stop()]);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
get activeTest(): Test | null {
|
|
511
|
+
return this._activeTest;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async startTest(test: Test) {
|
|
515
|
+
this._activeTest = test;
|
|
516
|
+
await this.reporter.reportTestStart(test);
|
|
517
|
+
await this.closeOtherTabs();
|
|
518
|
+
this.otherTabs = [];
|
|
519
|
+
|
|
520
|
+
const codeceptjsTest = toCodeceptjsTest(test);
|
|
521
|
+
|
|
522
|
+
const stepHandler = (step: any, status?: string, error?: string, log?: string) => {
|
|
523
|
+
if (!step.toCode) return;
|
|
524
|
+
if (step?.name?.startsWith('grab')) return;
|
|
525
|
+
if (step?.name?.startsWith('save')) return;
|
|
526
|
+
|
|
527
|
+
test.addStep(step.toCode(), step.duration, status, error, log);
|
|
528
|
+
|
|
529
|
+
if (!this.stateManager.getCurrentState()) return;
|
|
530
|
+
|
|
531
|
+
const lastScreenshot = ActionResult.fromState(this.stateManager.getCurrentState()!).screenshotFile;
|
|
532
|
+
if (!lastScreenshot) return;
|
|
533
|
+
|
|
534
|
+
const screenshotPath = outputPath('states', lastScreenshot);
|
|
535
|
+
test.addArtifact(screenshotPath);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const dialogHandler = (dialog: any) => {
|
|
539
|
+
const dialogType = dialog.type();
|
|
540
|
+
const dialogMessage = dialog.message();
|
|
541
|
+
test.addNote(`Native dialog ${dialogType} appeared: ${dialogMessage}. Accepted automatically`);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
this.playwrightHelper?.page?.on('dialog', dialogHandler);
|
|
545
|
+
|
|
546
|
+
codeceptjs.event.dispatcher.emit('test.before', codeceptjsTest);
|
|
547
|
+
codeceptjs.event.dispatcher.emit('test.start', codeceptjsTest);
|
|
548
|
+
codeceptjs.event.dispatcher.on('step.passed', (step: any) => stepHandler(step, 'passed'));
|
|
549
|
+
codeceptjs.event.dispatcher.on('step.failed', (step: any, error: any) => {
|
|
550
|
+
stepHandler(step, 'failed', error?.message || String(error), error?.stack);
|
|
551
|
+
});
|
|
552
|
+
codeceptjs.event.dispatcher.on('test.after', () => {
|
|
553
|
+
codeceptjs.event.dispatcher.off('step.passed', stepHandler);
|
|
554
|
+
codeceptjs.event.dispatcher.off('step.failed', stepHandler);
|
|
555
|
+
this.playwrightHelper?.page?.off('dialog', dialogHandler);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async stopTest(test: Test, meta?: Record<string, string>) {
|
|
560
|
+
this._activeTest = null;
|
|
561
|
+
const lastScreenshot = this.stateManager.getCurrentState()?.screenshotFile;
|
|
562
|
+
if (lastScreenshot) {
|
|
563
|
+
meta ||= {};
|
|
564
|
+
meta.screenshotFile = lastScreenshot;
|
|
565
|
+
}
|
|
566
|
+
await this.reporter.reportTest(test, meta);
|
|
567
|
+
const codeceptjsTest = toCodeceptjsTest(test);
|
|
568
|
+
|
|
569
|
+
if (test.isSuccessful) {
|
|
570
|
+
codeceptjsTest.state = 'passed';
|
|
571
|
+
codeceptjs.event.dispatcher.emit('test.passed', codeceptjsTest);
|
|
572
|
+
} else if (test.isSkipped) {
|
|
573
|
+
codeceptjsTest.state = 'skipped';
|
|
574
|
+
codeceptjs.event.dispatcher.emit('test.skipped', codeceptjsTest);
|
|
575
|
+
} else {
|
|
576
|
+
codeceptjsTest.state = 'failed';
|
|
577
|
+
codeceptjs.event.dispatcher.emit('test.failed', codeceptjsTest);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
codeceptjs.event.dispatcher.emit('test.finish', codeceptjsTest);
|
|
581
|
+
codeceptjs.event.dispatcher.emit('test.after', codeceptjsTest);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async hasPlaywrightLocator(locatorFn: (page: any) => any, opts: { multiple?: boolean; contents?: boolean; success?: (locator: any) => Promise<void> | void } = {}): Promise<boolean> {
|
|
585
|
+
try {
|
|
586
|
+
const pwLocator = locatorFn(this.playwrightHelper.page);
|
|
587
|
+
const count = await pwLocator.count();
|
|
588
|
+
if (opts.multiple ? count === 0 : count !== 1) return false;
|
|
589
|
+
if (opts.contents) {
|
|
590
|
+
const html = await pwLocator.first().innerHTML();
|
|
591
|
+
if (!html?.trim()) return false;
|
|
592
|
+
}
|
|
593
|
+
if (opts.success) await opts.success(pwLocator);
|
|
594
|
+
return true;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (this.isFatalBrowserError(error)) {
|
|
597
|
+
tag('warning').log(`hasPlaywrightLocator: ${error instanceof Error ? error.message : error}`);
|
|
598
|
+
await this.recoverFromBrowserError();
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async playwrightLocatorCount(locatorFn: (page: any) => any): Promise<number> {
|
|
605
|
+
try {
|
|
606
|
+
const pwLocator = locatorFn(this.playwrightHelper.page);
|
|
607
|
+
return await pwLocator.count();
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (this.isFatalBrowserError(error)) {
|
|
610
|
+
tag('warning').log(`playwrightLocatorCount: ${error instanceof Error ? error.message : error}`);
|
|
611
|
+
await this.recoverFromBrowserError();
|
|
612
|
+
}
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private bindFrameNavigated(page: any): void {
|
|
618
|
+
page.on('framenavigated', async (frame: any) => {
|
|
619
|
+
if (frame !== page.mainFrame()) return;
|
|
620
|
+
|
|
621
|
+
const newUrl = await frame.url();
|
|
622
|
+
let newTitle = '';
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
newTitle = await frame.title();
|
|
626
|
+
} catch (error) {
|
|
627
|
+
debugLog('Failed to get page title:', error);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.stateManager.updateStateFromBasic(newUrl, newTitle, 'navigation');
|
|
631
|
+
|
|
632
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async openFreshTab(): Promise<void> {
|
|
637
|
+
if (!this.playwrightHelper?.page) return;
|
|
638
|
+
|
|
639
|
+
const oldPage = this.playwrightHelper.page;
|
|
640
|
+
const context = oldPage.context();
|
|
641
|
+
const newPage = await context.newPage();
|
|
642
|
+
|
|
643
|
+
await oldPage.close();
|
|
644
|
+
await newPage.bringToFront();
|
|
645
|
+
|
|
646
|
+
this.playwrightHelper.page = newPage;
|
|
647
|
+
this.otherTabs = [];
|
|
648
|
+
|
|
649
|
+
this.bindFrameNavigated(newPage);
|
|
650
|
+
if (this.xhrCapture) {
|
|
651
|
+
this.xhrCapture.attach(newPage);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
debugLog('Opened fresh tab, closed previous tab');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private async closeOtherTabs(): Promise<void> {
|
|
658
|
+
if (!this.playwrightHelper) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const context = this.playwrightHelper.page.context();
|
|
663
|
+
const pages = context.pages();
|
|
664
|
+
|
|
665
|
+
if (pages.length <= 1) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
debugLog(`Found ${pages.length} tabs, cleaning up to keep only the first one`);
|
|
670
|
+
|
|
671
|
+
const firstPage = pages[0];
|
|
672
|
+
const tabsToClose = pages.slice(1);
|
|
673
|
+
|
|
674
|
+
for (const page of tabsToClose) {
|
|
675
|
+
await page.close();
|
|
676
|
+
debugLog(`Closed extra tab: ${await page.url()}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await firstPage.bringToFront();
|
|
680
|
+
|
|
681
|
+
this.playwrightHelper.page = firstPage;
|
|
682
|
+
|
|
683
|
+
debugLog(`Cleaned up tabs, now focused on: ${await firstPage.url()}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function toCodeceptjsTest(test: Test): any {
|
|
688
|
+
const parent = {
|
|
689
|
+
title: 'Auto-Explorotary Testing',
|
|
690
|
+
fullTitle: () => 'Auto-Explorotary Testing',
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const codeceptjsTest = createTest(test.scenario, () => {});
|
|
694
|
+
codeceptjsTest.parent = parent;
|
|
695
|
+
codeceptjsTest.fullTitle = () => `${parent.title} ${test.scenario}`;
|
|
696
|
+
codeceptjsTest.state = 'pending';
|
|
697
|
+
codeceptjsTest.notes = test.getPrintableNotes();
|
|
698
|
+
return codeceptjsTest;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
702
|
+
|
|
703
|
+
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
704
|
+
|
|
705
|
+
function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string; ref: string }> {
|
|
706
|
+
const entries: Array<{ role: string; name: string; ref: string }> = [];
|
|
707
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
708
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
709
|
+
if (!match) continue;
|
|
710
|
+
if (!ANNOTATABLE_ROLES.has(match[2])) continue;
|
|
711
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
712
|
+
}
|
|
713
|
+
return entries;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
717
|
+
const ariaSnapshot: string = await page.locator('body').ariaSnapshot({ forAI: true });
|
|
718
|
+
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
719
|
+
|
|
720
|
+
const byRole = new Map<string, Array<{ name: string; ref: string }>>();
|
|
721
|
+
for (const { role, name, ref } of refEntries) {
|
|
722
|
+
let list = byRole.get(role);
|
|
723
|
+
if (!list) {
|
|
724
|
+
list = [];
|
|
725
|
+
byRole.set(role, list);
|
|
726
|
+
}
|
|
727
|
+
list.push({ name, ref });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const elements: WebElement[] = [];
|
|
731
|
+
for (const [role, entries] of byRole) {
|
|
732
|
+
try {
|
|
733
|
+
const rawList = await page.getByRole(role).evaluateAll(
|
|
734
|
+
(domElements: Element[], [data, extractFnStr]: [Array<{ name: string; ref: string }>, string]) => {
|
|
735
|
+
const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
|
|
736
|
+
const results: any[] = [];
|
|
737
|
+
let ariaIdx = 0;
|
|
738
|
+
for (const el of domElements) {
|
|
739
|
+
if (ariaIdx >= data.length) break;
|
|
740
|
+
el.setAttribute('data-explorbot-eidx', data[ariaIdx].ref);
|
|
741
|
+
const elData = extract(el);
|
|
742
|
+
if (elData) results.push(elData);
|
|
743
|
+
ariaIdx++;
|
|
744
|
+
}
|
|
745
|
+
return results;
|
|
746
|
+
},
|
|
747
|
+
[entries, extractElementData.toString()]
|
|
748
|
+
);
|
|
749
|
+
for (const raw of rawList) {
|
|
750
|
+
elements.push(WebElement.fromRawData(raw, role));
|
|
751
|
+
}
|
|
752
|
+
} catch {
|
|
753
|
+
debugLog(`Failed to annotate role=${role}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return { ariaSnapshot, elements };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export default Explorer;
|