From bcb5202e8f71d98a7998f43535bb9f26ffdaeed1 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 13 Feb 2026 03:01:34 +0200 Subject: [PATCH 1/4] implemented path normalization --- lib/helper/Playwright.js | 5 +++-- lib/helper/Puppeteer.js | 5 +++-- lib/helper/WebDriver.js | 16 +++++++++++++--- lib/utils.js | 7 +++++++ test/helper/webapi.js | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 2ecbf0336..5e26afe9b 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -23,6 +23,7 @@ import { clearString, requireWithFallback, normalizeSpacesInString, + normalizePath, relativeDir, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' @@ -2412,7 +2413,7 @@ class Playwright extends Helper { const currentUrl = await this._getPageUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').assert(path, actualPath) + return equals('url path').assert(normalizePath(path), normalizePath(actualPath)) } /** @@ -2422,7 +2423,7 @@ class Playwright extends Helper { const currentUrl = await this._getPageUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').negate(path, actualPath) + return equals('url path').negate(normalizePath(path), normalizePath(actualPath)) } /** diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 0bf8e465c..f997ddb25 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -26,6 +26,7 @@ import { isModifierKey, requireWithFallback, normalizeSpacesInString, + normalizePath, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -1691,7 +1692,7 @@ class Puppeteer extends Helper { const currentUrl = await this._getPageUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').assert(path, actualPath) + return equals('url path').assert(normalizePath(path), normalizePath(actualPath)) } /** @@ -1701,7 +1702,7 @@ class Puppeteer extends Helper { const currentUrl = await this._getPageUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').negate(path, actualPath) + return equals('url path').negate(normalizePath(path), normalizePath(actualPath)) } /** diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index e07234a53..359bd44b2 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -13,7 +13,17 @@ import output from '../output.js' const { debug } = output import { empty } from '../assert/empty.js' import { truth } from '../assert/truth.js' -import { xpathLocator, fileExists, decodeUrl, chunkArray, convertCssPropertiesToCamelCase, screenshotOutputFolder, getNormalizedKeyAttributeValue, modifierKeys } from '../utils.js' +import { + xpathLocator, + fileExists, + decodeUrl, + chunkArray, + convertCssPropertiesToCamelCase, + screenshotOutputFolder, + getNormalizedKeyAttributeValue, + modifierKeys, + normalizePath, +} from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' import ConnectionRefused from './errors/ConnectionRefused.js' @@ -1851,7 +1861,7 @@ class WebDriver extends Helper { const currentUrl = await this.browser.getUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').assert(path, actualPath) + return equals('url path').assert(normalizePath(path), normalizePath(actualPath)) } /** @@ -1861,7 +1871,7 @@ class WebDriver extends Helper { const currentUrl = await this.browser.getUrl() const baseUrl = this.options.url || 'http://localhost' const actualPath = new URL(currentUrl, baseUrl).pathname - return equals('url path').negate(path, actualPath) + return equals('url path').negate(normalizePath(path), normalizePath(actualPath)) } /** diff --git a/lib/utils.js b/lib/utils.js index 1458f387c..f3029d60f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -150,6 +150,13 @@ export const decodeUrl = function (url) { return decodeURIComponent(decodeURIComponent(decodeURIComponent(url))) } +export const normalizePath = function (path) { + if (path === '' || path === '/') return '/' + return path + .replace(/\/+/g, '/') + .replace(/\/$/, '') || '/' +} + export const xpathLocator = { /** * @param {string} string diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 842a9d81b..212b3cebe 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -114,6 +114,42 @@ export function tests() { await I.seeCurrentPathEquals('/info') await I.dontSeeCurrentPathEquals('/info#section') }) + + it('should normalize trailing slashes in path comparison', async () => { + await I.amOnPage('/info/') + await I.seeCurrentPathEquals('/info') + await I.seeCurrentPathEquals('/info/') + + await I.amOnPage('/users/') + await I.seeCurrentPathEquals('/users') + await I.seeCurrentPathEquals('/users/') + }) + + it('should normalize multiple consecutive slashes in path', async () => { + await I.amOnPage('/users//1') + await I.seeCurrentPathEquals('/users/1') + await I.seeCurrentPathEquals('/users//1') + + await I.amOnPage('/info///test') + await I.seeCurrentPathEquals('/info/test') + }) + + it('should handle root path correctly', async () => { + await I.amOnPage('/') + await I.seeCurrentPathEquals('/') + await I.seeCurrentPathEquals('') + await I.dontSeeCurrentPathEquals('/info') + }) + + it('should normalize both expected and actual paths', async () => { + await I.amOnPage('/users/') + await I.seeCurrentPathEquals('/users/') + await I.seeCurrentPathEquals('/users') + + await I.amOnPage('/users//1/') + await I.seeCurrentPathEquals('/users/1') + await I.seeCurrentPathEquals('/users/1/') + }) }) describe('#waitInUrl, #waitUrlEquals', () => { From fd86037978a691a061e420721f34ee8ee1746cb4 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 15 Feb 2026 23:27:59 +0200 Subject: [PATCH 2/4] fixed tests --- test/helper/webapi.js | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 212b3cebe..bf97840e0 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -120,18 +120,15 @@ export function tests() { await I.seeCurrentPathEquals('/info') await I.seeCurrentPathEquals('/info/') - await I.amOnPage('/users/') - await I.seeCurrentPathEquals('/users') - await I.seeCurrentPathEquals('/users/') + await I.amOnPage('/form/field/') + await I.seeCurrentPathEquals('/form/field') + await I.seeCurrentPathEquals('/form/field/') }) it('should normalize multiple consecutive slashes in path', async () => { - await I.amOnPage('/users//1') - await I.seeCurrentPathEquals('/users/1') - await I.seeCurrentPathEquals('/users//1') - - await I.amOnPage('/info///test') - await I.seeCurrentPathEquals('/info/test') + await I.amOnPage('/form//field') + await I.seeCurrentPathEquals('/form/field') + await I.seeCurrentPathEquals('/form//field') }) it('should handle root path correctly', async () => { @@ -142,13 +139,13 @@ export function tests() { }) it('should normalize both expected and actual paths', async () => { - await I.amOnPage('/users/') - await I.seeCurrentPathEquals('/users/') - await I.seeCurrentPathEquals('/users') + await I.amOnPage('/form/field/') + await I.seeCurrentPathEquals('/form/field/') + await I.seeCurrentPathEquals('/form/field') - await I.amOnPage('/users//1/') - await I.seeCurrentPathEquals('/users/1') - await I.seeCurrentPathEquals('/users/1/') + await I.amOnPage('/form//field//') + await I.seeCurrentPathEquals('/form/field') + await I.seeCurrentPathEquals('/form/field/') }) }) From f1e376e9a8466c12d136a9cfa3c4e035482388f5 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Feb 2026 14:08:34 +0200 Subject: [PATCH 3/4] added path check to waiter --- lib/helper/Playwright.js | 29 +++++++++++++++++++++++++++++ lib/helper/Puppeteer.js | 29 +++++++++++++++++++++++++++++ lib/helper/WebDriver.js | 28 ++++++++++++++++++++++++++++ test/helper/webapi.js | 22 ++++++++++++++++++++++ 4 files changed, 108 insertions(+) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 5e26afe9b..65392579c 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -3434,6 +3434,35 @@ class Playwright extends Helper { } } + /** + * {{> waitCurrentPathEquals }} + */ + async waitCurrentPathEquals(path, sec = null) { + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + const normalizedPath = normalizePath(path) + + try { + await this.page.waitForFunction( + expectedPath => { + const actualPath = window.location.pathname + const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/') + return normalizePath(actualPath) === expectedPath + }, + { timeout: waitTimeout }, + normalizedPath, + ) + } catch (e) { + const currentUrl = await this._getPageUrl() + const baseUrl = this.options.url || 'http://localhost' + const actualPath = new URL(currentUrl, baseUrl).pathname + if (/Timeout/i.test(e.message)) { + throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`) + } else { + throw e + } + } + } + /** * {{> waitForText }} */ diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index f997ddb25..e1993da32 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2497,6 +2497,35 @@ class Puppeteer extends Helper { }) } + /** + * {{> waitCurrentPathEquals }} + */ + async waitCurrentPathEquals(path, sec = null) { + const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout + const normalizedPath = normalizePath(path) + + return this.page + .waitForFunction( + expectedPath => { + const actualPath = window.location.pathname + const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/') + return normalizePath(actualPath) === expectedPath + }, + { timeout: waitTimeout }, + normalizedPath, + ) + .catch(async e => { + const currUrl = await this._getPageUrl() + const baseUrl = this.options.url || 'http://localhost' + const actualPath = new URL(currUrl, baseUrl).pathname + if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) { + throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`) + } else { + throw e + } + }) + } + /** * {{> waitForText }} */ diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 359bd44b2..fa48ba4f1 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -2544,6 +2544,34 @@ class WebDriver extends Helper { }) } + /** + * {{> waitCurrentPathEquals }} + */ + async waitCurrentPathEquals(path, sec = null) { + const aSec = sec || this.options.waitForTimeoutInSeconds + const normalizedPath = normalizePath(path) + const baseUrl = this.options.url || 'http://localhost' + let actualPath = '' + + return this.browser + .waitUntil( + async () => { + const currUrl = await this.browser.getUrl() + const url = new URL(currUrl, baseUrl) + actualPath = url.pathname + return normalizePath(actualPath) === normalizedPath + }, + { timeout: aSec * 1000 }, + ) + .catch(e => { + e = wrapError(e) + if (e.message.indexOf('timeout')) { + throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`) + } + throw e + }) + } + /** * {{> waitForText }} * diff --git a/test/helper/webapi.js b/test/helper/webapi.js index bf97840e0..f0e234dbf 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -172,6 +172,28 @@ export function tests() { }) }) + describe('#waitCurrentPathEquals', () => { + it('should wait for path to match (ignoring query strings)', async () => { + await I.amOnPage('/info') + await I.waitCurrentPathEquals('/info') + }) + + it('should wait timeout with proper error message', async () => { + try { + await I.amOnPage('/info') + await I.waitCurrentPathEquals('/nonexistent', 0.1) + } catch (e) { + assert.include(e.message, 'expected path to be /nonexistent') + } + }) + + it('should normalize paths when comparing', async () => { + await I.amOnPage('/form/field/') + await I.waitCurrentPathEquals('/form/field') + await I.waitCurrentPathEquals('/form/field/') + }) + }) + describe('see text : #see', () => { it('should check text on site', async () => { await I.amOnPage('/') From 79f0ea77524b63368677c5f6ad4b7290267c9a58 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Wed, 18 Feb 2026 06:35:19 +0200 Subject: [PATCH 4/4] imrpved wait* methods --- lib/helper/Playwright.js | 23 ++++++++--------------- lib/helper/Puppeteer.js | 23 ++++++++--------------- lib/helper/WebDriver.js | 15 +++++++-------- lib/utils.js | 11 +++++++++++ test/helper/webapi.js | 2 +- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 65392579c..209406770 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -24,6 +24,7 @@ import { requireWithFallback, normalizeSpacesInString, normalizePath, + resolveUrl, relativeDir, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' @@ -3383,6 +3384,7 @@ 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( @@ -3390,13 +3392,13 @@ class Playwright extends Helper { const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href))) return currUrl.indexOf(urlPart) > -1 }, - urlPart, + expectedUrl, { timeout: waitTimeout }, ) .catch(async e => { - const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data. + const currUrl = await this._getPageUrl() if (/Timeout/i.test(e.message)) { - throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) } else { throw e } @@ -3408,26 +3410,17 @@ class Playwright extends Helper { */ async waitUrlEquals(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - - const baseUrl = this.options.url - let expectedUrl = urlPart - if (urlPart.indexOf('http') < 0) { - expectedUrl = baseUrl + urlPart - } + const expectedUrl = resolveUrl(urlPart, this.options.url) try { await this.page.waitForURL( - url => url.href.includes(expectedUrl), + url => url.href === expectedUrl, { timeout: waitTimeout }, ) } catch (e) { const currUrl = await this._getPageUrl() if (/Timeout/i.test(e.message)) { - if (!currUrl.includes(expectedUrl)) { - throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`) - } else { - throw new Error(`expected url not loaded, error message: ${e.message}`) - } + throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`) } else { throw e } diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index e1993da32..81a7ad0a8 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -27,6 +27,7 @@ import { requireWithFallback, normalizeSpacesInString, normalizePath, + resolveUrl, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -2442,6 +2443,7 @@ 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( @@ -2450,12 +2452,12 @@ class Puppeteer extends Helper { return currUrl.indexOf(urlPart) > -1 }, { timeout: waitTimeout }, - urlPart, + expectedUrl, ) .catch(async e => { - const currUrl = await this._getPageUrl() // Required because the waitForFunction can't return data. + 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 ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) } else { throw e } @@ -2467,18 +2469,13 @@ class Puppeteer extends Helper { */ async waitUrlEquals(urlPart, sec = null) { const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout - - const baseUrl = this.options.url - let expectedUrl = urlPart - if (urlPart.indexOf('http') < 0) { - expectedUrl = baseUrl + urlPart - } + const expectedUrl = resolveUrl(urlPart, this.options.url) return this.page .waitForFunction( url => { const currUrl = decodeURIComponent(window.location.href) - return currUrl.indexOf(url) > -1 + return currUrl === url }, { timeout: waitTimeout }, expectedUrl, @@ -2486,11 +2483,7 @@ class Puppeteer extends Helper { .catch(async e => { const currUrl = await this._getPageUrl() if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) { - if (!currUrl.includes(expectedUrl)) { - throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`) - } else { - throw new Error(`expected url not loaded, error message: ${e.message}`) - } + throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`) } else { throw e } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index fa48ba4f1..e29ffee25 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -23,6 +23,7 @@ import { getNormalizedKeyAttributeValue, modifierKeys, normalizePath, + resolveUrl, } from '../utils.js' import { isColorProperty, convertColorToRGBA } from '../colorUtils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -2497,6 +2498,7 @@ 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 @@ -2504,7 +2506,7 @@ class WebDriver extends Helper { function () { return this.getUrl().then(res => { currUrl = decodeUrl(res) - return currUrl.indexOf(urlPart) > -1 + return currUrl.indexOf(expectedUrl) > -1 }) }, { timeout: aSec * 1000 }, @@ -2512,7 +2514,7 @@ class WebDriver extends Helper { .catch(e => { e = wrapError(e) if (e.message.indexOf('timeout')) { - throw new Error(`expected url to include ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`) } throw e }) @@ -2523,22 +2525,19 @@ class WebDriver extends Helper { */ async waitUrlEquals(urlPart, sec = null) { const aSec = sec || this.options.waitForTimeoutInSeconds - const baseUrl = this.options.url - if (urlPart.indexOf('http') < 0) { - urlPart = baseUrl + urlPart - } + const expectedUrl = resolveUrl(urlPart, this.options.url) let currUrl = '' return this.browser .waitUntil(function () { return this.getUrl().then(res => { currUrl = decodeUrl(res) - return currUrl === urlPart + return currUrl === expectedUrl }) }, aSec * 1000) .catch(e => { e = wrapError(e) if (e.message.indexOf('timeout')) { - throw new Error(`expected url to be ${urlPart}, but found ${currUrl}`) + throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`) } throw e }) diff --git a/lib/utils.js b/lib/utils.js index f3029d60f..f3b2a2319 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -157,6 +157,17 @@ export const normalizePath = function (path) { .replace(/\/$/, '') || '/' } +export const resolveUrl = function (url, baseUrl) { + if (!url) return url + if (url.indexOf('http') === 0) return url + if (!baseUrl) return url + try { + return new URL(url, baseUrl).href + } catch (e) { + return url + } +} + export const xpathLocator = { /** * @param {string} string diff --git a/test/helper/webapi.js b/test/helper/webapi.js index f0e234dbf..420d8fc37 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -156,7 +156,7 @@ export function tests() { await I.waitInUrl('/info') await I.waitInUrl('/info2', 0.1) } catch (e) { - assert.include(e.message, `expected url to include /info2, but found ${siteUrl}/info`) + assert.include(e.message, `expected url to include ${siteUrl}/info2, but found ${siteUrl}/info`) } })