Skip to content

Before hook playwright script#8

Closed
IovetsNikolay wants to merge 2 commits intotestomatio:mainfrom
IovetsNikolay:before-hook-playwright-script
Closed

Before hook playwright script#8
IovetsNikolay wants to merge 2 commits intotestomatio:mainfrom
IovetsNikolay:before-hook-playwright-script

Conversation

@IovetsNikolay
Copy link
Contributor

@IovetsNikolay IovetsNikolay commented Feb 5, 2026

User description

Adding before hook script with Playwright API called after navigation, before capturing the page state. Allows to execute general prepare state preconditions as resolve login page redirect, removing the cookie banner, API login, etc..

Execution diagram:
Action.execute(I.amOnPage(...))
  ├─> Navigation starts
  ├─> recorder.promise() waits for navigation
  ├─> Navigation completes
  ├─> framenavigated event fires ────────┐
  ├─> WAIT for pendingSetupScripts ──────┤
  │                                       ▼
  │                              Setup scripts run:
  │                              ├─> Dismiss cookie banner
  │                              └─> Fill login form & submit
  │                                       │
  ├─< Setup scripts complete ◄───────────┘
  │
  └─> capturePageState()
      (Now captures AFTER login completes!)

Scripts can be placed in "browser-scripts" folder. Here is an example of a simple script:

 export async function setup({ page, log }) {
  await page.waitForLoadState('networkidle');
  const isCookieBarPresent = await page.locator('baner').isVisible();

  if (isCookieBarPresent) {
    try {
      await page.locator('[reject').click({ timeout: 2000 });
      await page.locator('baner').waitFor({ state: 'hidden', timeout: 5000 });
      log('Cookie banner dismissed');
    } catch {
      log('Cookie banner not found or already dismissed');
    }
  }

  if (!page.url().includes('/SignIn') && !page.url().includes('/login')) {
    return;
  }

  const isEmailInputPresent = await page.locator('email-input').isVisible();

  if (!isEmailInputPresent) {
    return;
  }

  try {
    log('Sign-in form detected, filling email');
    await page.locator('email-input').fill(process.env.EMAIL);
    await page.locator('confirm-btn').click();

    log('Login complete, redirected successfully');
  } catch (error) {
    log('Auto-login failed:', error.message);
  }
}


CodeAnt-AI Description

Run optional Playwright setup scripts after navigation to prepare page state

What Changed

  • New config option to list setup scripts that run after each navigation so pages can be prepared (examples: dismiss cookie banners, complete sign-in redirects, perform API logins).
  • The explorer loads configured scripts from disk and executes them after navigation and before capturing the page state or continuing actions.
  • The explorer waits for those scripts to finish, ensuring captured snapshots and subsequent actions reflect the post-setup page state rather than transient pre-login/consent UI.

Impact

✅ Captures reflect post-login and post-consent
✅ Fewer flaky captures from cookie banners or navigation redirects
✅ Less manual setup needed to reproduce state-dependent issues

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Add PageSetupManager to execute setup scripts after navigation.
Update .gitignore to exclude explorbot.config.js user config.
@codeant-ai
Copy link

codeant-ai bot commented Feb 5, 2026

CodeAnt AI is reviewing your PR.

@codeant-ai codeant-ai bot added the size:L This PR changes 100-499 lines, ignoring generated files label Feb 5, 2026
@codeant-ai
Copy link

