Skip to content

Commit 1822860

Browse files
miraoJaromir Obrclaude
authored
fix(playwright): don't block in see() when a matched element is removed (#5746)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Jaromir Obr <jobr@spork.tech> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a459969 commit 1822860

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

‎lib/helper/Playwright.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4428,7 +4428,8 @@ async function proceedSee(assertType, text, context, strict = false) {
44284428
description = `element ${locator.toString()}`
44294429
const els = await this._locate(locator)
44304430
assertElementExists(els, locator.toString())
4431-
allText = await Promise.all(els.map(el => el.innerText()))
4431+
allText = (await Promise.all(els.map(el => el.evaluateAll(nodes => nodes.map(node => node.innerText ?? node.textContent))))).flat()
4432+
assertElementExists(allText, locator.toString())
44324433
}
44334434

44344435
if (store?.currentStep?.opts?.ignoreCase === true) {

‎test/helper/Playwright_test.js‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,86 @@ describe('Playwright', function () {
730730
})
731731
})
732732

733+
describe('#see with context when a matched element is removed', () => {
734+
let locatorProto
735+
let originalAll
736+
737+
beforeEach(async () => {
738+
await I.amOnPage('/form/field')
739+
await I.executeScript(() => {
740+
for (const text of ['old toast', 'new toast']) {
741+
const el = document.createElement('div')
742+
el.className = 'toast'
743+
el.textContent = text
744+
document.body.append(el)
745+
}
746+
})
747+
748+
locatorProto = Object.getPrototypeOf(page.locator('body'))
749+
originalAll = locatorProto.all
750+
locatorProto.all = async function () {
751+
locatorProto.all = originalAll
752+
const els = await originalAll.call(this)
753+
await page.evaluate(() => document.querySelector('.toast')?.remove())
754+
return els
755+
}
756+
page.setDefaultTimeout(10000)
757+
})
758+
759+
afterEach(() => {
760+
locatorProto.all = originalAll
761+
page.setDefaultTimeout(I.options.timeout)
762+
})
763+
764+
it('should see text in a remaining element without waiting for timeout', async () => {
765+
const start = Date.now()
766+
await I.see('new toast', '.toast')
767+
expect(Date.now() - start).to.be.below(1000)
768+
})
769+
770+
it('should not see text of the removed element without waiting for timeout', async () => {
771+
const start = Date.now()
772+
await I.dontSee('old toast', '.toast')
773+
expect(Date.now() - start).to.be.below(1000)
774+
})
775+
776+
it('should fail text equality check when every matched element is removed', async () => {
777+
await I.executeScript(() => document.querySelectorAll('.toast')[1].remove())
778+
const start = Date.now()
779+
let error
780+
try {
781+
await I.seeTextEquals('old toast', '.toast')
782+
} catch (err) {
783+
error = err
784+
}
785+
expect(error?.message).to.include('was not found')
786+
expect(Date.now() - start).to.be.below(1000)
787+
})
788+
})
789+
790+
describe('#see with context matching a non-HTML element', () => {
791+
beforeEach(async () => {
792+
await I.amOnPage('/form/field')
793+
await I.executeScript(() => {
794+
document.body.insertAdjacentHTML('beforeend', '<svg width="200" height="50"><text class="chart-label" x="5" y="20">chart label</text></svg>')
795+
})
796+
})
797+
798+
it('should see text of an SVG element', async () => {
799+
await I.see('chart label', '.chart-label')
800+
})
801+
802+
it('should fail to not see text of an SVG element', async () => {
803+
let error
804+
try {
805+
await I.dontSee('chart label', '.chart-label')
806+
} catch (err) {
807+
error = err
808+
}
809+
expect(error).to.be.instanceOf(AssertionFailedError)
810+
})
811+
})
812+
733813
describe('check fields: #seeInField, #seeCheckboxIsChecked, ...', () => {
734814
it('should throw error if field is not empty', () =>
735815
I.amOnPage('/form/empty')

0 commit comments

Comments
 (0)