Repository navigation
Playwright: debug logging in _locate() blocks for the full timeout when the element is removed (--verbose/--debug) #5739
Description
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
fix(playwright): don't block on debug element preview when element is…