codeant-ai bot commented Feb 5, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Untrusted Script Execution
    The code dynamically imports and executes external setup scripts without sandboxing or validation. Those scripts run with full process privileges and can access environment variables, filesystem, network, etc. This can lead to execution of arbitrary/untrusted code if script paths are misconfigured or supplied by an attacker.

  • Execution Risk
    Adding setupScripts enables loading and running user-provided scripts before capturing page state. Running arbitrary scripts can execute code with filesystem and environment access (secrets, network, spawning processes). Verify how scripts are loaded/executed, enforce directory restrictions, and consider sandboxing or running scripts in a separate process.

  • Import Path Resolution
    The dynamic import uses the result of resolve(scriptPath) directly. Depending on ESM loader semantics and whether scriptPath is relative or absolute, Node's dynamic import may fail or import the wrong file (or require a file:// URL). Also resolution is relative to process working directory which may be different from project/config location.

  • No Script Execution Timeout
    Setup scripts are awaited without any timeout. A misbehaving or long-running setup script can hang navigation processing and block captures indefinitely. There is no per-script timeout or cancellation mechanism.

  • Potential hang / missing timeout
    The code awaits this.explorer.waitForPendingSetupScripts() with no timeout or error handling. If that promise never resolves or throws, execute can hang or fail the whole action flow. Add a timeout and/or catch errors so a flaky setup script can't block capturePageState.

await recorder.add(() => sleep(this.config.action?.delay || 500)); // wait for the action to be executed
await recorder.promise();

if (this.explorer?.waitForPendingSetupScripts) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The call to waitForPendingSetupScripts on the loosely-typed explorer object only checks that the property exists, not that it is actually a function, so if a non-conforming object is ever passed in as explorer (for example via a misconfigured caller), this will throw a TypeError: this.explorer.waitForPendingSetupScripts is not a function at runtime and break the whole action execution. [possible bug]

