Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
281be9b
AbleDOM playwright tools.
mshoho Feb 2, 2026
9d35fa8
Deps bump.
mshoho Feb 2, 2026
ec14012
Fixing the build.
mshoho Feb 2, 2026
8318293
New fixture and a bugfix.
mshoho Feb 3, 2026
7b0fb59
No auto highlighting for now.
mshoho Feb 3, 2026
fe315a0
Async func.
mshoho Feb 3, 2026
6fd6117
Updating versions.
mshoho Feb 3, 2026
2249cac
Built-in instance expose when the window flag is set.
mshoho Feb 3, 2026
747749a
Type fix.
mshoho Feb 3, 2026
e3f3f4d
Using JSON for the reports.
mshoho Feb 3, 2026
4220f00
New version.
mshoho Feb 3, 2026
448617e
README update.
mshoho Feb 3, 2026
c9c554c
Leftovers.
mshoho Feb 3, 2026
a728e6f
New version.
mshoho Feb 3, 2026
5f234e6
Removing unused convenience function.
mshoho Feb 3, 2026
9e9e327
Consistent interface naming.
mshoho Feb 3, 2026
cd868b0
CI permissions GitHub recommendation.
mshoho Feb 3, 2026
42bdc22
Counting good and bad assertions.
mshoho Feb 3, 2026
33c12d5
Adjusting default value for outputFile.
mshoho Feb 4, 2026
d47ee0f
Removing old fullMessage which is not needed.
mshoho Feb 4, 2026
7b67499
Expanding information about good an bad assertions for better debugab…
mshoho Feb 5, 2026
ebfcae7
Removing ableDOMInstanceForTestingNeeded window flag and adding a pro…
mshoho Feb 5, 2026
76630c3
Oops.
mshoho Feb 5, 2026
d946546
Exposing reporter interface and strongly typing the internal data.
mshoho Feb 5, 2026
bca5610
Version bump.
mshoho Feb 5, 2026
87e1df9
Normalizing test file locations.
mshoho Feb 5, 2026
e314c2b
Oops.
mshoho Feb 5, 2026
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: 73 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ env:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- uses: actions/checkout@v3
Expand All @@ -21,15 +24,77 @@ jobs:
with:
node-version: 22.x

- run: npm install
- run: npx playwright install
- run: npm run type-check
- run: npm run build
- run: npm run lint
- run: npm run format
- name: "Run tests"
- name: Install dependencies
run: npm install

- name: Install Playwright browsers
run: npx playwright install

- name: Type check
run: npm run type-check

- name: Build
run: npm run build

- name: Lint
run: npm run lint

- name: Format check
run: npm run format

- name: Run tests
run: npm run test
- name: "Check for unstaged changes"

- name: Check for unstaged changes
run: |
git status --porcelain
git diff-index --quiet HEAD -- || exit 1

playwright-tools:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v3

- name: Use Node.js 22
uses: actions/setup-node@v3
with:
node-version: 22.x

- name: Install root dependencies
run: npm install

- name: Install dependencies
working-directory: tools/playwright
run: npm install

- name: Install Playwright browsers
working-directory: tools/playwright
run: npx playwright install --with-deps chromium

- name: Type check
working-directory: tools/playwright
run: npm run type-check

- name: Build
working-directory: tools/playwright
run: npm run build

- name: Lint
working-directory: tools/playwright
run: npm run lint

- name: Format check
working-directory: tools/playwright
run: npm run format

- name: Run tests
working-directory: tools/playwright
run: npm run test

- name: Check for unstaged changes
run: |
git status --porcelain
git diff-index --quiet HEAD -- || exit 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ tsconfig.tsbuildinfo
/blob-report/
/playwright/.cache/
/logs/

