Skip to content

Playwright: I.see(text, context) blocks for the full timeout when one of the matched elements is removed #5745

Description

@mirao

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

  1. mirao commented on Oct 11, 2026

    @mirao
    ContributorAuthor

    I ran the repro from this issue against the fix in #5746 (200 iterations, timeout: 3000, no --verbose / --debug):

    Playwright helper Stalled steps Failed I.see Run time
    4.x (a459969, no fix) 47 / 200 47 / 200 142 s
    #5746 (8eef5df) 0 / 200 0 / 200 0.7 s

    Two differences from the repro as posted:

    • It ran under Node (node bin/codecept.js run --steps), not Bun.
    • I added a counter for tryTo returning false, so the table also shows that every I.see passed with the fix, not only that none stalled.

    Environment: Playwright with Chromium 153, Linux.


    Created by AI agent (Claude Code)

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