From ef1e3e043b845d22340e00d0e96c4587b8b6d446 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Sun, 11 Oct 2026 10:58:03 +0200 Subject: [PATCH 1/2] fix(playwright): don't block in see() when a matched element is removed proceedSee() read the text of each element matched by the context via Locator.innerText(), which auto-waits for the element to be attached. If one of the elements was removed between all() and innerText(), the step blocked for the full helper timeout and failed, even when another matched element contained the expected text. Read the texts with evaluateAll(), which doesn't wait, so a removed element contributes no text. Fixes #5745 Co-Authored-By: Claude Opus 5.5 --- lib/helper/Playwright.js | 2 +- test/helper/Playwright_test.js | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index f358a189c..89a5377c2 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -4428,7 +4428,7 @@ async function proceedSee(assertType, text, context, strict = false) { description = `element ${locator.toString()}` const els = await this._locate(locator) assertElementExists(els, locator.toString()) - allText = await Promise.all(els.map(el => el.innerText())) + allText = (await Promise.all(els.map(el => el.evaluateAll(nodes => nodes.map(node => node.innerText ?? ''))))).flat() } if (store?.currentStep?.opts?.ignoreCase === true) { diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index e7fdec3f5..9e1129710 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -730,6 +730,49 @@ describe('Playwright', function () { }) }) + describe('#see with context when a matched element is removed', () => { + let locatorProto + let originalAll + + beforeEach(async () => { + await I.amOnPage('/form/field') + await I.executeScript(() => { + for (const text of ['old toast', 'new toast']) { + const el = document.createElement('div') + el.className = 'toast' + el.textContent = text + document.body.append(el) + } + }) + + locatorProto = Object.getPrototypeOf(page.locator('body')) + originalAll = locatorProto.all + locatorProto.all = async function () { + const els = await originalAll.call(this) + await page.evaluate(() => document.querySelector('.toast')?.remove()) + return els + } + page.setDefaultTimeout(10000) + }) + + afterEach(() => { + locatorProto.all = originalAll + page.setDefaultTimeout(I.options.timeout) + }) + + it('should see text in a remaining element without waiting for timeout', async () => { + const start = Date.now() + await I.see('new toast', '.toast') + expect(Date.now() - start).to.be.below(1000) + }) + + it('should not see text of the removed element without waiting for timeout', async () => { + const start = Date.now() + await I.dontSee('old toast', '.toast') + expect(Date.now() - start).to.be.below(1000) + }) + }) + describe('check fields: #seeInField, #seeCheckboxIsChecked, ...', () => { it('should throw error if field is not empty', () => I.amOnPage('/form/empty') From 8eef5df740e22bda0ac172aa6266008c935e0899 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Sun, 11 Oct 2026 11:37:42 +0200 Subject: [PATCH 2/2] fix(playwright): don't pass see() checks on vanished or non-HTML elements Reading texts with evaluateAll() left two false passes in proceedSee(): - When every matched element was removed after it was located, no text was read, so seeTextEquals() asserted nothing and passed. Report the element as not found instead. - Non-HTML elements such as SVG have no innerText and were read as empty text, so dontSee() passed on text that was on the page. Fall back to textContent. Refs #5745 Co-Authored-By: Claude Opus 5.5 --- lib/helper/Playwright.js | 3 ++- test/helper/Playwright_test.js | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 89a5377c2..fb2037272 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -4428,7 +4428,8 @@ async function proceedSee(assertType, text, context, strict = false) { description = `element ${locator.toString()}` const els = await this._locate(locator) assertElementExists(els, locator.toString()) - allText = (await Promise.all(els.map(el => el.evaluateAll(nodes => nodes.map(node => node.innerText ?? ''))))).flat() + allText = (await Promise.all(els.map(el => el.evaluateAll(nodes => nodes.map(node => node.innerText ?? node.textContent))))).flat() + assertElementExists(allText, locator.toString()) } if (store?.currentStep?.opts?.ignoreCase === true) { diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 9e1129710..45c522edf 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -748,6 +748,7 @@ describe('Playwright', function () { locatorProto = Object.getPrototypeOf(page.locator('body')) originalAll = locatorProto.all locatorProto.all = async function () { + locatorProto.all = originalAll const els = await originalAll.call(this) await page.evaluate(() => document.querySelector('.toast')?.remove()) return els @@ -771,6 +772,42 @@ describe('Playwright', function () { await I.dontSee('old toast', '.toast') expect(Date.now() - start).to.be.below(1000) }) + + it('should fail text equality check when every matched element is removed', async () => { + await I.executeScript(() => document.querySelectorAll('.toast')[1].remove()) + const start = Date.now() + let error + try { + await I.seeTextEquals('old toast', '.toast') + } catch (err) { + error = err + } + expect(error?.message).to.include('was not found') + expect(Date.now() - start).to.be.below(1000) + }) + }) + + describe('#see with context matching a non-HTML element', () => { + beforeEach(async () => { + await I.amOnPage('/form/field') + await I.executeScript(() => { + document.body.insertAdjacentHTML('beforeend', 'chart label') + }) + }) + + it('should see text of an SVG element', async () => { + await I.see('chart label', '.chart-label') + }) + + it('should fail to not see text of an SVG element', async () => { + let error + try { + await I.dontSee('chart label', '.chart-label') + } catch (err) { + error = err + } + expect(error).to.be.instanceOf(AssertionFailedError) + }) }) describe('check fields: #seeInField, #seeCheckboxIsChecked, ...', () => {