Repository navigation
Playwright: I.see(text, context) blocks for the full timeout when one of the matched elements is removed #5745
Description
What are you trying to achieve?
I.see(text, context) should pass as soon as one of the elements matched by context contains the text, even if another matched element is removed from the DOM while the step runs.
What do you get instead?
If one of the matched elements is removed at the wrong moment, the step blocks for the full helper timeout and then fails with:
innerText: Timeout 3000ms exceeded.
- waiting for locator('.toast').nth(<index of the removed element>)
This is the same mechanism as #5739, but in a different place, and it does not need --verbose / --debug. The fix for #5739 (#5740) only covers the debug preview in elToString(), so this path is still affected on main.
We hit this on CI with toast messages. Two toasts were on the page: an old one about to auto-close and a new one the test was checking. I.see('…', '.Toastify__toast-body') matched both, the old toast was removed during the step, and the step blocked for 30 s (our timeout). By the time retryFailedStep retried it, the new 5 s toast was gone as well, so the test failed although the text had been on the page.
Cause
proceedSee() in lib/helper/Playwright.js:
const els = await this._locate(locator) // locator(...).all()
assertElementExists(els, locator.toString())
allText = await Promise.all(els.map(el => el.innerText()))_locate() returns locator(...).all(), i.e. Playwright Locators (nth(i)), not element handles. Locator.innerText() auto-waits for the element to be attached. When an element is removed between .all() and innerText(), the nth(i) locator no longer resolves, so innerText() waits for the page default timeout (set from the helper's timeout) and throws. One removed element fails the whole Promise.all, even when another element holds the expected text.
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('see stall')
Scenario('one of two matched elements removed between all() and innerText()', async ({ I }) => {
I.executeScript(() => {
const keep = document.createElement('div')
keep.className = 'toast'
keep.textContent = 'new toast'
document.body.append(keep)
})
let stalls = 0
for (let i = 0; i < 200; i++) {
I.executeScript(() => {
const el = document.createElement('div')
el.className = 'toast'
el.textContent = 'old toast'
document.body.append(el)
setTimeout(() => el.remove(), Math.random() * 5)
})
const start = Date.now()
await tryTo(() => I.see('new toast', '.toast'))
const ms = Date.now() - start
if (ms > 1000) {
stalls++
I.say(`STALL attempt ${i}: see took ${ms}ms`)
}
}
I.say(`stalls: ${stalls}/200`)
})The "new toast" element is on the page the whole time, so every I.see should pass immediately.
Result of bunx --bun codeceptjs run --steps (no --verbose, no --debug): 31 / 200 steps stalled for 3 s each, and the run took 1.5 min.
Affected versions
Reproduced on 4.2.0. The same line is still on main after #5740.
Possible fix
Read the texts without auto-waiting per element, for example with one call on the parent locator (locator.allInnerTexts() / evaluateAll()), or make each read tolerant of a removed element:
allText = (await Promise.all(els.map(el => el.innerText({ timeout: 100 }).catch(() => null)))).filter(t => t !== null)Other helper methods use the same Promise.all(els.map(el => el.<autoWaitingCall>())) pattern on the result of _locate() (isVisible() is not affected, it doesn't wait; inputValue(), innerHTML(), evaluate(), isChecked() look like they are). I have only tested I.see(text, context).
Environment info
- CodeceptJS: 4.2.0
- Playwright: 1.63.0 (Chromium 153)
- Bun 1.4.2
- Operating System: Linux
- Playwright helper
Created by AI agent (Claude Code)
Activity
fix(playwright): don't pass see() checks on vanished or non-HTML elem…
mirao commentedon Oct 11, 2026
I ran the repro from this issue against the fix in #5746 (200 iterations,
timeout: 3000, no--verbose/--debug):I.see4.x(a459969, no fix)Two differences from the repro as posted:
node bin/codecept.js run --steps), not Bun.tryToreturningfalse, so the table also shows that everyI.seepassed with the fix, not only that none stalled.Environment: Playwright with Chromium 153, Linux.
Created by AI agent (Claude Code)