diff --git a/src/core/best-practices.js b/src/core/best-practices.js index ea48d5f2d1..f468291bd2 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("h1, 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/src/core/caniuse.js b/src/core/caniuse.js index a78896c327..4462c6b5df 100644 --- a/src/core/caniuse.js +++ b/src/core/caniuse.js @@ -195,22 +195,28 @@ 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 ariaLabel = `${feature} is ${supportLevel} since ${browserName} on ${type}.`; - const cssClass = `caniuse-cell ${caniuse}`; - const title = capitalize(`${supportLevel} since ${browserName}.`); const textVersion = version ? 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 ${name}${versionSuffix}.`); const src = getLogoSrc(browserId); const result = html` -
+ `; diff --git a/src/core/style.js b/src/core/style.js index 8f3c4c2029..6d95a14375 100644 --- a/src/core/style.js +++ b/src/core/style.js @@ -25,7 +25,15 @@ function insertStyle() { const styleElement = document.createElement("style"); styleElement.id = "respec-mainstyle"; styleElement.textContent = css; - document.head.appendChild(styleElement); + // Insert before the first stylesheet or style element so author-provided + // 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, + document.head.querySelector("link[rel~='stylesheet'], style") + ); return styleElement; } 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/best-practices-spec.js b/tests/spec/core/best-practices-spec.js index 6d4a1fdd77..1446d5c095 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("h1, h2, h3, h4, h5, h6"); + expect(headings).toHaveSize(1); + expect(headings[0].textContent).toContain("Custom Heading"); + }); }); diff --git a/tests/spec/core/caniuse-spec.js b/tests/spec/core/caniuse-spec.js index f0fe7a6ae2..e1c676daa8 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] = @@ -112,6 +116,12 @@ describe("Core — Can I Use", () => { 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"); diff --git a/tests/spec/core/style-spec.js b/tests/spec/core/style-spec.js index 06048ac159..fd7f06f586 100644 --- a/tests/spec/core/style-spec.js +++ b/tests/spec/core/style-spec.js @@ -7,4 +7,21 @@ describe("Core — Style", () => { const style = doc.getElementById("respec-mainstyle"); expect(style).toBeTruthy(); }); + + it("inserts respec-mainstyle before author-provided stylesheets", async () => { + 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(); + 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(); + }); });