From 12e96fd6a562ba7c3b9adbb596d0a6ca1f663163 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sun, 12 Apr 2026 16:20:54 +1000 Subject: [PATCH 01/13] fix(caniuse): add role=img to browser support cells for valid HTML The div has aria-label with the full description, so children are presentational. Set inner img alt to empty to avoid double-announcement. --- src/core/caniuse.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/caniuse.js b/src/core/caniuse.js index 5d7f36612c..c57a6affe4 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -171,13 +171,18 @@ function browserCellRenderer(feature) { const textVersion = version ? version : "—"; const src = getLogoSrc(browserId); const result = html` -
+ `; From 5ef5330e2b540ee2bea8143e61906c1e81ed5cbd Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 13 Apr 2026 11:23:27 +1000 Subject: [PATCH 02/13] test(caniuse): update test expectations for role=img change --- tests/spec/core/caniuse-spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/spec/core/caniuse-spec.js b/tests/spec/core/caniuse-spec.js index f0fe7a6ae2..47e0ad310f 100644 --- a/tests/spec/core/caniuse-spec.js +++ b/tests/spec/core/caniuse-spec.js @@ -103,7 +103,11 @@ describe("Core — Can I Use", () => { expect(firefox.width).toBe(20); expect(firefox.height).toBe(20); - expect(chrome.alt).toBe("Android Chrome logo"); + expect(chrome.alt).toBe(""); + + // The parent cell has role=img with aria-label for accessibility + const firstCell = cells[0]; + expect(firstCell.getAttribute("role")).toBe("img"); // The version numbers const [firefoxVersion, chromeVersion, safariVersion] = From 6547bd687234af697e4a41f73ac8513998163d49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:38:40 +0000 Subject: [PATCH 03/13] fix(caniuse): include version in aria-label even when version is unknown Agent-Logs-Url: https://github.com/speced/respec/sessions/391940a3-1f50-400e-849b-39f06e39e7bb Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- src/core/caniuse.js | 5 +++-- tests/spec/core/caniuse-spec.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/caniuse.js b/src/core/caniuse.js index c57a6affe4..f3ade68314 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -165,10 +165,11 @@ function browserCellRenderer(feature) { const versionLong = version ? ` version ${version}` : ""; const browserName = `${name}${versionLong}`; const supportLevel = statToText.get(caniuse); - const ariaLabel = `${feature} is ${supportLevel} since ${browserName} on ${type}.`; + const textVersion = version ?? "—"; + const versionSuffix = version ? ` version ${version}` : " (version unknown)"; + const ariaLabel = `${feature} is ${supportLevel} since ${name}${versionSuffix} on ${type}.`; const cssClass = `caniuse-cell ${caniuse}`; const title = capitalize(`${supportLevel} since ${browserName}.`); - const textVersion = version ? version : "—"; const src = getLogoSrc(browserId); const result = html`
{ expect(firefoxVersion.textContent).toBe("66"); expect(safariVersion.textContent).toBe("—"); + // aria-label for no-version cell uses "(version unknown)" to match visible "—" + const safariCell = safariVersion.closest(".caniuse-cell"); + expect(safariCell.getAttribute("aria-label")).toBe( + "FEATURE is unknown support since iOS Safari (version unknown) on mobile." + ); + // More info link const moreInfoLink = cells.item(3); expect(moreInfoLink.href).toBe("https://caniuse.com/FEATURE"); From fb850cfbd1260dd3ea088fcb1aef334ef0026b16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:02:16 +0000 Subject: [PATCH 04/13] fix(caniuse): format ternary per prettier rules to fix lint CI Agent-Logs-Url: https://github.com/speced/respec/sessions/6ae7a1ff-7486-4388-b079-084233604857 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- src/core/caniuse.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/caniuse.js b/src/core/caniuse.js index f3ade68314..235fba3c7e 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -166,7 +166,9 @@ function browserCellRenderer(feature) { const browserName = `${name}${versionLong}`; const supportLevel = statToText.get(caniuse); const textVersion = version ?? "—"; - const versionSuffix = version ? ` version ${version}` : " (version unknown)"; + const versionSuffix = version + ? ` version ${version}` + : " (version unknown)"; const ariaLabel = `${feature} is ${supportLevel} since ${name}${versionSuffix} on ${type}.`; const cssClass = `caniuse-cell ${caniuse}`; const title = capitalize(`${supportLevel} since ${browserName}.`); From 6cb969fd0ef116cec8436ee255df0fcd593ceaef Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 18 Apr 2026 05:24:14 +1000 Subject: [PATCH 05/13] fix(caniuse): revert ?? to ternary to handle empty-string version The nullish coalescing operator treats "" as non-nullish, so an empty-string version would display as blank instead of the em dash fallback. The ternary correctly maps falsy values (including "") to the fallback. --- src/core/caniuse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/caniuse.js b/src/core/caniuse.js index 4b7d131d64..d6a2a196b0 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -198,7 +198,7 @@ function browserCellRenderer(feature) { const versionLong = version ? ` version ${version}` : ""; const browserName = `${name}${versionLong}`; const supportLevel = statToText.get(caniuse); - const textVersion = version ?? "—"; + const textVersion = version ? version : "—"; const versionSuffix = version ? ` version ${version}` : " (version unknown)"; From 3ee9d5695e0e63db36b85e4fd6d8fc1a662297b0 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 18 Apr 2026 13:34:32 +1000 Subject: [PATCH 06/13] fix(caniuse): unify title and aria-label text for unknown version --- src/core/caniuse.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/caniuse.js b/src/core/caniuse.js index d6a2a196b0..4462c6b5df 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -195,8 +195,6 @@ function browserCellRenderer(feature) { return (groups, { browser: browserId, version, caniuse }) => { const entry = BROWSERS.get(browserId); const { name, type } = entry ?? { name: browserId, type: "desktop" }; - const versionLong = version ? ` version ${version}` : ""; - const browserName = `${name}${versionLong}`; const supportLevel = statToText.get(caniuse); const textVersion = version ? version : "—"; const versionSuffix = version @@ -204,7 +202,7 @@ function browserCellRenderer(feature) { : " (version unknown)"; const ariaLabel = `${feature} is ${supportLevel} since ${name}${versionSuffix} on ${type}.`; const cssClass = `caniuse-cell ${caniuse}`; - const title = capitalize(`${supportLevel} since ${browserName}.`); + const title = capitalize(`${supportLevel} since ${name}${versionSuffix}.`); const src = getLogoSrc(browserId); const result = html`
Date: Sat, 18 Apr 2026 19:15:55 +1000 Subject: [PATCH 07/13] fix(best-practices): respect existing heading in #bp-summary Avoid injecting a duplicate heading when #bp-summary already has one, and use prepend() so the generated heading always appears at the top. Closes #5176 --- src/core/best-practices.js | 7 +++++-- tests/spec/core/best-practices-spec.js | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/core/best-practices.js b/src/core/best-practices.js index ea48d5f2d1..121f30b2ad 100644 --- a/src/core/best-practices.js +++ b/src/core/best-practices.js @@ -57,8 +57,11 @@ export function run() { }); if (bps.length) { if (bpSummary) { - bpSummary.appendChild(html`

Best Practices Summary

`); - if (summaryItems) bpSummary.appendChild(summaryItems); + const existingHeading = bpSummary.querySelector("h2, h3, h4, h5, h6"); + if (!existingHeading) { + bpSummary.prepend(html`

Best Practices Summary

`); + } + if (summaryItems) bpSummary.append(summaryItems); } } else if (bpSummary) { const msg = `Using best practices summary (#bp-summary) but no best practices found.`; diff --git a/tests/spec/core/best-practices-spec.js b/tests/spec/core/best-practices-spec.js index 6d4a1fdd77..f4714395f4 100644 --- a/tests/spec/core/best-practices-spec.js +++ b/tests/spec/core/best-practices-spec.js @@ -81,4 +81,25 @@ describe("Core — Best Practices", () => { ); expect(bps.querySelectorAll("ul li")).toHaveSize(3); }); + + it("does not duplicate heading when bp-summary already has one", async () => { + const body = ` +
+

Section

+ BP1 +
+

Custom Heading

+
+
+ `; + const ops = { + config: makeBasicConfig(), + body, + }; + const doc = await makeRSDoc(ops); + const bpSummary = doc.getElementById("bp-summary"); + const headings = bpSummary.querySelectorAll("h2, h3, h4, h5, h6"); + expect(headings).toHaveSize(1); + expect(headings[0].textContent).toBe("Custom Heading"); + }); }); From 3e00a49ddabba36e1b20a805b006886a65a56c33 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 18 Apr 2026 19:33:03 +1000 Subject: [PATCH 08/13] fix(core/style): insert respec-mainstyle before author-provided stylesheets Author custom CSS placed in before the ReSpec script was getting overridden because respec-mainstyle was appended after it. Use insertBefore to target the first instead, so author stylesheets remain last. Closes #5035 --- src/core/style.js | 5 ++++- tests/spec/core/custom-style.html | 18 ++++++++++++++++++ tests/spec/core/style-spec.js | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/spec/core/custom-style.html diff --git a/src/core/style.js b/src/core/style.js index 8f3c4c2029..5457cb8065 100644 --- a/src/core/style.js +++ b/src/core/style.js @@ -25,7 +25,10 @@ function insertStyle() { const styleElement = document.createElement("style"); styleElement.id = "respec-mainstyle"; styleElement.textContent = css; - document.head.appendChild(styleElement); + // Insert before any existing elements so author-provided stylesheets + // retain their position after all ReSpec-injected styles, letting custom CSS + // override ReSpec defaults. insertBefore(el, null) === appendChild. + document.head.insertBefore(styleElement, document.head.querySelector("link")); return styleElement; } diff --git a/tests/spec/core/custom-style.html b/tests/spec/core/custom-style.html new file mode 100644 index 0000000000..66520a4139 --- /dev/null +++ b/tests/spec/core/custom-style.html @@ -0,0 +1,18 @@ + + + + Custom Style Test + + +
+

Custom style ordering test

+
+
+

Test doc for verifying custom CSS insertion order.

+
+ diff --git a/tests/spec/core/style-spec.js b/tests/spec/core/style-spec.js index 06048ac159..510b9ea0d4 100644 --- a/tests/spec/core/style-spec.js +++ b/tests/spec/core/style-spec.js @@ -7,4 +7,18 @@ describe("Core — Style", () => { const style = doc.getElementById("respec-mainstyle"); expect(style).toBeTruthy(); }); + + it("inserts respec-mainstyle before author-provided stylesheets", async () => { + const doc = await makeRSDoc( + makeStandardOps(), + "spec/core/custom-style.html" + ); + const respecStyle = doc.getElementById("respec-mainstyle"); + const authorLink = doc.querySelector("link.custom-author-style"); + expect(respecStyle).toBeTruthy(); + expect(authorLink).toBeTruthy(); + // Bitmask: Node.DOCUMENT_POSITION_FOLLOWING = 4 means respecStyle comes before authorLink + const position = respecStyle.compareDocumentPosition(authorLink); + expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); }); From fadcda66bbf011565ff78bb7de38e14b3c52cec0 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 18 Apr 2026 19:57:44 +1000 Subject: [PATCH 09/13] fix(style): narrow selector to avoid displacing non-stylesheet links querySelector("link") matched ALL link types including preconnect and icon links, potentially moving respec-mainstyle ahead of those and altering document head order unexpectedly. Narrow to "link[rel~='stylesheet'], style" so only stylesheet links are used as the insertion point. --- src/core/style.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/style.js b/src/core/style.js index 5457cb8065..e5963d2292 100644 --- a/src/core/style.js +++ b/src/core/style.js @@ -25,10 +25,15 @@ function insertStyle() { const styleElement = document.createElement("style"); styleElement.id = "respec-mainstyle"; styleElement.textContent = css; - // Insert before any existing elements so author-provided stylesheets - // retain their position after all ReSpec-injected styles, letting custom CSS - // override ReSpec defaults. insertBefore(el, null) === appendChild. - document.head.insertBefore(styleElement, document.head.querySelector("link")); + // Insert before the first stylesheet or style element so author-provided + // stylesheets retain their position after all ReSpec-injected styles, letting + // custom CSS override ReSpec defaults. Using "link[rel~='stylesheet'], style" + // avoids displacing preconnect, icon, or other non-stylesheet link elements. + // insertBefore(el, null) === appendChild when no match is found. + document.head.insertBefore( + styleElement, + document.head.querySelector("link[rel~='stylesheet'], style") + ); return styleElement; } From 39ef0381ad7532780d268c19d46d2b41d08a02b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:17:25 +0000 Subject: [PATCH 10/13] test(style): inline style-order fixture setup in spec helper ops Agent-Logs-Url: https://github.com/speced/respec/sessions/22993ec7-f01b-4d55-b22a-d8fa87521f2a --- tests/spec/SpecHelper.js | 7 +++++++ tests/spec/core/custom-style.html | 18 ------------------ tests/spec/core/style-spec.js | 9 ++++++--- 3 files changed, 13 insertions(+), 21 deletions(-) delete mode 100644 tests/spec/core/custom-style.html diff --git a/tests/spec/SpecHelper.js b/tests/spec/SpecHelper.js index 9eb6c0f2da..6b683ddc5b 100644 --- a/tests/spec/SpecHelper.js +++ b/tests/spec/SpecHelper.js @@ -146,6 +146,12 @@ function decorateDocument(doc, opts) { }); } + function decorateHead({ head = "" }) { + if (head) { + doc.head.insertAdjacentHTML("beforeend", head); + } + } + if (opts.htmlAttrs) { Object.keys(opts.htmlAttrs).reduce( intoAttributes.bind(opts.htmlAttrs), @@ -156,6 +162,7 @@ function decorateDocument(doc, opts) { doc.title = opts.title; } decorateBody(opts); + decorateHead(opts); addRespecConfig(opts); if (!doc.querySelector("script[src]")) { addReSpecLoader(opts); diff --git a/tests/spec/core/custom-style.html b/tests/spec/core/custom-style.html deleted file mode 100644 index 66520a4139..0000000000 --- a/tests/spec/core/custom-style.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Custom Style Test - - -
-

Custom style ordering test

-
-
-

Test doc for verifying custom CSS insertion order.

-
- diff --git a/tests/spec/core/style-spec.js b/tests/spec/core/style-spec.js index 510b9ea0d4..fd7f06f586 100644 --- a/tests/spec/core/style-spec.js +++ b/tests/spec/core/style-spec.js @@ -9,10 +9,13 @@ describe("Core — Style", () => { }); it("inserts respec-mainstyle before author-provided stylesheets", async () => { - const doc = await makeRSDoc( - makeStandardOps(), - "spec/core/custom-style.html" + const ops = makeStandardOps( + {}, + "

foo

" ); + ops.head = + ''; + const doc = await makeRSDoc(ops); const respecStyle = doc.getElementById("respec-mainstyle"); const authorLink = doc.querySelector("link.custom-author-style"); expect(respecStyle).toBeTruthy(); From a7526f5d6125171fa9c07bf064364101895fa58c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:19:55 +0000 Subject: [PATCH 11/13] fix(best-practices): treat existing h1 as summary heading Agent-Logs-Url: https://github.com/speced/respec/sessions/2f9d0835-9497-4acc-a4b7-90047bf4c773 --- src/core/best-practices.js | 2 +- tests/spec/core/best-practices-spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/best-practices.js b/src/core/best-practices.js index 121f30b2ad..f468291bd2 100644 --- a/src/core/best-practices.js +++ b/src/core/best-practices.js @@ -57,7 +57,7 @@ export function run() { }); if (bps.length) { if (bpSummary) { - const existingHeading = bpSummary.querySelector("h2, h3, h4, h5, h6"); + const existingHeading = bpSummary.querySelector("h1, h2, h3, h4, h5, h6"); if (!existingHeading) { bpSummary.prepend(html`

Best Practices Summary

`); } diff --git a/tests/spec/core/best-practices-spec.js b/tests/spec/core/best-practices-spec.js index f4714395f4..6533ab76f6 100644 --- a/tests/spec/core/best-practices-spec.js +++ b/tests/spec/core/best-practices-spec.js @@ -88,7 +88,7 @@ describe("Core — Best Practices", () => {

Section

BP1
-

Custom Heading

+

Custom Heading

`; @@ -98,7 +98,7 @@ describe("Core — Best Practices", () => { }; const doc = await makeRSDoc(ops); const bpSummary = doc.getElementById("bp-summary"); - const headings = bpSummary.querySelectorAll("h2, h3, h4, h5, h6"); + const headings = bpSummary.querySelectorAll("h1, h2, h3, h4, h5, h6"); expect(headings).toHaveSize(1); expect(headings[0].textContent).toBe("Custom Heading"); }); From 1e081f6d8ee74a52e1c22b6be6bec818ed3b4c74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:28:16 +0000 Subject: [PATCH 12/13] test(best-practices): allow section numbering in custom summary heading assertion Agent-Logs-Url: https://github.com/speced/respec/sessions/c34dc5ec-ef27-48b0-aacc-d3bd0965ec64 --- tests/spec/core/best-practices-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spec/core/best-practices-spec.js b/tests/spec/core/best-practices-spec.js index 6533ab76f6..1446d5c095 100644 --- a/tests/spec/core/best-practices-spec.js +++ b/tests/spec/core/best-practices-spec.js @@ -100,6 +100,6 @@ describe("Core — Best Practices", () => { const bpSummary = doc.getElementById("bp-summary"); const headings = bpSummary.querySelectorAll("h1, h2, h3, h4, h5, h6"); expect(headings).toHaveSize(1); - expect(headings[0].textContent).toBe("Custom Heading"); + expect(headings[0].textContent).toContain("Custom Heading"); }); }); From c7e815f5c908bc6aa3b767996d750a892dabdc89 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sun, 26 Apr 2026 13:53:50 +1000 Subject: [PATCH 13/13] fix(core/style): clarify comment about stylesheet ordering Reword to describe respec-mainstyle ordering specifically, not all ReSpec-injected styles (other modules append later). --- src/core/style.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/style.js b/src/core/style.js index e5963d2292..6d95a14375 100644 --- a/src/core/style.js +++ b/src/core/style.js @@ -26,9 +26,9 @@ function insertStyle() { styleElement.id = "respec-mainstyle"; styleElement.textContent = css; // Insert before the first stylesheet or style element so author-provided - // stylesheets retain their position after all ReSpec-injected styles, letting - // custom CSS override ReSpec defaults. Using "link[rel~='stylesheet'], style" - // avoids displacing preconnect, icon, or other non-stylesheet link elements. + // stylesheets that are already in keep their later position, letting + // custom CSS override ReSpec's main stylesheet. Using "link[rel~='stylesheet'], + // style" avoids displacing preconnect, icon, or other non-stylesheet links. // insertBefore(el, null) === appendChild when no match is found. document.head.insertBefore( styleElement,