Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 26 additions & 55 deletions tools/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tools/playwright/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "abledom-playwright",
"version": "0.0.15",
"version": "0.0.16",
"description": "AbleDOM tools for Playwright",
"author": "Marat Abdullin <marata@microsoft.com>",
"license": "MIT",
Expand Down
54 changes: 15 additions & 39 deletions tools/playwright/src/page-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Locator["waitFor"]>
) {
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<string, unknown>)
Expand Down Expand Up @@ -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<Locator["waitFor"]>
) {
const ret = await origWaitFor.apply(this, args);
await reportAbleDOMIssues(this);
return ret;
};

Expand All @@ -252,47 +255,20 @@ 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<void>;
type LocatorProtoWithActions = Record<string, LocatorAction>;

for (const action of actionsToPatch) {
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;
};
}
}
Expand Down
99 changes: 1 addition & 98 deletions tools/playwright/tests/page-injector.test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,<html><body><button>Test</button></body></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,<html><body><input type="text" /></body></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,<html><body><button>Test</button></body></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,
}) => {
Expand Down