From 0dabcf4cfd990160edd9426e3b1f92e3bb163dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Fri, 13 Mar 2026 11:54:50 +0000 Subject: [PATCH] Enable ACT tests for td-headers-attr, th-has-data-cells Part of #348 Co-Authored-By: Claude Opus 4.6 --- generate-act-tests.js | 2 - rules.json | 4 +- src/rules/aria-allowed-attr.ts | 24 ++- src/rules/td-headers-attr.ts | 5 + src/rules/th-has-data-cells.ts | 182 ++++++++++++++++-- ...400d13aa5a86dbacf71db631f5de1abfc982094.ts | 38 ++++ ...bdbd209a611d68876d5b6e37541f7ddc2038f97.ts | 43 +++++ ...291b4b36dfa21e666a765a51c01e777d40a5174.ts | 35 ++++ ...f2be26b42fa5846a09019bb949c44be95586e0d.ts | 35 ++++ ...391fee07d35c11cfb3fecd19ddaad0fb8c68871.ts | 39 ++++ ...1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.ts | 35 ++++ ...a5019010a6e0cfbcb46b2f7e9e63a6117e06f97.ts | 34 ++++ ...02748c85d58e188b3c13773986272df616b2f3c.ts | 42 ++++ ...d25fd6cc4fde1734fc90c2f11e71886e3458007.ts | 38 ++++ ...0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.ts | 35 ++++ ...935494fdcd2c1fef14d14842c0a19c8f8c54c78.ts | 37 ++++ ...99c8bd6aa53c3b2f4d63fee994333453df410c6.ts | 39 ++++ ...a0ee1b5549d2f1eebd337e85cae8487331ab723.ts | 37 ++++ ...8e0234356523086d570a5b8f959e8cc5ea6b4a6.ts | 37 ++++ ...7a80af86b4ea6357997fa76a62cd55dcb8f2fe7.ts | 45 +++++ ...d021e317ad660d19925651ead361fcaf474dc76.ts | 33 ++++ ...dba1a02d3852eecca6f2f50e1812bce802a42de.ts | 43 +++++ ...64972feaac1097f9365d73aac844c81fa927fa2.ts | 38 ++++ ...bb6ca5dcdbd1fef063561f61de88740db24bd5d.ts | 35 ++++ ...fbe21d1ffdb176ef89afc95cc3f4f666353ee25.ts | 38 ++++ ...e8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.ts | 43 +++++ tests/th-has-data-cells.ts | 7 +- 27 files changed, 987 insertions(+), 36 deletions(-) create mode 100644 tests/act/tests/a25f45/1400d13aa5a86dbacf71db631f5de1abfc982094.ts create mode 100644 tests/act/tests/a25f45/1bdbd209a611d68876d5b6e37541f7ddc2038f97.ts create mode 100644 tests/act/tests/a25f45/7291b4b36dfa21e666a765a51c01e777d40a5174.ts create mode 100644 tests/act/tests/a25f45/7f2be26b42fa5846a09019bb949c44be95586e0d.ts create mode 100644 tests/act/tests/a25f45/8391fee07d35c11cfb3fecd19ddaad0fb8c68871.ts create mode 100644 tests/act/tests/a25f45/b1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.ts create mode 100644 tests/act/tests/a25f45/ba5019010a6e0cfbcb46b2f7e9e63a6117e06f97.ts create mode 100644 tests/act/tests/a25f45/c02748c85d58e188b3c13773986272df616b2f3c.ts create mode 100644 tests/act/tests/a25f45/cd25fd6cc4fde1734fc90c2f11e71886e3458007.ts create mode 100644 tests/act/tests/a25f45/d0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.ts create mode 100644 tests/act/tests/a25f45/d935494fdcd2c1fef14d14842c0a19c8f8c54c78.ts create mode 100644 tests/act/tests/a25f45/f99c8bd6aa53c3b2f4d63fee994333453df410c6.ts create mode 100644 tests/act/tests/d0f69e/1a0ee1b5549d2f1eebd337e85cae8487331ab723.ts create mode 100644 tests/act/tests/d0f69e/28e0234356523086d570a5b8f959e8cc5ea6b4a6.ts create mode 100644 tests/act/tests/d0f69e/47a80af86b4ea6357997fa76a62cd55dcb8f2fe7.ts create mode 100644 tests/act/tests/d0f69e/4d021e317ad660d19925651ead361fcaf474dc76.ts create mode 100644 tests/act/tests/d0f69e/4dba1a02d3852eecca6f2f50e1812bce802a42de.ts create mode 100644 tests/act/tests/d0f69e/664972feaac1097f9365d73aac844c81fa927fa2.ts create mode 100644 tests/act/tests/d0f69e/6bb6ca5dcdbd1fef063561f61de88740db24bd5d.ts create mode 100644 tests/act/tests/d0f69e/9fbe21d1ffdb176ef89afc95cc3f4f666353ee25.ts create mode 100644 tests/act/tests/d0f69e/be8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.ts diff --git a/generate-act-tests.js b/generate-act-tests.js index 2423b1cf..2a3b95e3 100644 --- a/generate-act-tests.js +++ b/generate-act-tests.js @@ -149,8 +149,6 @@ const rulesToIgnore = [ // --- Not implemented - various other rules --- "2ee8b8", // Visible label is part of accessible name - not implemented "73f2c2", // Autocomplete attribute has valid value - not implemented in ACT test format - "a25f45", // Headers attribute specified on a cell refers to cells in the same table element - not implemented - "d0f69e", // Table header cell has assigned cells - not implemented "b4f0c3", // Meta viewport allows for zoom - not implemented in ACT test format "4b1c6c", // Iframe elements with identical accessible names have equivalent purpose - not implemented "akn7bn", // Iframe with interactive elements is not excluded from tab-order - not implemented diff --git a/rules.json b/rules.json index e8799b5b..8b40e363 100644 --- a/rules.json +++ b/rules.json @@ -523,7 +523,7 @@ "ACT Rules": "[7d6734](https://act-rules.github.io/rules/7d6734)" }, { - "implemented": "❌", + "implemented": "✅", "id": "td-headers-attr", "url": "https://dequeuniversity.com/rules/axe/4.11/td-headers-attr?application=RuleDescription", "Description": "Ensure that each cell in a table that uses the headers attribute refers only to other cells in that table", @@ -533,7 +533,7 @@ "ACT Rules": "[a25f45](https://act-rules.github.io/rules/a25f45)" }, { - "implemented": "❌", + "implemented": "✅", "id": "th-has-data-cells", "url": "https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells?application=RuleDescription", "Description": "Ensure that <th> elements and elements with role=columnheader/rowheader have data cells they describe", diff --git a/src/rules/aria-allowed-attr.ts b/src/rules/aria-allowed-attr.ts index f59b6eb4..4eb220af 100644 --- a/src/rules/aria-allowed-attr.ts +++ b/src/rules/aria-allowed-attr.ts @@ -99,25 +99,33 @@ const implicitRoles: Record = { function getInputImplicitRole(element: Element): string | undefined { const type = (element.getAttribute("type") || "text").toLowerCase(); switch (type) { - case "checkbox": + case "checkbox": { return "checkbox"; - case "radio": + } + case "radio": { return "radio"; - case "range": + } + case "range": { return "slider"; - case "number": + } + case "number": { return "spinbutton"; - case "search": + } + case "search": { return "searchbox"; + } case "button": case "image": case "reset": - case "submit": + case "submit": { return "button"; - case "hidden": + } + case "hidden": { return undefined; - default: + } + default: { return "textbox"; + } } } diff --git a/src/rules/td-headers-attr.ts b/src/rules/td-headers-attr.ts index 67297311..d0b1e180 100644 --- a/src/rules/td-headers-attr.ts +++ b/src/rules/td-headers-attr.ts @@ -37,6 +37,11 @@ function validateHeadersAttribute(cell: HTMLTableCellElement): boolean { return false; } + // A cell must not reference itself + if (referencedElement === cell) { + return false; + } + // Check if it's a table cell (td or th) if ( referencedElement.tagName !== "TD" && diff --git a/src/rules/th-has-data-cells.ts b/src/rules/th-has-data-cells.ts index 3b26b8c3..5c10cacd 100644 --- a/src/rules/th-has-data-cells.ts +++ b/src/rules/th-has-data-cells.ts @@ -7,6 +7,54 @@ const text = "Ensure that elements and elements with role=columnheader/rowheader have data cells they describe"; const url = `https://dequeuniversity.com/rules/axe/4.11/${id}`; +/** + * Build a column-index map for a table that accounts for colspan/rowspan. + * Returns a Map from each cell element to its starting column index. + */ +function buildColumnMap( + table: HTMLTableElement, +): Map { + const map = new Map(); + // Track which grid positions are occupied (for rowspan) + const occupied: boolean[][] = []; + + for (let r = 0; r < table.rows.length; r++) { + if (!occupied[r]) occupied[r] = []; + const row = table.rows[r]; + let col = 0; + for (let c = 0; c < row.cells.length; c++) { + // Find next unoccupied column + while (occupied[r][col]) col++; + const cell = row.cells[c]; + map.set(cell, col); + const colspan = cell.colSpan || 1; + const rowspan = cell.rowSpan || 1; + // Mark occupied positions + for (let dr = 0; dr < rowspan; dr++) { + if (!occupied[r + dr]) occupied[r + dr] = []; + for (let dc = 0; dc < colspan; dc++) { + occupied[r + dr][col + dc] = true; + } + } + col += colspan; + } + } + return map; +} + +/** + * Check if a cell in a given row occupies a particular column index, + * accounting for colspan. + */ +function cellOccupiesColumn( + cell: HTMLTableCellElement, + colStart: number, + targetCol: number, +): boolean { + const colspan = cell.colSpan || 1; + return targetCol >= colStart && targetCol < colStart + colspan; +} + /** * Check if a header cell has associated data cells */ @@ -18,8 +66,9 @@ function hasDataCells( const headerId = header.getAttribute("id"); if (headerId) { // Search all td cells in the table - const allCells = querySelectorAll("td", table); + const allCells = querySelectorAll("td, th", table); for (const cell of allCells) { + if (cell === header) continue; const headersAttr = cell.getAttribute("headers"); if (headersAttr) { const headerIds = headersAttr.trim().split(/\s+/); @@ -41,32 +90,64 @@ function hasDataCells( const row = header.parentElement as HTMLTableRowElement; if (!row) return false; - // Get the column index of this header - const cellIndex = [...row.cells].indexOf(header); + const colMap = buildColumnMap(table); + const headerCol = colMap.get(header) ?? 0; if ( scope === "col" || scope === "colgroup" || (!scope && row.rowIndex === 0) ) { - // Column header - check if there are data cells in the same column below + // Column header - check if there are cells in the same column below + // that don't override assignment with an explicit headers attribute for (let i = 0; i < table.rows.length; i++) { - if (i === row.rowIndex) continue; // Skip the header row itself - const cell = table.rows[i].cells[cellIndex]; - if (cell && cell.tagName === "TD") { - return true; + if (i === row.rowIndex) continue; + for (let c = 0; c < table.rows[i].cells.length; c++) { + const cell = table.rows[i].cells[c]; + const cellCol = colMap.get(cell) ?? 0; + if (cellOccupiesColumn(cell, cellCol, headerCol)) { + // If the cell has an explicit headers attribute, it must reference + // this header's id for the assignment to count + if (cell.hasAttribute("headers")) { + if ( + headerId && + cell + .getAttribute("headers")! + .trim() + .split(/\s+/) + .includes(headerId) + ) { + return true; + } + // Explicit headers don't include this header - not assigned + continue; + } + // Skip th elements that are themselves column headers + // (they have scope="col"/"colgroup" and are not data cells) + const cellScope = cell.getAttribute("scope"); + if ( + cell.tagName === "TH" && + (cellScope === "col" || cellScope === "colgroup") + ) { + continue; + } + // Cell is implicitly assigned to this column header + // Count both td and th without column scope (a th can be + // an assigned cell of another th, e.g. row headers in a column) + return true; + } } } } else if ( scope === "row" || scope === "rowgroup" || - (!scope && cellIndex === 0) + (!scope && headerCol === 0) ) { // Row header - check if there are data cells in the same row for (let i = 0; i < row.cells.length; i++) { - if (i === cellIndex) continue; // Skip the header itself const cell = row.cells[i]; - if (cell && cell.tagName === "TD") { + if (cell === header) continue; + if (cell.tagName === "TD") { return true; } } @@ -75,29 +156,88 @@ function hasDataCells( // Check column for (let i = 0; i < table.rows.length; i++) { if (i === row.rowIndex) continue; - const cell = table.rows[i].cells[cellIndex]; - if (cell && cell.tagName === "TD") { - return true; + for (let c = 0; c < table.rows[i].cells.length; c++) { + const cell = table.rows[i].cells[c]; + const cellCol = colMap.get(cell) ?? 0; + if (cellOccupiesColumn(cell, cellCol, headerCol)) { + if (cell.hasAttribute("headers")) { + if ( + headerId && + cell + .getAttribute("headers")! + .trim() + .split(/\s+/) + .includes(headerId) + ) { + return true; + } + continue; + } + // Skip th elements that are column headers themselves + const cellScope = cell.getAttribute("scope"); + if ( + cell.tagName === "TH" && + (cellScope === "col" || cellScope === "colgroup") + ) { + continue; + } + return true; + } } } // Check row for (let i = 0; i < row.cells.length; i++) { - if (i === cellIndex) continue; const cell = row.cells[i]; - if (cell && cell.tagName === "TD") { + if (cell === header) continue; + if (cell.tagName === "TD") { return true; } } } } - // For elements with role=columnheader or rowheader, check for cells with role=cell + // For elements with role=columnheader or rowheader, check positionally const role = header.getAttribute("role"); if (role === "columnheader" || role === "rowheader") { - // Find all cells in the same table/grid - const cells = querySelectorAll('[role="cell"], [role="gridcell"]', table); - if (cells.length > 0) { - return true; + const rows = querySelectorAll('[role="row"]', table); + // Find which row the header is in and its position + const headerRow = header.closest('[role="row"]'); + if (headerRow) { + const headerChildren = [...headerRow.children]; + const headerIndex = headerChildren.indexOf(header as HTMLElement); + + if (role === "columnheader") { + // Check if any other row has a cell/gridcell at this position + for (const row of rows) { + if (row === headerRow) continue; + const children = [...row.children]; + if (headerIndex < children.length) { + const cell = children[headerIndex]; + const cellRole = cell.getAttribute("role"); + if ( + cellRole === "cell" || + cellRole === "gridcell" || + cell.tagName === "TD" + ) { + return true; + } + } + } + } else { + // rowheader - check if there are cells in the same row + const children = [...headerRow.children]; + for (const [i, child] of children.entries()) { + if (i === headerIndex) continue; + const cellRole = child.getAttribute("role"); + if ( + cellRole === "cell" || + cellRole === "gridcell" || + child.tagName === "TD" + ) { + return true; + } + } + } } } diff --git a/tests/act/tests/a25f45/1400d13aa5a86dbacf71db631f5de1abfc982094.ts b/tests/act/tests/a25f45/1400d13aa5a86dbacf71db631f5de1abfc982094.ts new file mode 100644 index 00000000..11cb17a6 --- /dev/null +++ b/tests/act/tests/a25f45/1400d13aa5a86dbacf71db631f5de1abfc982094.ts @@ -0,0 +1,38 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 2 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/1400d13aa5a86dbacf71db631f5de1abfc982094.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 2 + + + + + + + + + + + + + + +
ProjectsExams
15%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/1bdbd209a611d68876d5b6e37541f7ddc2038f97.ts b/tests/act/tests/a25f45/1bdbd209a611d68876d5b6e37541f7ddc2038f97.ts new file mode 100644 index 00000000..605846c1 --- /dev/null +++ b/tests/act/tests/a25f45/1bdbd209a611d68876d5b6e37541f7ddc2038f97.ts @@ -0,0 +1,43 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Failed Example 4 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/1bdbd209a611d68876d5b6e37541f7ddc2038f97.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 4 + + + + + + + + + + + +
+ Projects + + Objective +
+ 15% + + 10% +
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/a25f45/7291b4b36dfa21e666a765a51c01e777d40a5174.ts b/tests/act/tests/a25f45/7291b4b36dfa21e666a765a51c01e777d40a5174.ts new file mode 100644 index 00000000..8a70c72a --- /dev/null +++ b/tests/act/tests/a25f45/7291b4b36dfa21e666a765a51c01e777d40a5174.ts @@ -0,0 +1,35 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 8 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/7291b4b36dfa21e666a765a51c01e777d40a5174.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 8 + + + + + + + + + + + +
Projects
15%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/7f2be26b42fa5846a09019bb949c44be95586e0d.ts b/tests/act/tests/a25f45/7f2be26b42fa5846a09019bb949c44be95586e0d.ts new file mode 100644 index 00000000..72c8e46e --- /dev/null +++ b/tests/act/tests/a25f45/7f2be26b42fa5846a09019bb949c44be95586e0d.ts @@ -0,0 +1,35 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Failed Example 1 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/7f2be26b42fa5846a09019bb949c44be95586e0d.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 1 + + + + + + + + + + + +
ProjectsObjective
15%10%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/a25f45/8391fee07d35c11cfb3fecd19ddaad0fb8c68871.ts b/tests/act/tests/a25f45/8391fee07d35c11cfb3fecd19ddaad0fb8c68871.ts new file mode 100644 index 00000000..5811931b --- /dev/null +++ b/tests/act/tests/a25f45/8391fee07d35c11cfb3fecd19ddaad0fb8c68871.ts @@ -0,0 +1,39 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 3 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/8391fee07d35c11cfb3fecd19ddaad0fb8c68871.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 3 + + + + + + + + + + + + + + + +
ProjectsObjective
15%10%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/b1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.ts b/tests/act/tests/a25f45/b1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.ts new file mode 100644 index 00000000..c246b923 --- /dev/null +++ b/tests/act/tests/a25f45/b1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.ts @@ -0,0 +1,35 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 7 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/b1b17ab86ee2ebce350af1c41d2e6ff8911a33f1.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 7 + + + + + + + + + + + +
ProjectsProgress
My Project15%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/ba5019010a6e0cfbcb46b2f7e9e63a6117e06f97.ts b/tests/act/tests/a25f45/ba5019010a6e0cfbcb46b2f7e9e63a6117e06f97.ts new file mode 100644 index 00000000..37004942 --- /dev/null +++ b/tests/act/tests/a25f45/ba5019010a6e0cfbcb46b2f7e9e63a6117e06f97.ts @@ -0,0 +1,34 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 6 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/ba5019010a6e0cfbcb46b2f7e9e63a6117e06f97.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 6 + + + + + + + + + + +
Name
FirstnameLastname
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/c02748c85d58e188b3c13773986272df616b2f3c.ts b/tests/act/tests/a25f45/c02748c85d58e188b3c13773986272df616b2f3c.ts new file mode 100644 index 00000000..8cfd9918 --- /dev/null +++ b/tests/act/tests/a25f45/c02748c85d58e188b3c13773986272df616b2f3c.ts @@ -0,0 +1,42 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 4 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/c02748c85d58e188b3c13773986272df616b2f3c.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 4 + + + + + + + + + + + + + + + + + + +
ProjectsExams
1212
15%15%45%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/cd25fd6cc4fde1734fc90c2f11e71886e3458007.ts b/tests/act/tests/a25f45/cd25fd6cc4fde1734fc90c2f11e71886e3458007.ts new file mode 100644 index 00000000..cc25b0c9 --- /dev/null +++ b/tests/act/tests/a25f45/cd25fd6cc4fde1734fc90c2f11e71886e3458007.ts @@ -0,0 +1,38 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Failed Example 2 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/cd25fd6cc4fde1734fc90c2f11e71886e3458007.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 2 + + + + + + + +
ProjectsObjective
+ + + + + + +
15%10%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/a25f45/d0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.ts b/tests/act/tests/a25f45/d0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.ts new file mode 100644 index 00000000..5bb28332 --- /dev/null +++ b/tests/act/tests/a25f45/d0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.ts @@ -0,0 +1,35 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Failed Example 3 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/d0c53c06c9e0a766fd5830fbbaa7df76f8cef92a.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 3 + + + + + + + + + +
Event Type
+ Birthday +
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/a25f45/d935494fdcd2c1fef14d14842c0a19c8f8c54c78.ts b/tests/act/tests/a25f45/d935494fdcd2c1fef14d14842c0a19c8f8c54c78.ts new file mode 100644 index 00000000..266be2f5 --- /dev/null +++ b/tests/act/tests/a25f45/d935494fdcd2c1fef14d14842c0a19c8f8c54c78.ts @@ -0,0 +1,37 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 5 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/d935494fdcd2c1fef14d14842c0a19c8f8c54c78.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 5 + + + + + + + + + + + + + +
Age65
Objective40%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/a25f45/f99c8bd6aa53c3b2f4d63fee994333453df410c6.ts b/tests/act/tests/a25f45/f99c8bd6aa53c3b2f4d63fee994333453df410c6.ts new file mode 100644 index 00000000..ea4ab392 --- /dev/null +++ b/tests/act/tests/a25f45/f99c8bd6aa53c3b2f4d63fee994333453df410c6.ts @@ -0,0 +1,39 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[a25f45]Headers attribute specified on a cell refers to cells in the same table element", function () { + it("Passed Example 1 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/a25f45/f99c8bd6aa53c3b2f4d63fee994333453df410c6.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 1 + + + + + + + + + + + + + + + +
ProjectsObjective
15%10%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/td-headers-attr"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/1a0ee1b5549d2f1eebd337e85cae8487331ab723.ts b/tests/act/tests/d0f69e/1a0ee1b5549d2f1eebd337e85cae8487331ab723.ts new file mode 100644 index 00000000..95fd41ae --- /dev/null +++ b/tests/act/tests/d0f69e/1a0ee1b5549d2f1eebd337e85cae8487331ab723.ts @@ -0,0 +1,37 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Failed Example 3 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/1a0ee1b5549d2f1eebd337e85cae8487331ab723.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 3 + + +
+
+
Room
+
Occupant
+
+
+
1A
+
+
+
2A
+
+
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/d0f69e/28e0234356523086d570a5b8f959e8cc5ea6b4a6.ts b/tests/act/tests/d0f69e/28e0234356523086d570a5b8f959e8cc5ea6b4a6.ts new file mode 100644 index 00000000..f0489035 --- /dev/null +++ b/tests/act/tests/d0f69e/28e0234356523086d570a5b8f959e8cc5ea6b4a6.ts @@ -0,0 +1,37 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 5 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/28e0234356523086d570a5b8f959e8cc5ea6b4a6.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 5 + + + + + + + + + + + + + +
CitiesCount
Paris
1
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/47a80af86b4ea6357997fa76a62cd55dcb8f2fe7.ts b/tests/act/tests/d0f69e/47a80af86b4ea6357997fa76a62cd55dcb8f2fe7.ts new file mode 100644 index 00000000..2828c69f --- /dev/null +++ b/tests/act/tests/d0f69e/47a80af86b4ea6357997fa76a62cd55dcb8f2fe7.ts @@ -0,0 +1,45 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 6 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/47a80af86b4ea6357997fa76a62cd55dcb8f2fe7.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 6 + + + + + + + + + + + + + + + + + + + +
+ Opening hours +
DayMorningAfternoon
Mon-Fri8-1214-17
Sat-Sun10-14Closed
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/4d021e317ad660d19925651ead361fcaf474dc76.ts b/tests/act/tests/d0f69e/4d021e317ad660d19925651ead361fcaf474dc76.ts new file mode 100644 index 00000000..84180ed6 --- /dev/null +++ b/tests/act/tests/d0f69e/4d021e317ad660d19925651ead361fcaf474dc76.ts @@ -0,0 +1,33 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 1 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/4d021e317ad660d19925651ead361fcaf474dc76.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 1 + + + + + + + + + +
Time
05:41
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/4dba1a02d3852eecca6f2f50e1812bce802a42de.ts b/tests/act/tests/d0f69e/4dba1a02d3852eecca6f2f50e1812bce802a42de.ts new file mode 100644 index 00000000..be5f183c --- /dev/null +++ b/tests/act/tests/d0f69e/4dba1a02d3852eecca6f2f50e1812bce802a42de.ts @@ -0,0 +1,43 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 4 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/4dba1a02d3852eecca6f2f50e1812bce802a42de.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 4 + + + + + + + + + + + + + + + + + + + +
BreakfastLunchDinner
Day 18:0013:0018:00
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/664972feaac1097f9365d73aac844c81fa927fa2.ts b/tests/act/tests/d0f69e/664972feaac1097f9365d73aac844c81fa927fa2.ts new file mode 100644 index 00000000..c0b5f291 --- /dev/null +++ b/tests/act/tests/d0f69e/664972feaac1097f9365d73aac844c81fa927fa2.ts @@ -0,0 +1,38 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Failed Example 1 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/664972feaac1097f9365d73aac844c81fa927fa2.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 1 + + + + + + + + + + + + + + +
RateValue
15%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/d0f69e/6bb6ca5dcdbd1fef063561f61de88740db24bd5d.ts b/tests/act/tests/d0f69e/6bb6ca5dcdbd1fef063561f61de88740db24bd5d.ts new file mode 100644 index 00000000..028de394 --- /dev/null +++ b/tests/act/tests/d0f69e/6bb6ca5dcdbd1fef063561f61de88740db24bd5d.ts @@ -0,0 +1,35 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Failed Example 2 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/6bb6ca5dcdbd1fef063561f61de88740db24bd5d.html)", async () => { + const document = parser.parseFromString(` + + + Failed Example 2 + + + + + + + + + + + +
CountryStarting with a Z
ZambiaZimbabwe
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.not.be.empty; + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + expect(results.some(r => expectedUrls.includes(r.url))).to.be.true; + }); +}); diff --git a/tests/act/tests/d0f69e/9fbe21d1ffdb176ef89afc95cc3f4f666353ee25.ts b/tests/act/tests/d0f69e/9fbe21d1ffdb176ef89afc95cc3f4f666353ee25.ts new file mode 100644 index 00000000..8c69b076 --- /dev/null +++ b/tests/act/tests/d0f69e/9fbe21d1ffdb176ef89afc95cc3f4f666353ee25.ts @@ -0,0 +1,38 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 3 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/9fbe21d1ffdb176ef89afc95cc3f4f666353ee25.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 3 + + + + + + + + + + + + + + +
ProjectsExams
15%
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/act/tests/d0f69e/be8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.ts b/tests/act/tests/d0f69e/be8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.ts new file mode 100644 index 00000000..eaf3d748 --- /dev/null +++ b/tests/act/tests/d0f69e/be8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.ts @@ -0,0 +1,43 @@ +import { expect } from "@open-wc/testing"; +import { scan } from "../../../../src/scanner"; + +const parser = new DOMParser(); + +describe("[d0f69e]Table header cell has assigned cells", function () { + it("Passed Example 2 (https://www.w3.org/WAI/content-assets/wcag-act-rules/testcases/d0f69e/be8acb4fa0dd3057dd28f7cc43e64a95eff15ac6.html)", async () => { + const document = parser.parseFromString(` + + + Passed Example 2 + + +
+
+
+ Month + Top Temperature +
+
+
+
+ July + 40 C +
+
+ August + 45 C +
+
+
+ +`, 'text/html'); + + const results = (await scan(document.body)).map(({ text, url }) => { + return { text, url }; + }); + + const expectedUrls = ["https://dequeuniversity.com/rules/axe/4.11/th-has-data-cells"]; + const relevant = results.filter(r => expectedUrls.includes(r.url)); + expect(relevant).to.be.empty; + }); +}); diff --git a/tests/th-has-data-cells.ts b/tests/th-has-data-cells.ts index 024d196e..11acb04f 100644 --- a/tests/th-has-data-cells.ts +++ b/tests/th-has-data-cells.ts @@ -136,7 +136,7 @@ describe("th-has-data-cells", function () { expect(results.length).to.equal(1); }); - it("th in column with no data cells", async () => { + it("column header with explicit headers override has no assigned cells", async () => { const table = await fixture(html` @@ -145,13 +145,14 @@ describe("th-has-data-cells", function () { - +
Data 1Not a data cellData 2
`); const results = await scanner.scan(table); - // header2 has no data cells in its column + // header2 has no assigned cells: the td below it uses explicit + // headers="header1" which overrides the implicit column assignment expect(results.length).to.equal(1); }); });