Skip to content

Playwright: debug logging in _locate() blocks for the full timeout when the element is removed (--verbose/--debug) #5739

Closed

Description

@mirao

What are you trying to achieve?

I.dontSeeElement() (and anything else built on _locate()) should return in milliseconds when the element disappears, whether or not --verbose / --debug is on.

What do you get instead?

With --verbose or --debug, if the matched element is removed from the DOM at the wrong moment, the step blocks for the full helper timeout and then fails with:

TimeoutError: evaluate: Timeout 3000ms exceeded.

Without --verbose the same test never stalls.

We hit this on CI: a modal closed during a retried I.dontSeeElement('.modal'), the step blocked for 30 s (our timeout), and a 5 s success toast shown when the modal closed was gone by the time the next step looked for it.

Cause

Since #4712 (3.7.2), _locate() logs the found elements in debug mode:

const els = await findElements.call(this, context, locator)   // locator(...).all()
if (store.debugMode) {
  const previewElements = els.slice(0, 3)
  let htmls = await Promise.all(previewElements.map(el => elToString(el, previewElements.length)))

and elToString() does await el.evaluate(node => node.outerHTML).

findElements() returns locator(...).all(), i.e. Playwright Locators (nth(i)), not element handles. Locator.evaluate() auto-waits for the element to be attached. When the element is removed between .all() and evaluate(), evaluate() waits for the page default timeout (set from the helper's timeout) and throws.

Steps to reproduce

codecept.conf.js:

export const config = {
  tests: './*_test.js',
  output: './output',
  helpers: { Playwright: { url: 'about:blank', browser: 'chromium', show: false, timeout: 3000 } },
  name: 'repro',
}

repro_test.js:

import { tryTo } from 'codeceptjs/effects'

Feature('dontSeeElement stall')

Scenario('element removed between all() and evaluate()', async ({ I }) => {
  let stalls = 0
  for (let i = 0; i < 200; i++) {
    I.executeScript(() => {
      const el = document.createElement('div')
      el.id = 'modal'
      el.textContent = 'modal'
      document.body.append(el)
      setTimeout(() => el.remove(), Math.random() * 5)
    })
    const start = Date.now()
    await tryTo(() => I.dontSeeElement('#modal'))
    const ms = Date.now() - start
    if (ms > 1000) {
      stalls++
      I.say(`STALL attempt ${i}: dontSeeElement took ${ms}ms`)
    }
  }
  I.say(`stalls: ${stalls}/200`)
})

Results:

Mode Stalls Duration
bunx --bun codeceptjs run --verbose 25 / 200, each TimeoutError: evaluate: Timeout 3000ms exceeded. 1 min
bunx --bun codeceptjs run 0 / 200 243 ms

Affected versions

3.7.2 and later, including 4.2.0. 3.7.1 and older have no debug logging in _locate(), so they don't stall.

Possible fix

Make the debug preview best-effort so it never blocks or fails the step, e.g. a short timeout and a fallback in elToString():

async function elToString(el, numberOfElements) {
  const html = await el.evaluate(node => node.outerHTML, undefined, { timeout: 100 }).catch(() => '<detached>')

or take elementHandle()s for the preview instead of evaluating the Locators.

Environment info

  • CodeceptJS: 4.2.0
  • Playwright: 1.63.0 (Chromium)
  • NodeJS Version: 24.18.0 / Bun 1.4.2
  • Operating System: Linux
  • Playwright helper

Created by AI agent (Claude Code)

Activity

  1. added a commit that references this issue on Oct 7, 2026
    76534ac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions