From f06e8c26681df6efe6879888b52f2289737d4bb1 Mon Sep 17 00:00:00 2001 From: Marat Abdullin Date: Thu, 19 Feb 2026 18:21:28 +0100 Subject: [PATCH] Fixing Playwright reporter timeouts in some scenarios. --- tools/playwright/package-lock.json | 81 +++++---------- tools/playwright/package.json | 2 +- tools/playwright/src/page-injector.ts | 54 +++------- .../tests/page-injector.test.spec.ts | 99 +------------------ 4 files changed, 43 insertions(+), 193 deletions(-) diff --git a/tools/playwright/package-lock.json b/tools/playwright/package-lock.json index 38cd5ac..3946672 100644 --- a/tools/playwright/package-lock.json +++ b/tools/playwright/package-lock.json @@ -1,12 +1,12 @@ { "name": "abledom-playwright", - "version": "0.0.15", + "version": "0.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "abledom-playwright", - "version": "0.0.15", + "version": "0.0.16", "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.2", @@ -723,16 +723,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1167,13 +1157,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -1410,9 +1400,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2779,13 +2769,13 @@ } }, "node_modules/glob": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz", - "integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.5.tgz", + "integrity": "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.2.0", + "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" }, @@ -2810,14 +2800,11 @@ } }, "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", "dev": true, "license": "MIT", - "dependencies": { - "jackspeak": "^4.2.3" - }, "engines": { "node": "20 || >=22" } @@ -2836,9 +2823,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3436,22 +3423,6 @@ "dev": true, "license": "ISC" }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -3643,11 +3614,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4910,9 +4881,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, diff --git a/tools/playwright/package.json b/tools/playwright/package.json index b78e124..ae9a1db 100644 --- a/tools/playwright/package.json +++ b/tools/playwright/package.json @@ -1,6 +1,6 @@ { "name": "abledom-playwright", - "version": "0.0.15", + "version": "0.0.16", "description": "AbleDOM tools for Playwright", "author": "Marat Abdullin ", "license": "MIT", diff --git a/tools/playwright/src/page-injector.ts b/tools/playwright/src/page-injector.ts index dc83a5e..fdfffa9 100644 --- a/tools/playwright/src/page-injector.ts +++ b/tools/playwright/src/page-injector.ts @@ -141,14 +141,8 @@ export async function attachAbleDOMMethodsToPage( if (!locatorProto.__locatorIsMonkeyPatchedWithAbleDOM) { locatorProto.__locatorIsMonkeyPatchedWithAbleDOM = true; - const origWaitFor = locatorProto.waitFor; - - locatorProto.waitFor = async function waitFor( - this: Locator, - ...args: Parameters - ) { - const ret = await origWaitFor.apply(this, args); - const currentPage = this.page(); + const reportAbleDOMIssues = async (self: Locator) => { + const currentPage = self.page(); // Get options from the page object const pageMarkAsRead = (currentPage as unknown as Record) @@ -229,7 +223,16 @@ export async function attachAbleDOMMethodsToPage( // Note: We don't throw an error here - just report the issues // This allows tests to continue and report all issues found } + }; + const origWaitFor = locatorProto.waitFor; + + locatorProto.waitFor = async function waitFor( + this: Locator, + ...args: Parameters + ) { + const ret = await origWaitFor.apply(this, args); + await reportAbleDOMIssues(this); return ret; }; @@ -252,21 +255,6 @@ export async function attachAbleDOMMethodsToPage( "setInputFiles", ] as const; - // For all actions, options object (containing timeout) is always the last argument: - // - click(options?) - options is only/last arg - // - dblclick(options?) - options is only/last arg - // - fill(value, options?) - options is 2nd/last arg - // - type(text, options?) - options is 2nd/last arg - // - press(key, options?) - options is 2nd/last arg - // - check(options?) - options is only/last arg - // - uncheck(options?) - options is only/last arg - // - selectOption(values, options?) - options is 2nd/last arg - // - hover(options?) - options is only/last arg - // - tap(options?) - options is only/last arg - // - focus(options?) - options is only/last arg - // - blur(options?) - options is only/last arg - // - clear(options?) - options is only/last arg - // - setInputFiles(files, options?) - options is 2nd/last arg type LocatorAction = (...args: unknown[]) => Promise; type LocatorProtoWithActions = Record; @@ -274,25 +262,13 @@ export async function attachAbleDOMMethodsToPage( const originalAction = ( locatorProto as unknown as LocatorProtoWithActions )[action]; + if (originalAction) { (locatorProto as unknown as LocatorProtoWithActions)[action] = async function (this: Locator, ...args: unknown[]) { - // Extract timeout from the last argument if it's an options object - const lastArg = args[args.length - 1]; - const timeout = - lastArg && - typeof lastArg === "object" && - "timeout" in lastArg && - typeof (lastArg as { timeout?: unknown }).timeout === "number" - ? (lastArg as { timeout: number }).timeout - : undefined; - // Call our patched waitFor first to trigger AbleDOM checks - // This will check accessibility before performing the action - await this.waitFor({ state: "attached", timeout }); - // Then perform the original action - // Note: Using Function.prototype.apply directly because Playwright's - // Locator class has its own 'apply' and 'call' methods - return Function.prototype.apply.call(originalAction, this, args); + const ret = await originalAction.apply(this, args); + await reportAbleDOMIssues(this); + return ret; }; } } diff --git a/tools/playwright/tests/page-injector.test.spec.ts b/tools/playwright/tests/page-injector.test.spec.ts index 3187f7a..550a2e0 100644 --- a/tools/playwright/tests/page-injector.test.spec.ts +++ b/tools/playwright/tests/page-injector.test.spec.ts @@ -441,104 +441,7 @@ baseTest("should work without testInfo parameter", async ({ page }) => { baseTest.expect(true).toBe(true); }); -test.describe("action timeout passthrough to waitFor", () => { - test("should pass timeout from click options to waitFor", async ({ - page, - }) => { - await page.goto( - "data:text/html,", - ); - - // Track waitFor() calls to capture the timeout passed - const waitForCalls: { state?: string; timeout?: number }[] = []; - await page.evaluate(() => { - const win = window as WindowWithAbleDOMInstance; - win.ableDOMInstanceForTesting = { - idle: async () => [], - highlightElement: () => { - /* noop */ - }, - }; - }); - - // Intercept waitFor by wrapping the locator - const locator = page.locator("button"); - const originalWaitFor = locator.waitFor.bind(locator); - locator.waitFor = async function (options) { - waitForCalls.push(options ?? {}); - return originalWaitFor(options); - }; - - // Click with a custom timeout - await locator.click({ timeout: 7500 }); - - // waitFor should have been called with the same timeout - expect(waitForCalls.length).toBeGreaterThan(0); - expect(waitForCalls[0].timeout).toBe(7500); - }); - - test("should pass timeout from fill options to waitFor", async ({ page }) => { - await page.goto( - 'data:text/html,', - ); - - const waitForCalls: { state?: string; timeout?: number }[] = []; - await page.evaluate(() => { - const win = window as WindowWithAbleDOMInstance; - win.ableDOMInstanceForTesting = { - idle: async () => [], - highlightElement: () => { - /* noop */ - }, - }; - }); - - const locator = page.locator("input"); - const originalWaitFor = locator.waitFor.bind(locator); - locator.waitFor = async function (options) { - waitForCalls.push(options ?? {}); - return originalWaitFor(options); - }; - - // fill(value, options?) - timeout is in the second argument - await locator.fill("test value", { timeout: 3000 }); - - expect(waitForCalls.length).toBeGreaterThan(0); - expect(waitForCalls[0].timeout).toBe(3000); - }); - - test("should use undefined timeout when action has no timeout option", async ({ - page, - }) => { - await page.goto( - "data:text/html,", - ); - - const waitForCalls: { state?: string; timeout?: number }[] = []; - await page.evaluate(() => { - const win = window as WindowWithAbleDOMInstance; - win.ableDOMInstanceForTesting = { - idle: async () => [], - highlightElement: () => { - /* noop */ - }, - }; - }); - - const locator = page.locator("button"); - const originalWaitFor = locator.waitFor.bind(locator); - locator.waitFor = async function (options) { - waitForCalls.push(options ?? {}); - return originalWaitFor(options); - }; - - // Click without timeout option - await locator.click(); - - expect(waitForCalls.length).toBeGreaterThan(0); - expect(waitForCalls[0].timeout).toBeUndefined(); - }); - +test.describe("action argument passthrough", () => { test("should pass all arguments correctly to original action (fill)", async ({ page, }) => {