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