Severity Level: Major ⚠️
- ❌ Action.execute may crash on misconfigured explorer.
- ❌ capturePageState() not reached after action.
- ⚠️ Before-hook setup scripts silently not awaited.
- ⚠️ Test flows using manual Action construction affected.
Suggested change
if (this.explorer?.waitForPendingSetupScripts) {
if (this.explorer && typeof this.explorer.waitForPendingSetupScripts === 'function') {
Steps of Reproduction ✅
1. Construct an Action with a non-function explorer value:

   - In any script or test instantiate Action as `new Action(actor, stateManager,
   explorer)` where the constructor is defined at `src/action.ts:41-48`
   (`constructor(actor: CodeceptJS.I, stateManager: StateManager, explorer?: any)`).

2. Pass a mis-shaped explorer object:

   - Provide `explorer = { waitForPendingSetupScripts: true }` (boolean) or any
   non-callable value for that property when creating the Action instance.

3. Execute an action that runs the normal execute flow:

   - Call `await action.execute("I.click('selector')")`. The `execute` function flow is in
   `src/action.ts` around lines 186-206 (where `codeFunction` is invoked, `recorder.add()`
   and `recorder.promise()` are awaited).

4. Observe the runtime TypeError immediately after navigation wait:

   - After `await recorder.promise()` completes, the code at `src/action.ts:199-201` runs:

     - `199 if (this.explorer?.waitForPendingSetupScripts) {`

     - `200 await this.explorer.waitForPendingSetupScripts();`

     - `201 }`

   - Because `this.explorer.waitForPendingSetupScripts` is not a function (it's boolean),
   awaiting it throws `TypeError: this.explorer.waitForPendingSetupScripts is not a
   function`.

   - The TypeError prevents the subsequent `capturePageState()` call at
   `src/action.ts:203` and aborts the action.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/action.ts
**Line:** 199:199
**Comment:**
	*Possible Bug: The call to `waitForPendingSetupScripts` on the loosely-typed `explorer` object only checks that the property exists, not that it is actually a function, so if a non-conforming object is ever passed in as `explorer` (for example via a misconfigured caller), this will throw a `TypeError: this.explorer.waitForPendingSetupScripts is not a function` at runtime and break the whole action execution.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

await action.execute(`I.waitForElement(${JSON.stringify(waitForElement)})`);
}

await this.pageSetupManager.executeAfterNavigation(this.playwrightHelper.page, url);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: After adding the framenavigated listener that already invokes executeAfterNavigation, the explicit call to pageSetupManager.executeAfterNavigation inside visit() causes setup scripts to run twice for navigations triggered via visit(), which can double-submit forms or perform other non-idempotent actions and lead to inconsistent browser state. Instead, visit() should only wait for any pending setup started by the navigation listener. [logic error]

Severity Level: Critical 🚨
- ❌ Duplicate setup runs cause double form submissions.
- ❌ Cookie-banner dismissal may run twice unexpectedly.
- ⚠️ Explorer.visit flows may produce flaky captures.
Suggested change
await this.pageSetupManager.executeAfterNavigation(this.playwrightHelper.page, url);
await this.waitForPendingSetupScripts();
Steps of Reproduction ✅
1. Call Explorer.visit(url) (src/explorer.ts visit() function, see the line that executes
await this.pageSetupManager.executeAfterNavigation(this.playwrightHelper.page, url) at
line 240). visit() issues I.amOnPage(...) which triggers navigation.

2. The page navigation emits Playwright's 'framenavigated' event; the listener registered
in listenToStateChanged (src/explorer.ts framenavigated handler at lines ~328-337) also
calls pageSetupManager.executeAfterNavigation(...) for the same navigation.

3. Because visit() calls executeAfterNavigation directly (line 240) while the
framenavigated handler also calls it, the same setup scripts run twice for a single
navigation (PageSetupManager.executeAfterNavigation is invoked from both locations).

4. Observe duplicated side effects when running a non-idempotent setup script (e.g.,
browser-scripts that fill-and-submit login forms or click cookie banners) leading to
double submits or unexpected page state. The duplicate invocation originates from the two
call sites above.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/explorer.ts
**Line:** 240:240
**Comment:**
	*Logic Error: After adding the `framenavigated` listener that already invokes `executeAfterNavigation`, the explicit call to `pageSetupManager.executeAfterNavigation` inside `visit()` causes setup scripts to run twice for navigations triggered via `visit()`, which can double-submit forms or perform other non-idempotent actions and lead to inconsistent browser state. Instead, `visit()` should only wait for any pending setup started by the navigation listener.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +335 to +337
this.pendingSetupScripts = this.pageSetupManager.executeAfterNavigation(page, newUrl);
await this.pendingSetupScripts;
this.pendingSetupScripts = null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Using the shared pendingSetupScripts field directly for both storing and awaiting the promise in the framenavigated handler creates a race condition when multiple navigations happen quickly: later events overwrite the field before earlier awaits complete, so earlier handlers may end up awaiting the wrong promise and one or more setup runs can be skipped or left untracked. Capturing the promise in a local variable and only clearing the field if it still matches that promise also ensures the field is reset correctly even if the setup throws. [race condition]

Severity Level: Major ⚠️
- ❌ Page setup scripts tracking becomes unreliable.
- ❌ Auto-login or cookie dismissal may be skipped.
- ⚠️ capturePageState may capture preconditioned pages incorrectly.
Suggested change
this.pendingSetupScripts = this.pageSetupManager.executeAfterNavigation(page, newUrl);
await this.pendingSetupScripts;
this.pendingSetupScripts = null;
const setupPromise = this.pageSetupManager.executeAfterNavigation(page, newUrl);
this.pendingSetupScripts = setupPromise;
try {
await setupPromise;
} finally {
if (this.pendingSetupScripts === setupPromise) {
this.pendingSetupScripts = null;
}
}
Steps of Reproduction ✅
1. Start the explorer by calling Explorer.start() so listeners are installed (see
src/explorer.ts around the start() method where it calls await
this.pageSetupManager.loadSetupScripts(); and this.listenToStateChanged()). This
initializes Playwright and registers the framenavigated handler in listenToStateChanged.

2. Trigger two navigations quickly (e.g. call explorer.visit(urlA) then immediately
explorer.visit(urlB) from the same process). visit() performs navigation (src/explorer.ts
visit() function, the I.amOnPage call) which causes Playwright to emit 'framenavigated'.

3. Each framenavigated event enters the handler defined in listenToStateChanged
(src/explorer.ts at the framenavigated listener, lines shown at 328-337). The handler runs
the snippet that assigns this.pendingSetupScripts =
this.pageSetupManager.executeAfterNavigation(...); then awaits it (lines 335-337).

4. Because the code stores the promise in the shared field and an immediately subsequent
framenavigated overwrites it, the first handler's await may end up awaiting the later
promise and the finally clearing (this.pendingSetupScripts = null) may clear the field
incorrectly. Result: one setup run is left untracked or awaited incorrectly, causing setup
scripts (login/cookie dismissal) to be skipped or race between runs.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/explorer.ts
**Line:** 335:337
**Comment:**
	*Race Condition: Using the shared `pendingSetupScripts` field directly for both storing and awaiting the promise in the `framenavigated` handler creates a race condition when multiple navigations happen quickly: later events overwrite the field before earlier awaits complete, so earlier handlers may end up awaiting the wrong promise and one or more setup runs can be skipped or left untracked. Capturing the promise in a local variable and only clearing the field if it still matches that promise also ensures the field is reset correctly even if the setup throws.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

}

async executeAfterNavigation(page: any, url: string): Promise<void> {
if (this.scripts.length === 0) return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The executeAfterNavigation method assumes that loadSetupScripts has already been called and completed, so if callers forget to load scripts or don't await the loader, this.scripts remains empty and setup scripts silently never run after navigation. Because loadSetupScripts is asynchronous and guarded by isLoaded, it is safe and more robust to call it from executeAfterNavigation itself so scripts are always loaded before execution. [possible bug]

Severity Level: Major ⚠️
- ❌ Post-navigation setup scripts silently never execute.
- ⚠️ Cookie banners remain and block interactions.
- ⚠️ Auto-login/setup flows don't run before capture.
- ⚠️ Explorer framenavigated handler continues without preconditions.
Suggested change
if (this.scripts.length === 0) return;
await this.loadSetupScripts();
Steps of Reproduction ✅
1. Configure Playwright setup scripts in runtime config (Explorbot config passed into
Explorer). The PageSetupManager expects script paths to come from
config.playwright.setupScripts (see src/page-setup-manager.ts: lines defining
loadSetupScripts at lines 36-71 in the PR hunk).

2. Start the Explorer and navigate pages so the Playwright page emits frame navigation. In
src/explorer.ts (file excerpt in Additional Downstream Code Context, within the page
'framenavigated' handler) the code calls:

   this.pendingSetupScripts = this.pageSetupManager.executeAfterNavigation(page, newUrl);

   await this.pendingSetupScripts;

   (this is the only invocation path that runs setup scripts after navigation).

3. Execution enters PageSetupManager.executeAfterNavigation at src/page-setup-manager.ts
lines 73-87. That function immediately returns when this.scripts.length === 0 (line 74),
but loadSetupScripts() is never invoked here and there is no other guaranteed call site in
the shown codebase that loads scripts prior to first navigation.

4. Observe behavior: despite having configured setup script paths, setup code never runs
after navigation (no cookie dismissal, no auto-login). The check at
src/page-setup-manager.ts line 74 causes silent early return because this.scripts is still
empty (loadSetupScripts was not awaited or invoked). This reproduces reliably whenever
setupScripts are configured but loadSetupScripts hasn't been called before the first frame
navigation.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/page-setup-manager.ts
**Line:** 74:74
**Comment:**
	*Possible Bug: The `executeAfterNavigation` method assumes that `loadSetupScripts` has already been called and completed, so if callers forget to load scripts or don't await the loader, `this.scripts` remains empty and setup scripts silently never run after navigation. Because `loadSetupScripts` is asynchronous and guarded by `isLoaded`, it is safe and more robust to call it from `executeAfterNavigation` itself so scripts are always loaded before execution.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai
Copy link

codeant-ai bot commented Feb 5, 2026

CodeAnt AI finished reviewing your PR.

Copy link
Contributor

@DavertMik DavertMik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid purpose and good idea but implementation contradicts current architecture

I will add an alternative option for it

this.experienceTracker = stateManager.getExperienceTracker();
this.config = ConfigParser.getInstance().getConfig();
this.playwrightHelper = container.helpers('Playwright');
this.explorer = explorer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this breaks dependency hierarchy

action and managed by explorer, not vice versa

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants