From bcb5202e8f71d98a7998f43535bb9f26ffdaeed1 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 13 Feb 2026 03:01:34 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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('/')