# Debug logs
codespace-telemetry-debug.log
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Ignore artifacts:
dist
node_modules
test-results
.test-output
*.yml
11 changes: 10 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ export interface AbleDOMProps {
): void;
onIssueRemoved?(element: HTMLElement, rule: ValidationRule): void;
};
// Expose the created AbleDOM instance as window.ableDOMInstanceForTesting,
// in order for abledom-playwright to be able to automatically grab notifications
// during the tests.
exposeInstanceForTesting?: boolean;
}

export class AbleDOM {
private _win: Window;
private _win: Window & { ableDOMInstanceForTesting?: AbleDOM };
private _isDisposed = false;
private _props: AbleDOMProps | undefined = undefined;
private _observer: MutationObserver;
Expand All @@ -68,6 +72,11 @@ export class AbleDOM {

constructor(win: Window, props: AbleDOMProps = {}) {
this._win = win;

if (props.exposeInstanceForTesting) {
this._win.ableDOMInstanceForTesting = this;
}

this._props = props;

const _elementsToValidate: Set<HTMLElementWithAbleDOM> = new Set();
Expand Down
13 changes: 13 additions & 0 deletions tests/testingMode/exposed-headless.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Exposed Instance - Headless</title>
<script type="module" src="./exposed-headless.ts"></script>
</head>
<body>
<h1>Exposed Instance - Headless</h1>
<button id="button-1">Button1</button>
</body>
</html>
17 changes: 17 additions & 0 deletions tests/testingMode/exposed-headless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AbleDOM, FocusableElementLabelRule } from "abledom";
import { initIdleProp, getAbleDOMCallbacks } from "../utils";

// Create AbleDOM with headless: true (hide UI) and expose instance for testing
const ableDOM = new AbleDOM(window, {
headless: true,
exposeInstanceForTesting: true,
callbacks: getAbleDOMCallbacks(),
});
initIdleProp(ableDOM);
ableDOM.addRule(new FocusableElementLabelRule());
ableDOM.start();
13 changes: 13 additions & 0 deletions tests/testingMode/exposed-with-ui.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Exposed Instance - With UI</title>
<script type="module" src="./exposed-with-ui.ts"></script>
</head>
<body>
<h1>Exposed Instance - With UI</h1>
<button id="button-1">Button1</button>
</body>
</html>
17 changes: 17 additions & 0 deletions tests/testingMode/exposed-with-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AbleDOM, FocusableElementLabelRule } from "abledom";
import { initIdleProp, getAbleDOMCallbacks } from "../utils";

// Create AbleDOM with headless: false (show UI) and expose instance for testing
const ableDOM = new AbleDOM(window, {
headless: false,
exposeInstanceForTesting: true,
callbacks: getAbleDOMCallbacks(),
});
initIdleProp(ableDOM);
ableDOM.addRule(new FocusableElementLabelRule());
ableDOM.start();
13 changes: 13 additions & 0 deletions tests/testingMode/not-exposed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Instance Not Exposed</title>
<script type="module" src="./not-exposed.ts"></script>
</head>
<body>
<h1>Instance Not Exposed</h1>
<button id="button-1">Button1</button>
</body>
</html>
16 changes: 16 additions & 0 deletions tests/testingMode/not-exposed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AbleDOM, FocusableElementLabelRule } from "abledom";
import { initIdleProp, getAbleDOMCallbacks } from "../utils";

// Create AbleDOM without exposing instance for testing
const ableDOM = new AbleDOM(window, {
headless: true,
callbacks: getAbleDOMCallbacks(),
});
initIdleProp(ableDOM);
ableDOM.addRule(new FocusableElementLabelRule());
ableDOM.start();
143 changes: 143 additions & 0 deletions tests/testingMode/testingMode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { test, expect } from "@playwright/test";
import { loadTestPage, issueSelector } from "../utils";

interface WindowWithAbleDOMInstance extends Window {
ableDOMInstanceForTesting?: {
idle: () => Promise<unknown[]>;
highlightElement: (element: HTMLElement, scrollIntoView: boolean) => void;
};
}

