From 5a76331824651870f31d100405a8048365141db0 Mon Sep 17 00:00:00 2001 From: Nicolas Lespiaucq Date: Fri, 7 Aug 2026 11:44:18 +0200 Subject: [PATCH] fix(helpers): restore substring matching in waitInUrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5451 wired resolveUrl() into waitInUrl alongside waitUrlEquals. For waitUrlEquals that is correct — a strict comparison needs the relative path resolved against the configured base url. For waitInUrl it silently turned a documented substring match into an origin-anchored one: const expectedUrl = resolveUrl(urlPart, this.options.url) // '/users' -> 'https://app.example.com/users' return currUrl.indexOf(expectedUrl) > -1 // scheme+host+port must now match exactly So I.waitInUrl('/users') breaks after a redirect to a different host, port or scheme, and a partial match such as I.waitInUrl('user=test') resolves to /user=test and never matches /info?user=test. Compare against the raw urlPart again in Playwright, Puppeteer and WebDriver. waitUrlEquals is left untouched. The existing test asserted only inside catch, so it passed vacuously whenever the wait unexpectedly succeeded — which is how this shipped. It now fails on a missing timeout, plus a new case pinning the regression. docs/migration-4.md listed waitInUrl among the methods that resolve relative urls while its own example said the opposite; corrected. Co-authored-by: Claude --- docs/migration-4.md | 4 +++- lib/helper/Playwright.js | 5 ++--- lib/helper/Puppeteer.js | 5 ++--- lib/helper/WebDriver.js | 5 ++--- test/helper/webapi.js | 17 ++++++++++++++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/migration-4.md b/docs/migration-4.md index fb6126e02..8223682bd 100644 --- a/docs/migration-4.md +++ b/docs/migration-4.md @@ -614,7 +614,7 @@ Test files written for 3.x keep working until you flip the flag. ### `wait*` Methods Resolve Relative URLs -`waitInUrl`, `waitUrlEquals`, and `waitCurrentPathEquals` now resolve a relative path against the helper's configured `url` before comparing. In 3.x a literal substring match against `window.location.href` would fail for relative paths. +`waitUrlEquals` and `waitCurrentPathEquals` now resolve a relative path against the helper's configured `url` before comparing. In 3.x a literal comparison against `window.location.href` would fail for relative paths. ```js // helpers: { Playwright: { url: 'https://app.example.com' } } @@ -623,6 +623,8 @@ I.waitUrlEquals('/dashboard') // matches https://app.example.com/dashboard I.waitInUrl('/users') // matches any URL containing /users ``` +`waitInUrl` is unchanged from 3.x — it stays a plain substring match against the current URL and never resolves its argument. + `waitUrlEquals` error messages now include the actual URL the page was on when the wait timed out — easier to diagnose `/dashboard` vs `/dashboard?session=expired`. ## 6. Adopt New Behaviors diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 939988c9a..f93187cca 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -3423,7 +3423,6 @@ class Playwright extends Helper { */ async waitInUrl(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - const expectedUrl = resolveUrl(urlPart, this.options.url) return this.page .waitForFunction( @@ -3431,13 +3430,13 @@ class Playwright extends Helper { const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))) return currUrl.indexOf(urlPart) > -1 }, - expectedUrl, + urlPart, { timeout: waitTimeout }, ) .catch(async e => { const currUrl = await this._getPageUrl() if (/Timeout/i.test(e.message)) { - throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) + throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) } else { throw e } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ff00f6dd8..26dd1a9e2 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2501,7 +2501,6 @@ class Puppeteer extends Helper { */ async waitInUrl(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - const expectedUrl = resolveUrl(urlPart, this.options.url) return this.page .waitForFunction( @@ -2510,12 +2509,12 @@ class Puppeteer extends Helper { return currUrl.indexOf(urlPart) > -1 }, { timeout: waitTimeout }, - expectedUrl, + urlPart, ) .catch(async e => { const currUrl = await this._getPageUrl() if (/Waiting failed:/i.test(e.message) || /failed: timeout/i.test(e.message)) { - throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) + throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) } else { throw e } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 62c0b4dc2..3ae88c127 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -2521,7 +2521,6 @@ class WebDriver extends Helper { async waitInUrl(urlPart, sec = null) { const client = this.browser const aSec = sec || this.options.waitForTimeoutInSeconds - const expectedUrl = resolveUrl(urlPart, this.options.url) let currUrl = '' return client @@ -2529,7 +2528,7 @@ class WebDriver extends Helper { function () { return this.getUrl().then(res => { currUrl = decodeUrl(res) - return currUrl.indexOf(expectedUrl) > -1 + return currUrl.indexOf(urlPart) > -1 }) }, { timeout: aSec * 1000 }, @@ -2537,7 +2536,7 @@ class WebDriver extends Helper { .catch(e => { e = wrapError(e) if (e.message.indexOf('timeout')) { - throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) + throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) } throw e }) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 40b51c3c3..ea7e146cc 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -151,13 +151,24 @@ export function tests() { describe('#waitInUrl, #waitUrlEquals', () => { it('should wait part of the URL to match the expected', async () => { + await I.amOnPage('/info') + await I.waitInUrl('/info') + await I.waitInUrl(`${siteUrl}/info`) + + let err try { - await I.amOnPage('/info') - await I.waitInUrl('/info') await I.waitInUrl('/info2', 0.1) } catch (e) { - assert.include(e.message, `expected url to include ${siteUrl}/info2, but found ${siteUrl}/info`) + err = e } + assert.isDefined(err, 'expected waitInUrl to time out') + assert.include(err.message, `expected url to include /info2, but found ${siteUrl}/info`) + }) + + it('should match a URL part that is not anchored at the base url', async () => { + await I.amOnPage('/info?user=test') + await I.waitInUrl('user=test') + await I.waitInUrl('/info?user=test') }) it('should wait for the entire URL to match the expected', async () => {