Skip to content

Commit b27e9cf

Browse files
Copilotkobenguyent
andauthored
Fix waitForText timeout regression in Playwright helper (#5093)
* Initial plan * Fix waitForText timeout regression in Playwright helper * Add comprehensive tests for waitForText timeout behavior * Changes before error encountered Co-authored-by: kobenguyent <[email protected]> * Fix waitForText timeout regression by checking title text * Changes before error encountered Co-authored-by: kobenguyent <[email protected]> * Changes before error encountered Co-authored-by: kobenguyent <[email protected]> * Fix contextObject.waitForFunction error in iframe contexts Co-authored-by: kobenguyent <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: kobenguyent <[email protected]>
1 parent 75d98de commit b27e9cf

File tree

2 files changed

+100
-41
lines changed

2 files changed

+100
-41
lines changed

lib/helper/Playwright.js

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,61 +2779,62 @@ class Playwright extends Helper {
27792779
.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
27802780
.first()
27812781
.waitFor({ timeout: waitTimeout, state: 'visible' })
2782+
.catch(e => {
2783+
throw new Error(errorMessage)
2784+
})
27822785
}
27832786

27842787
if (locator.isXPath()) {
2785-
return contextObject.waitForFunction(
2786-
([locator, text, $XPath]) => {
2787-
eval($XPath)
2788-
const el = $XPath(null, locator)
2789-
if (!el.length) return false
2790-
return el[0].innerText.indexOf(text) > -1
2791-
},
2792-
[locator.value, text, $XPath.toString()],
2793-
{ timeout: waitTimeout },
2794-
)
2788+
return contextObject
2789+
.waitForFunction(
2790+
([locator, text, $XPath]) => {
2791+
eval($XPath)
2792+
const el = $XPath(null, locator)
2793+
if (!el.length) return false
2794+
return el[0].innerText.indexOf(text) > -1
2795+
},
2796+
[locator.value, text, $XPath.toString()],
2797+
{ timeout: waitTimeout },
2798+
)
2799+
.catch(e => {
2800+
throw new Error(errorMessage)
2801+
})
27952802
}
27962803
} catch (e) {
27972804
throw new Error(`${errorMessage}\n${e.message}`)
27982805
}
27992806
}
28002807

2808+
// Based on original implementation but fixed to check title text and remove problematic promiseRetry
2809+
// Original used timeoutGap for waitForFunction to give it slightly more time than the locator
28012810
const timeoutGap = waitTimeout + 1000
28022811

