diff --git a/.gitignore b/.gitignore index 6725f99..abfefaf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,10 @@ dist/ build/ out/ -# Example folder +# App specific folders example/ +browser-scripts/ +knowledge/ # Output directories output/ diff --git a/explorbot.config.example.ts b/explorbot.config.example.ts index bdffe04..6744838 100644 --- a/explorbot.config.example.ts +++ b/explorbot.config.example.ts @@ -19,6 +19,7 @@ interface PlaywrightConfig { height: number; }; args: string[]; + setupScripts?: string[]; } interface AppConfig { diff --git a/src/action.ts b/src/action.ts index 798852b..2767c3c 100644 --- a/src/action.ts +++ b/src/action.ts @@ -36,13 +36,15 @@ class Action { private expectation: string | null = null; public lastError: Error | null = null; public playwrightHelper: any; + private explorer: any; - constructor(actor: CodeceptJS.I, stateManager: StateManager) { + constructor(actor: CodeceptJS.I, stateManager: StateManager, explorer?: any) { this.actor = actor; this.stateManager = stateManager; this.experienceTracker = stateManager.getExperienceTracker(); this.config = ConfigParser.getInstance().getConfig(); this.playwrightHelper = container.helpers('Playwright'); + this.explorer = explorer; } async caputrePageWithScreenshot(): Promise { @@ -194,6 +196,10 @@ class Action { await recorder.add(() => sleep(this.config.action?.delay || 500)); // wait for the action to be executed await recorder.promise(); + if (this.explorer?.waitForPendingSetupScripts) { + await this.explorer.waitForPendingSetupScripts(); + } + const pageState = await this.capturePageState(); if (executedSteps.length > 0) { codeString = executedSteps.join('\n'); diff --git a/src/config.ts b/src/config.ts index 0fec6b5..820aad5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,6 +27,7 @@ interface PlaywrightConfig { height: number; }; args?: string[]; + setupScripts?: string[]; } interface AgentConfig { diff --git a/src/explorer.ts b/src/explorer.ts index e6648b8..95fbca2 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -9,6 +9,7 @@ import type { ExplorbotConfig } from './config.js'; import { ConfigParser } from './config.js'; import type { UserResolveFunction } from './explorbot.js'; import { KnowledgeTracker } from './knowledge-tracker.js'; +import { PageSetupManager } from './page-setup-manager.js'; import { Reporter } from './reporter.ts'; import { StateManager } from './state-manager.js'; import { Test } from './test-plan.ts'; @@ -47,6 +48,8 @@ class Explorer { private options?: { show?: boolean; headless?: boolean; incognito?: boolean }; private reporter!: Reporter; private otherTabs: TabInfo[] = []; + private pageSetupManager!: PageSetupManager; + private pendingSetupScripts: Promise | null = null; constructor(config: ExplorbotConfig, aiProvider: AIProvider, options?: { show?: boolean; headless?: boolean; incognito?: boolean }) { this.config = config; @@ -56,6 +59,7 @@ class Explorer { this.stateManager = new StateManager({ incognito: this.options?.incognito }); this.knowledgeTracker = new KnowledgeTracker(); this.reporter = new Reporter(); + this.pageSetupManager = new PageSetupManager(this, config); } private initializeContainer() { @@ -180,6 +184,9 @@ class Explorer { } await this.playwrightHelper._startBrowser(); await this.playwrightHelper._createContextPage(); + + await this.pageSetupManager.loadSetupScripts(); + const I = codeceptjs.container.support('I'); this.actor = I; @@ -195,7 +202,13 @@ class Explorer { } createAction() { - return new Action(this.actor, this.stateManager); + return new Action(this.actor, this.stateManager, this); + } + + async waitForPendingSetupScripts(): Promise { + if (this.pendingSetupScripts) { + await this.pendingSetupScripts; + } } async visit(url: string) { @@ -224,6 +237,8 @@ class Explorer { await action.execute(`I.waitForElement(${JSON.stringify(waitForElement)})`); } + await this.pageSetupManager.executeAfterNavigation(this.playwrightHelper.page, url); + return action; } @@ -313,10 +328,13 @@ class Explorer { debugLog('Failed to get page title:', error); } - // // Update state from navigation this.stateManager.updateStateFromBasic(newUrl, newTitle, 'navigation'); await new Promise((resolve) => setTimeout(resolve, 500)); + + this.pendingSetupScripts = this.pageSetupManager.executeAfterNavigation(page, newUrl); + await this.pendingSetupScripts; + this.pendingSetupScripts = null; }); debugLog('Listening for automatic state changes'); diff --git a/src/page-setup-manager.ts b/src/page-setup-manager.ts new file mode 100644 index 0000000..48e6618 --- /dev/null +++ b/src/page-setup-manager.ts @@ -0,0 +1,100 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { ExplorbotConfig } from './config.js'; +import type Explorer from './explorer.js'; +import type { KnowledgeTracker } from './knowledge-tracker.js'; +import type { StateManager } from './state-manager.js'; +import { createDebug, tag } from './utils/logger.js'; + +const debugLog = createDebug('explorbot:page-setup'); + +export interface SetupScriptContext { + page: any; + context: any; + explorer: Explorer; + stateManager: StateManager; + knowledgeTracker: KnowledgeTracker; + config: ExplorbotConfig; + log: (...args: any[]) => void; +} + +interface SetupScriptModule { + setup: (context: SetupScriptContext) => Promise; +} + +export class PageSetupManager { + private scripts: SetupScriptModule[] = []; + private explorer: Explorer; + private config: ExplorbotConfig; + private isLoaded = false; + + constructor(explorer: Explorer, config: ExplorbotConfig) { + this.explorer = explorer; + this.config = config; + } + + async loadSetupScripts(): Promise { + if (this.isLoaded) return; + + const scriptPaths = this.config.playwright?.setupScripts || []; + if (scriptPaths.length === 0) { + debugLog('No setup scripts configured'); + this.isLoaded = true; + return; + } + + for (const scriptPath of scriptPaths) { + try { + const fullPath = resolve(scriptPath); + + if (!existsSync(fullPath)) { + tag('warning').log(`Setup script not found: ${scriptPath}`); + continue; + } + + const module = await import(fullPath); + + if (typeof module.setup !== 'function') { + tag('warning').log(`Setup script ${scriptPath} does not export a setup() function`); + continue; + } + + this.scripts.push(module); + debugLog(`Loaded setup script: ${scriptPath}`); + tag('substep').log(`Loaded setup script: ${scriptPath}`); + } catch (error) { + tag('error').log(`Failed to load setup script ${scriptPath}: ${error}`); + } + } + + this.isLoaded = true; + } + + async executeAfterNavigation(page: any, url: string): Promise { + if (this.scripts.length === 0) return; + + debugLog(`Executing ${this.scripts.length} setup scripts after navigation to ${url}`); + + const context = this.createContext(page); + + for (const script of this.scripts) { + try { + await script.setup(context); + } catch (error) { + debugLog(`Setup script error: ${error}`); + } + } + } + + private createContext(page: any): SetupScriptContext { + return { + page, + context: page.context(), + explorer: this.explorer, + stateManager: this.explorer.getStateManager(), + knowledgeTracker: this.explorer.getKnowledgeTracker(), + config: this.config, + log: (...args: any[]) => tag('setup').log(...args), + }; + } +}