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)
What are you trying to achieve?
I.see(text, context)should pass as soon as one of the elements matched bycontextcontains 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
timeoutand then fails with: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 inelToString(), so this path is still affected onmain.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 (ourtimeout). By the timeretryFailedStepretried it, the new 5 s toast was gone as well, so the test failed although the text had been on the page.Cause
proceedSee()inlib/helper/Playwright.js:_locate()returnslocator(...).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()andinnerText(), thenth(i)locator no longer resolves, soinnerText()waits for the page default timeout (set from the helper'stimeout) and throws. One removed element fails the wholePromise.all, even when another element holds the expected text.Steps to reproduce
codecept.conf.js:repro_test.js:The "new toast" element is on the page the whole time, so every
I.seeshould 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
mainafter #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: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 testedI.see(text, context).Environment info
Created by AI agent (Claude Code)