2803-
// We add basic timeout to make sure we don't wait forever
2804-
// We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
2805-
// or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
2806-
// If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
2807-
2808-
// Use a flag to stop retries when race resolves
2809-
let shouldStop = false
2810-
let timeoutId
2811-
2812-
const racePromise = Promise.race([
2813-
new Promise((_, reject) => {
2814-
timeoutId = setTimeout(() => reject(errorMessage), waitTimeout)
2815-
}),
2816-
this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2817-
promiseRetry(
2818-
async (retry, number) => {
2819-
// Stop retrying if race has resolved
2820-
if (shouldStop) {
2821-
throw new Error('Operation cancelled')
2812+
return Promise.race([
2813+
// Strategy 1: waitForFunction that checks both body AND title text
2814+
// Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
2815+
// Original only checked document.body.innerText, missing title text like "TestEd"
2816+
this.page.waitForFunction(
2817+
function (text) {
2818+
// Check body text (original behavior)
2819+
if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
2820+
return true
28222821
}
2823-
const textPresent = await contextObject
2824-
.locator(`:has-text(${JSON.stringify(text)})`)
2825-
.first()
2826-
.isVisible()
2827-
if (!textPresent) retry(errorMessage)
2822+
// Check document title (fixes the TestEd in title issue)
2823+
if (document.title && document.title.indexOf(text) > -1) {
2824+
return true
2825+
}
2826+
return false
28282827
},
2829-
{ retries: 10, minTimeout: 100, maxTimeout: 500, factor: 1.5 },
2828+
text,
2829+
{ timeout: timeoutGap },
28302830
),
2831-
])
2832-
2833-
// Clean up when race resolves/rejects
2834-
return racePromise.finally(() => {
2835-
if (timeoutId) clearTimeout(timeoutId)
2836-
shouldStop = true
2831+
// Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
2832+
contextObject
2833+
.locator(`:has-text(${JSON.stringify(text)})`)
2834+
.first()
2835+
.waitFor({ timeout: waitTimeout }),
2836+
]).catch(err => {
2837+
throw new Error(errorMessage)
28372838
})
28382839
}
28392840

test/helper/Playwright_test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,64 @@ describe('Playwright', function () {
778778
.then(() => I.seeInField('#text2', 'London')))
779779
})
780780

781+
describe('#waitForText timeout fix', () => {
782+
it('should wait for the full timeout duration when text is not found', async function () {
783+
this.timeout(10000) // Allow up to 10 seconds for this test
784+
785+
const startTime = Date.now()
786+
const timeoutSeconds = 3 // 3 second timeout
787+
788+
try {
789+
await I.amOnPage('/')
790+
await I.waitForText('ThisTextDoesNotExistAnywhere12345', timeoutSeconds)
791+
// Should not reach here
792+
throw new Error('waitForText should have thrown an error')
793+
} catch (error) {
794+
const elapsedTime = Date.now() - startTime
795+
const expectedTimeout = timeoutSeconds * 1000
796+
797+
// Verify it waited close to the full timeout (allow 500ms tolerance)
798+
assert.ok(elapsedTime >= expectedTimeout - 500, `Expected to wait at least ${expectedTimeout - 500}ms, but waited ${elapsedTime}ms`)
799+
assert.ok(elapsedTime <= expectedTimeout + 1000, `Expected to wait at most ${expectedTimeout + 1000}ms, but waited ${elapsedTime}ms`)
800+
assert.ok(error.message.includes('was not found on page after'), `Expected error message about text not found, got: ${error.message}`)
801+
}
802+
})
803+
804+
it('should return quickly when text is found', async function () {
805+
this.timeout(5000)
806+
807+
const startTime = Date.now()
808+
809+
await I.amOnPage('/')
810+
await I.waitForText('TestEd', 10) // This text should exist on the test page
811+
812+
const elapsedTime = Date.now() - startTime
813+
// Should find text quickly, within 2 seconds
814+
assert.ok(elapsedTime < 2000, `Expected to find text quickly but took ${elapsedTime}ms`)
815+
})
816+
817+
it('should work correctly with context parameter and proper timeout', async function () {
818+
this.timeout(8000)
819+
820+
const startTime = Date.now()
821+
const timeoutSeconds = 2
822+
823+
try {
824+
await I.amOnPage('/')
825+
await I.waitForText('NonExistentTextInBody', timeoutSeconds, 'body')
826+
throw new Error('Should have thrown timeout error')
827+
} catch (error) {
828+
const elapsedTime = Date.now() - startTime
829+
const expectedTimeout = timeoutSeconds * 1000
830+
831+
// Verify proper timeout behavior with context
832+
assert.ok(elapsedTime >= expectedTimeout - 500, `Expected to wait at least ${expectedTimeout - 500}ms, but waited ${elapsedTime}ms`)
833+
assert.ok(elapsedTime <= expectedTimeout + 1000, `Expected to wait at most ${expectedTimeout + 1000}ms, but waited ${elapsedTime}ms`)
834+
assert.ok(error.message.includes('was not found on page after'), `Expected timeout error message, got: ${error.message}`)
835+
}
836+
})
837+
})
838+
781839
describe('#grabHTMLFrom', () => {
782840
it('should grab inner html from an element using xpath query', () =>
783841
I.amOnPage('/')

0 commit comments

Comments
 (0)