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
13 changes: 5 additions & 8 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,10 +574,7 @@ export class AbleDOM {
return issues;
}

idle(
markAsRead?: boolean,
timeout?: number,
): Promise<ValidationIssue[] | null> {
idle(markAsRead?: boolean, timeout?: number): Promise<ValidationIssue[]> {
if (!this._clearValidationTimeout) {
return Promise.resolve(this._getCurrentIssues(!!markAsRead));
}
Expand All @@ -586,11 +583,10 @@ export class AbleDOM {
let timeoutResolve: (() => void) | undefined;
let timedOut = false;
let timeoutPromise = timeout
? new Promise<null>((resolve) => {
? new Promise<ValidationIssue[]>((resolve) => {
timeoutResolve = () => {
timeoutClear?.();
timeoutResolve = undefined;
resolve(null);
resolve(this._getCurrentIssues(!!markAsRead));
};

let timeoutTimer = this._win.setTimeout(() => {
Expand All @@ -601,6 +597,7 @@ export class AbleDOM {

timeoutClear = () => {
this._win.clearTimeout(timeoutTimer);
timeoutResolve = undefined;
timeoutClear = undefined;
};
})
Expand All @@ -612,7 +609,7 @@ export class AbleDOM {
delete this._idlePromise;
delete this._idleResolve;
resolve(this._getCurrentIssues(timedOut ? false : !!markAsRead));
timeoutResolve?.();
timeoutClear?.();
};
});
}
Expand Down
41 changes: 11 additions & 30 deletions tests/testingMode/testingMode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { loadTestPage, issueSelector } from "../utils";

interface WindowWithAbleDOMInstance extends Window {
ableDOMInstanceForTesting?: {
idle: (markAsRead?: boolean, timeout?: number) => Promise<unknown[] | null>;
idle: (markAsRead?: boolean, timeout?: number) => Promise<unknown[]>;
highlightElement: (element: HTMLElement, scrollIntoView: boolean) => void;
};
}
Expand Down Expand Up @@ -209,7 +209,7 @@ test.describe("exposeInstanceForTesting prop", () => {
expect(secondCallIssues).toBe(1);
});

test("idle() with timeout should return null when timeout expires before validation completes", async ({
test("idle() with timeout should return current issues when timeout expires before validation completes", async ({
page,
}) => {
await loadTestPage(page, "tests/testingMode/exposed-headless.html");
Expand All @@ -234,13 +234,12 @@ test.describe("exposeInstanceForTesting prop", () => {
btn.setAttribute("data-test", "changed");
}

// Call idle with 1ms timeout - should return null if validation is still pending
// Call idle with 1ms timeout - should return current issues even if validation is still pending
return await instance?.idle(false, 1);
});

// Result should be null (timeout) or an array (validation completed fast enough)
// We accept both outcomes since timing can vary
expect(result === null || Array.isArray(result)).toBe(true);
// Result should always be an array (either empty or with current issues)
expect(Array.isArray(result)).toBe(true);
});

test("idle() with sufficient timeout should return issues", async ({
Expand Down Expand Up @@ -298,22 +297,12 @@ test.describe("exposeInstanceForTesting prop", () => {
btn.setAttribute("data-test", Date.now().toString());
}

// Call with markAsRead=true but 1ms timeout - should return null
// Call with markAsRead=true but 1ms timeout - should return current issues
return await instance?.idle(true, 1);
});

// If timeout occurred, issues should NOT have been marked as read
if (timedOutResult === null) {
const subsequentIssues = await page.evaluate(async () => {
const instance = (window as WindowWithAbleDOMInstance)
.ableDOMInstanceForTesting;
const result = await instance?.idle(false, 5000);
return result?.length ?? 0;
});

// Issues should still be available since timeout prevented markAsRead
expect(subsequentIssues).toBe(1);
}
// Result should always be an array
expect(Array.isArray(timedOutResult)).toBe(true);
});

test("concurrent idle() calls - one with timeout, one without - both should get issues", async ({
Expand Down Expand Up @@ -352,17 +341,9 @@ test.describe("exposeInstanceForTesting prop", () => {
};
});

// The call with short timeout may return null (timed out) or issues (validation was fast)
// The call with long timeout should always return issues
expect(
results.withoutTimeout === null || Array.isArray(results.withoutTimeout),
).toBe(true);

// If the short timeout call returned null, the long timeout call should still get issues
if (results.withTimeout === null) {
expect(Array.isArray(results.withoutTimeout)).toBe(true);
expect((results.withoutTimeout as unknown[])?.length).toBeGreaterThan(0);
}
// Both calls should return arrays (current issues or completed validation issues)
expect(Array.isArray(results.withTimeout)).toBe(true);
expect(Array.isArray(results.withoutTimeout)).toBe(true);
});

test("concurrent idle() calls without timeout should both receive issues", async ({
Expand Down
4 changes: 2 additions & 2 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.19",
"version": "0.0.20",
"description": "AbleDOM tools for Playwright",
"author": "Marat Abdullin <marata@microsoft.com>",
"license": "MIT",
Expand Down
6 changes: 4 additions & 2 deletions tools/playwright/src/page-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface AbleDOMIdleOptions {
markAsRead?: boolean;
/**
* Timeout in milliseconds to wait for validation to complete.
* If validation doesn't complete within the timeout, returns null.
* If validation doesn't complete within the timeout, returns the current issues.
* @default 1000
*/
timeout?: number;
Expand Down Expand Up @@ -302,7 +302,9 @@ export async function attachAbleDOMMethodsToPage(
(locatorProto as unknown as LocatorProtoWithActions)[action] =
async function (this: Locator, ...args: unknown[]) {
const ret = await originalAction.apply(this, args);
await reportAbleDOMIssues(this, 0); // Setting the timeout for actions to 0 to avoid side effects.
await reportAbleDOMIssues(this, 1); // 0 would block idle() till it's resolved,
// default timeout will give side effects in a form of timeouts,
// so, we're adding just a little bit of asynchronosity.
return ret;
};
}
Expand Down
12 changes: 6 additions & 6 deletions tools/playwright/tests/page-injector.test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,31 +334,31 @@ test.describe("idle options (markAsRead and timeout)", () => {
expect(calls[0]).toEqual({ markAsRead: true, timeout: 1000 });
});

test("should handle null return from idle() when timeout expires", async ({
test("should handle empty array return from idle() when timeout expires", async ({
page,
}, testInfo) => {
await page.goto(
"data:text/html,<html><body><button>Test</button></body></html>",
);

// Mock AbleDOM to return null (simulating timeout expiration)
// Mock AbleDOM to return empty array (simulating timeout with no current issues)
await page.evaluate(() => {
const win = window as WindowWithAbleDOMInstance;
win.ableDOMInstanceForTesting = {
idle: async () => {
// Return null to simulate timeout
return null;
// Return empty array to simulate timeout with no current issues
return [];
},
highlightElement: () => {
/* noop */
},
};
});

// This should not throw even when idle() returns null
// This should not throw even when idle() returns empty array
await page.locator("button").waitFor();

// Verify no issues were reported (since null was returned)
// Verify no issues were reported (since empty array was returned)
const customDataAttachments = testInfo.attachments.filter(
(att) => att.name === "abledom-test-data",
);
Expand Down