test.describe("exposeInstanceForTesting prop", () => {
test("exposeInstanceForTesting: true with headless: false should expose instance and show UI", async ({
page,
}) => {
await loadTestPage(page, "tests/testingMode/exposed-with-ui.html");

// Check that the instance is exposed
const hasInstance = await page.evaluate(() => {
return (
typeof (window as WindowWithAbleDOMInstance)
.ableDOMInstanceForTesting !== "undefined"
);
});
expect(hasInstance).toBe(true);

// Check that the instance has the expected methods
const hasIdleMethod = await page.evaluate(() => {
return (
typeof (window as WindowWithAbleDOMInstance).ableDOMInstanceForTesting
?.idle === "function"
);
});
expect(hasIdleMethod).toBe(true);

// Create an issue by removing the button text
await page.evaluate(() => {
const btn = document.getElementById("button-1");
if (btn) {
btn.innerText = "";
}
});

// Wait for AbleDOM to process
await page.evaluate(async () => {
await (
window as WindowWithAbleDOMInstance
).ableDOMInstanceForTesting?.idle();
});

// With headless: false, the UI should be visible
const issueCount = await page.$$(issueSelector);
expect(issueCount.length).toBeGreaterThan(0);
});

test("exposeInstanceForTesting: true with headless: true should expose instance but hide UI", async ({
page,
}) => {
await loadTestPage(page, "tests/testingMode/exposed-headless.html");

// Check that the instance is exposed
const hasInstance = await page.evaluate(() => {
return (
typeof (window as WindowWithAbleDOMInstance)
.ableDOMInstanceForTesting !== "undefined"
);
});
expect(hasInstance).toBe(true);

// Check that the instance has the expected methods
const hasIdleMethod = await page.evaluate(() => {
return (
typeof (window as WindowWithAbleDOMInstance).ableDOMInstanceForTesting
?.idle === "function"
);
});
expect(hasIdleMethod).toBe(true);

// Create an issue by removing the button text
await page.evaluate(() => {
const btn = document.getElementById("button-1");
if (btn) {
btn.innerText = "";
}
});

// Wait for AbleDOM to process
await page.evaluate(async () => {
await (
window as WindowWithAbleDOMInstance
).ableDOMInstanceForTesting?.idle();
});

// With headless: true, the UI should NOT be visible
const issueCount = await page.$$(issueSelector);
expect(issueCount.length).toBe(0);
});

test("without exposeInstanceForTesting should NOT expose instance", async ({
page,
}) => {
await loadTestPage(page, "tests/testingMode/not-exposed.html");

// Check that the instance is NOT exposed
const hasInstance = await page.evaluate(() => {
return (
typeof (window as WindowWithAbleDOMInstance)
.ableDOMInstanceForTesting !== "undefined"
);
});
expect(hasInstance).toBe(false);
});

test("exposed instance idle() should return issues", async ({ page }) => {
await loadTestPage(page, "tests/testingMode/exposed-headless.html");

// Create an issue
await page.evaluate(() => {
const btn = document.getElementById("button-1");
if (btn) {
btn.innerText = "";
}
});

// Use the exposed instance to get issues
const issues = await page.evaluate(async () => {
const instance = (window as WindowWithAbleDOMInstance)
.ableDOMInstanceForTesting;
const result = await instance?.idle();
return result?.map((issue) => ({
id: (issue as { id?: string }).id,
message: (issue as { message?: string }).message,
}));
});

expect(issues).toBeDefined();
expect(issues!.length).toBe(1);
expect(issues![0].id).toBe("focusable-element-label");
});
});
6 changes: 4 additions & 2 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ interface WindowWithAbleDOMData extends Window {
>;
}

export interface ValidationIssueForTestsIdle
extends Omit<ValidationIssue, "element"> {
export interface ValidationIssueForTestsIdle extends Omit<
ValidationIssue,
"element"
> {
element?: string;
}

Expand Down
5 changes: 5 additions & 0 deletions tools/playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
*.tsbuildinfo
.test-output/
test-results/
Loading