From fae85ac7d9d1a3d454ccb215f1cc00338b708e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Wed, 19 Jun 2024 22:16:18 +0200 Subject: [PATCH] aria-required-attr (#37) * aria-required-attr * try fixing tooltip tests * use summary reporter * publish test results * increase permissions for action --- .github/workflows/ci.yml | 8 ++++ README.md | 2 +- package-lock.json | 34 ++++++++++++++++ package.json | 1 + src/rules/aria-required-attr.ts | 71 +++++++++++++++++++++++++++++++++ tests/aria-required-attr.ts | 54 +++++++++++++++++++++++++ tests/aria-tooltip-name.ts | 4 +- web-test-runner.config.mjs | 18 ++++++++- 8 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 src/rules/aria-required-attr.ts create mode 100644 tests/aria-required-attr.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bd8fa3..fef0602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,9 @@ jobs: runs-on: macos-latest permissions: pull-requests: write + contents: read + checks: write + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -27,3 +30,8 @@ jobs: minimum-coverage: 90 artifact-name: code-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: './test-results.xml' diff --git a/README.md b/README.md index c3b74de..b64c0a7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ | ❌ | aria-input-field-name | https://dequeuniversity.com/rules/axe/4.4/aria-input-field-name?application=RuleDescription | Ensures every ARIA input field has an accessible name | Moderate, Serious | cat.aria, wcag2a, wcag412, ACT | failure, needs review | [e086e5](https://act-rules.github.io/rules/e086e5) | | ❌ | aria-meter-name | https://dequeuniversity.com/rules/axe/4.4/aria-meter-name?application=RuleDescription | Ensures every ARIA meter node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | | ❌ | aria-progressbar-name | https://dequeuniversity.com/rules/axe/4.4/aria-progressbar-name?application=RuleDescription | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review | | -| ❌ | aria-required-attr | https://dequeuniversity.com/rules/axe/4.4/aria-required-attr?application=RuleDescription | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | +| ✅ | aria-required-attr | https://dequeuniversity.com/rules/axe/4.4/aria-required-attr?application=RuleDescription | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure | [4e8ab6](https://act-rules.github.io/rules/4e8ab6) | | ❌ | aria-required-children | https://dequeuniversity.com/rules/axe/4.4/aria-required-children?application=RuleDescription | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | failure, needs review | [bc4a75](https://act-rules.github.io/rules/bc4a75) | | ❌ | aria-required-parent | https://dequeuniversity.com/rules/axe/4.4/aria-required-parent?application=RuleDescription | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | failure | [ff89c9](https://act-rules.github.io/rules/ff89c9) | | ❌ | aria-roledescription | https://dequeuniversity.com/rules/axe/4.4/aria-roledescription?application=RuleDescription | Ensure aria-roledescription is only used on elements with an implicit or explicit role | Serious | cat.aria, wcag2a, wcag412 | failure, needs review | | diff --git a/package-lock.json b/package-lock.json index d98cdff..25e4b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@web/dev-server": "^0.4.5", "@web/dev-server-esbuild": "^1.0.2", "@web/test-runner": "^0.18.2", + "@web/test-runner-junit-reporter": "^0.7.1", "@web/test-runner-playwright": "^0.11.0", "eslint": "^8.25.0", "eslint-plugin-github": "^4.4.0", @@ -1787,6 +1788,22 @@ "node": ">=18.0.0" } }, + "node_modules/@web/test-runner-junit-reporter": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-junit-reporter/-/test-runner-junit-reporter-0.7.1.tgz", + "integrity": "sha512-5fxiMw3lvoFluvGHxThKtT+BPt2nATVlx4/emlUS1ZCpCq/1Xucmsux/JvIqOChzahzPSTTgmqCkVwB1vbnloQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-chrome": "^0.16.0", + "@web/test-runner-core": "^0.13.0", + "array-flat-polyfill": "^1.0.1", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@web/test-runner-mocha": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz", @@ -1976,6 +1993,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flat-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz", + "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", @@ -8251,6 +8278,13 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 9ad6743..370872d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@web/dev-server": "^0.4.5", "@web/dev-server-esbuild": "^1.0.2", "@web/test-runner": "^0.18.2", + "@web/test-runner-junit-reporter": "^0.7.1", "@web/test-runner-playwright": "^0.11.0", "eslint": "^8.25.0", "eslint-plugin-github": "^4.4.0", diff --git a/src/rules/aria-required-attr.ts b/src/rules/aria-required-attr.ts new file mode 100644 index 0000000..a5615ac --- /dev/null +++ b/src/rules/aria-required-attr.ts @@ -0,0 +1,71 @@ +import { AccessibilityError } from "../scanner"; +import { querySelectorAll } from "../utils"; + +// TODO: This list is incomplete! +type Role = + | "checkbox" + | "combobox" + | "heading" + | "menuitemcheckbox" + | "menuitemradio" + | "meter" + | "radio" + | "scrollbar" + | "seperator" + | "slider" + | "switch"; + +// TODO: This list is incomplete! +type AriaAttribute = + | "aria-checked" + | "aria-expanded" + | "aria-level" + | "aria-checked" + | "aria-valuenow" + | "aria-controls"; + +/** + * Required States and Properties: + * + * @see https://w3c.github.io/aria/#authorErrorDefaultValuesTable + */ +const roleToRequiredStatesAndPropertiesMaps: Record = { + checkbox: ["aria-checked"], + combobox: ["aria-expanded"], + heading: ["aria-level"], + menuitemcheckbox: ["aria-checked"], + menuitemradio: ["aria-checked"], + meter: ["aria-valuenow"], + radio: ["aria-checked"], + scrollbar: ["aria-controls", "aria-valuenow"], + // If focusable + seperator: ["aria-valuenow"], + slider: ["aria-valuenow"], + switch: ["aria-checked"], +}; + +const id = "aria-required-attr"; +const text = "Required ARIA attributes must be provided"; +const url = `https://dequeuniversity.com/rules/axe/4.4/${id}?application=RuleDescription`; + +export function ariaRequiredAttr(el: Element): AccessibilityError[] { + const errors = []; + + const selector = Object.entries(roleToRequiredStatesAndPropertiesMaps) + .map(([role, attributes]) => { + return `[role=${role}]:not(${attributes.map((attr) => `[${attr}]`).join("")})`; + }) + .join(","); + + const elements = querySelectorAll(selector, el); + if (el.matches(selector)) elements.push(el); + + for (const element of elements) { + errors.push({ + element, + text, + url, + }); + } + return errors; +} diff --git a/tests/aria-required-attr.ts b/tests/aria-required-attr.ts new file mode 100644 index 0000000..bc75aa1 --- /dev/null +++ b/tests/aria-required-attr.ts @@ -0,0 +1,54 @@ +import { fixture, expect } from "@open-wc/testing"; +import { Scanner } from "../src/scanner"; +import ariaValidAttr from "../src/rules/aria-valid-attr"; + +const scanner = new Scanner([ariaValidAttr]); + +const passes = [ + '
', + '
', + '', + '', + '', + '', + '
', +]; +const violations = [ + '
', + '
', + '
', + '', + '
', + '
', + '
', + '
', +]; + +describe("aria-required-attr", async function () { + for (const markup of passes) { + const el = await fixture(markup); + it(el.outerHTML, async () => { + const results = (await scanner.scan(el)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.be.empty; + }); + } + + for (const markup of violations) { + const el = await fixture(markup); + it(el.outerHTML, async () => { + const results = (await scanner.scan(el)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.eql([ + { + text: "ARIA attributes must conform to valid names", + url: "https://dequeuniversity.com/rules/axe/4.4/aria-valid-attr", + }, + ]); + }); + } +}); diff --git a/tests/aria-tooltip-name.ts b/tests/aria-tooltip-name.ts index 1e8fe99..6f0572a 100644 --- a/tests/aria-tooltip-name.ts +++ b/tests/aria-tooltip-name.ts @@ -30,7 +30,7 @@ const violations = [ describe("aria-tooltip-name", async function () { for (const markup of passes) { - const el = await fixture(html`${markup}`); + const el = await fixture(markup); it(el.outerHTML, async () => { const results = (await scanner.scan(el)).map(({ text, url }) => { return { text, url }; @@ -41,7 +41,7 @@ describe("aria-tooltip-name", async function () { } for await (const markup of violations) { - const el = await fixture(html`${markup}`); + const el = await fixture(markup); it(el.outerHTML, async () => { const results = (await scanner.scan(el)).map(({ text, url }) => { return { text, url }; diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index e77f10f..f113677 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -1,21 +1,37 @@ +// eslint-disable-next-line foo +import { env } from "node:process"; + +import { summaryReporter } from "@web/test-runner"; import { esbuildPlugin } from "@web/dev-server-esbuild"; import { playwrightLauncher } from "@web/test-runner-playwright"; +import { junitReporter } from "@web/test-runner-junit-reporter"; const browsers = [playwrightLauncher({ product: "chromium" })]; -if (process.env.CI) { +if (env.CI) { browsers.push( playwrightLauncher({ product: "firefox" }), playwrightLauncher({ product: "webkit" }), ); } +const reporters = [ + summaryReporter(), + env.CI + ? junitReporter({ + outputPath: "./test-results.xml", + reportLogs: true, + }) + : null, +]; + export default { nodeResolve: true, coverage: true, files: ["tests/**/*.ts", "tests/**/*.js"], plugins: [esbuildPlugin({ ts: true, target: "esnext" })], browsers, + reporters, filterBrowserLogs(log) { if ( typeof log.args[0] === "string" &&