From 6688a53acc423c2d83bbb928e02b148afb3e319c Mon Sep 17 00:00:00 2001 From: Aden Linday Date: Mon, 24 Aug 2026 13:29:02 +0930 Subject: [PATCH] feat: cleanup and improve detection delay on kitten plugin --- jest.config.js | 2 - src/SEQTA.ts | 96 ++++++------ .../built-in/errorPageKitten/index.test.ts | 107 ++++--------- src/plugins/built-in/errorPageKitten/index.ts | 140 +++++------------- src/plugins/built-in/errorPageKitten/lazy.ts | 14 -- .../built-in/errorPageKitten/styles.css | 12 +- src/plugins/core/manager.ts | 3 - src/plugins/index.ts | 4 +- src/test/mocks/assetStub.ts | 2 - src/test/mocks/inlineStub.ts | 2 - src/test/mocks/webextension-polyfill.ts | 3 - 11 files changed, 111 insertions(+), 274 deletions(-) delete mode 100644 src/plugins/built-in/errorPageKitten/lazy.ts delete mode 100644 src/test/mocks/assetStub.ts delete mode 100644 src/test/mocks/inlineStub.ts diff --git a/jest.config.js b/jest.config.js index 090cf8fc..a63ca1c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,8 +14,6 @@ export default { '/node_modules/(?!(color|color-string|color-convert|color-name)/)', ], moduleNameMapper: { - '\\.png$': '/src/test/mocks/assetStub.ts', - '\\?inline$': '/src/test/mocks/inlineStub.ts', '^@/(.*)$': '/src/$1', '^color$': '/src/test/mocks/color.ts', '^webextension-polyfill$': '/src/test/mocks/webextension-polyfill.ts', diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 142554b2..219482b0 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -53,9 +53,8 @@ if (document.childNodes[1]) { init(); } -// The 404 page is a bare document with no second childNode, so it must be -// booted unconditionally (outside the gate above). It self-guards on title. -bootErrorPage(); +// Standalone 404 documents never pass the SEQTA SPA gate above. +void bootErrorPage(); if (import.meta.env.DEV) { window.addEventListener("unhandledrejection", (event) => { @@ -142,65 +141,68 @@ async function init() { } } -// The 404 page is a standalone document that never passes the SPA gate above, -// so the normal plugin path never boots there; start just the classic kitten -// 404 plugin so it renders without any SPA work. +function shouldBootKitten404(): boolean { + if (IsSEQTAPage || document.getElementById("container")) return false; + return /404|not found/i.test(document.title); +} + +/** Hide the rebranded 404 before the kitten card mounts (removed after boot). */ +function inject404FlashHide(): void { + if (document.getElementById("bsplus-404-flash-hide")) return; + const style = document.createElement("style"); + style.id = "bsplus-404-flash-hide"; + style.textContent = + "html,body{background:#333!important}.message{display:none!important}"; + (document.head ?? document.documentElement).appendChild(style); +} + async function bootErrorPage() { if (IsSEQTAPage) return; - // Runs at document_start, so and body are not available yet; wait - // for the DOM before testing the 404 title. - await new Promise<void>((resolve) => { - if (document.readyState !== "loading") { - resolve(); - return; - } - document.addEventListener("DOMContentLoaded", () => resolve(), { - once: true, - }); - }); - - const is404Page = - /404/.test(document.title) || /not found/i.test(document.title); - if (!is404Page) return; - - const stored = await browser.storage.local.get([ + const storagePromise = browser.storage.local.get([ "onoff", "plugin.error-page-kitten.settings", ]); - if ((stored.onoff ?? true) === false) return; - const kittenEnabled = ( - stored["plugin.error-page-kitten.settings"] as - | { enabled?: boolean } - | undefined - )?.enabled; - if (kittenEnabled === false) return; - hideRebranded404(); + const watchTitle = () => { + if (shouldBootKitten404()) inject404FlashHide(); + }; + watchTitle(); + if (document.readyState === "loading") { + document.addEventListener("readystatechange", watchTitle); + } + + if (document.readyState === "loading") { + await new Promise<void>((resolve) => { + document.addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }); + } + document.removeEventListener("readystatechange", watchTitle); + + if (!shouldBootKitten404()) return; + + const stored = await storagePromise; + if ((stored.onoff ?? true) === false) return; + const kittenSettings = stored["plugin.error-page-kitten.settings"] as + | { enabled?: boolean } + | undefined; + if (kittenSettings?.enabled === false) return; try { - const { pluginManager } = await import("@/plugins/index"); - await pluginManager.startPlugin("error-page-kitten"); + const { mountErrorPageKitten } = await import( + "@/plugins/built-in/errorPageKitten" + ); + mountErrorPageKitten(); + document.getElementById("bsplus-404-flash-hide")?.remove(); } catch (error) { - // Restore the rebranded page so the user is not left with a blank document. + document.getElementById("bsplus-404-flash-hide")?.remove(); document - .querySelectorAll<HTMLElement>(".bsplus-kitten-404-hidden") - .forEach((el) => { - el.classList.remove("bsplus-kitten-404-hidden"); - el.style.display = ""; - }); + .querySelectorAll(".bsplus-kitten-404-hidden") + .forEach((el) => el.classList.remove("bsplus-kitten-404-hidden")); console.error("[BetterSEQTA+] Failed to boot 404 page:", error); } } -function hideRebranded404() { - const message = document.querySelector<HTMLElement>(".message"); - if (message) { - message.classList.add("bsplus-kitten-404-hidden"); - message.style.display = "none"; - } -} - function replaceIcons() { document .querySelectorAll<HTMLLinkElement>('link[rel*="icon"]') diff --git a/src/plugins/built-in/errorPageKitten/index.test.ts b/src/plugins/built-in/errorPageKitten/index.test.ts index 655a0fbc..f3144e3f 100644 --- a/src/plugins/built-in/errorPageKitten/index.test.ts +++ b/src/plugins/built-in/errorPageKitten/index.test.ts @@ -3,65 +3,33 @@ */ /// <reference types="jest" /> import type { PluginAPI } from "@/plugins/core/types"; -import errorPageKittenPlugin from "./index"; + +jest.mock("@/lib/extensionAssetUrl", () => ({ + resolveExtensionAssetUrl: (url: string) => + `chrome-extension://test/${String(url).replace(/^\/+/, "")}`, +})); +jest.mock("@/resources/error-page/kitten.png", () => ({ + __esModule: true, + default: "/resources/error-page/kitten.png", +})); +jest.mock("./styles.css?inline", () => ({ + __esModule: true, + default: "/* test */", +})); + +import { mountErrorPageKitten } from "./index"; const REBRANDED_404_BODY = ` <div class="message"> <h1>Page not found</h1> <p>We can't find the page you're looking for.</p> - <p>It may have been moved, deleted or the link might be out of date.</p> - <p class="error-ref">Ref: 404</p> </div> `; -const api = { - settings: { loaded: Promise.resolve() }, -} as unknown as PluginAPI<{}>; - -async function startPlugin(): Promise<() => void> { - const cleanup = (await errorPageKittenPlugin.run(api)) ?? (() => {}); - return cleanup; -} - function getCard(): HTMLElement | null { - // The card container shares its class with <html>, so scope to body. return document.body.querySelector<HTMLElement>(".bsplus-kitten-404"); } -function expectKittenRendered(): void { - const card = getCard(); - expect(card).not.toBeNull(); - expect(document.documentElement.classList).toContain("bsplus-kitten-404"); - expect(card?.querySelector("h1")?.textContent).toBe("404 Not Found"); - // The original document's title. - expect(document.title).toBe("404 Not Found"); - // The reference page's exact vendor-prefixed banner CSS is shipped at runtime. - const stripeStyle = Array.from( - document.head.querySelectorAll("style"), - ).find((s) => s.textContent?.includes("-webkit-repeating-linear-gradient")); - expect(stripeStyle?.textContent).toContain( - "repeating-linear-gradient(45deg", - ); - const img = card?.querySelector(".kitten img") as HTMLImageElement | null; - expect(img?.getAttribute("alt")).toBeNull(); - expect(img?.src).toContain("kitten"); - const flickrLink = card?.querySelector( - ".attrib a", - ) as HTMLAnchorElement | null; - expect(flickrLink?.textContent).toBe("by storyvillegirl"); - expect(flickrLink?.href).toBe( - "http://www.flickr.com/photos/bibbit/2756165489/", - ); - const seqtaLink = card?.querySelector("a[href='http://www.seqta.com.au']"); - expect(seqtaLink?.textContent).toBe("SEQTA"); - // Original rebranded message is hidden. - expect( - document.querySelector(".message")?.classList.contains( - "bsplus-kitten-404-hidden", - ), - ).toBe(true); -} - describe("errorPageKitten", () => { beforeEach(() => { document.body.innerHTML = ""; @@ -69,47 +37,24 @@ describe("errorPageKitten", () => { document.documentElement.classList.remove("bsplus-kitten-404"); }); - it("renders the classic kitten 404 card over the rebranded page", async () => { + it("mounts the classic kitten 404 card", () => { document.title = "Page not found"; document.body.innerHTML = REBRANDED_404_BODY; - const cleanup = await startPlugin(); + const cleanup = mountErrorPageKitten(); - expectKittenRendered(); + const card = getCard(); + expect(card).not.toBeNull(); + expect(document.documentElement.classList).toContain("bsplus-kitten-404"); + expect(card?.querySelector("h1")?.textContent).toBe("404 Not Found"); + expect(document.title).toBe("404 Not Found"); + expect(card?.querySelector(".kitten img")?.getAttribute("src")).toContain("kitten"); + expect( + document.querySelector(".message")?.classList.contains("bsplus-kitten-404-hidden"), + ).toBe(true); cleanup(); expect(getCard()).toBeNull(); expect(document.title).toBe("Page not found"); - expect( - Array.from(document.head.querySelectorAll("style")).some((s) => - s.textContent?.includes("-webkit-repeating-linear-gradient"), - ), - ).toBe(false); - expect( - document.documentElement.classList.contains("bsplus-kitten-404"), - ).toBe(false); - expect( - document - .querySelector(".message") - ?.classList.contains("bsplus-kitten-404-hidden"), - ).toBe(false); - }); - - it("does not render inside the SPA (no .message, has #container)", async () => { - document.title = "SEQTA"; - document.body.innerHTML = '<div id="container"></div>'; - - await startPlugin(); - - expect(getCard()).toBeNull(); - }); - - it("does not render on an unrelated page", async () => { - document.title = "Home"; - document.body.innerHTML = '<div id="app"></div>'; - - await startPlugin(); - - expect(getCard()).toBeNull(); }); }); diff --git a/src/plugins/built-in/errorPageKitten/index.ts b/src/plugins/built-in/errorPageKitten/index.ts index c2731eb5..624a06b0 100644 --- a/src/plugins/built-in/errorPageKitten/index.ts +++ b/src/plugins/built-in/errorPageKitten/index.ts @@ -4,72 +4,43 @@ import kittenPng from "@/resources/error-page/kitten.png"; import styles from "./styles.css?inline"; const KITTEN_IMG = resolveExtensionAssetUrl(kittenPng); -const KITTEN_CLASS = "bsplus-kitten-404"; +const ROOT_CLASS = "bsplus-kitten-404"; -// The reference page's exact h1 background declarations, in the original -// order. Each engine keeps the last declaration it understands (-webkit in -// Blink, -moz in Gecko, the legacy standard otherwise), so shipping them -// byte-for-byte reproduces the reference banner on every engine. Lives in a -// JS string because the CSS minifier drops vendor-prefixed declarations. -const H1_STRIPE_CSS = - "html.bsplus-kitten-404 .bsplus-kitten-404>h1{background-image:-moz-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -moz-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:-webkit-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -webkit-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:-o-repeating-linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), -o-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1));background-image:repeating-linear-gradient(45deg, rgba(0,0,0,0), rgba(0,0,0,0) 8px, rgba(0,0,0,0.05) 8px, rgba(0,0,0,0.05) 16px), linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.1))}"; +const CARD_HTML = `<h1>404 Not Found</h1> +<p>Sorry — the resource you are looking for could not be found.</p> +<p>If you believe you are seeing this message in error, please contact your IT department.</p> +<div class="kitten"> + <p>I did find this picture of a kitten for you, though!</p> + <img src="${KITTEN_IMG}" alt=""> + <div class="attrib">CC-BY-SA <a href="http://www.flickr.com/photos/bibbit/2756165489/">by storyvillegirl</a></div> +</div> +<a href="http://www.seqta.com.au">SEQTA</a>`; -// The 404 page (legacy and rebranded) is a standalone document built around a -// single `.message` block; the real app mounts a `#container`. Require the -// former and rule out the latter so the plugin never renders inside the app. -function is404Page(): boolean { - if (document.getElementById("container")) return false; - const heading = document.querySelector(".message > h1"); - const text = heading?.textContent ?? document.title; - return /not found/i.test(text) || /404/.test(document.title); -} +/** Boot path in SEQTA.ts gates when this runs; keep render-only here. */ +export function mountErrorPageKitten(): () => void { + const styleEl = document.createElement("style"); + styleEl.textContent = styles; + document.head.appendChild(styleEl); + + const originalTitle = document.title; + document.querySelector(".message")?.classList.add("bsplus-kitten-404-hidden"); + document.documentElement.classList.add(ROOT_CLASS); + document.title = "404 Not Found"; -function buildCard(): HTMLElement { const card = document.createElement("div"); - card.className = KITTEN_CLASS; + card.className = ROOT_CLASS; + card.innerHTML = CARD_HTML; + document.body.appendChild(card); - const h1 = document.createElement("h1"); - h1.textContent = "404 Not Found"; - card.appendChild(h1); - - const p1 = document.createElement("p"); - p1.textContent = - "Sorry — the resource you are looking for could not be found."; - card.appendChild(p1); - - const p2 = document.createElement("p"); - p2.textContent = - "If you believe you are seeing this message in error, please contact your IT department."; - card.appendChild(p2); - - const kitten = document.createElement("div"); - kitten.className = "kitten"; - - const kittenLine = document.createElement("p"); - kittenLine.textContent = "I did find this picture of a kitten for you, though!"; - kitten.appendChild(kittenLine); - - const img = document.createElement("img"); - img.src = KITTEN_IMG; - kitten.appendChild(img); - - const attrib = document.createElement("div"); - attrib.className = "attrib"; - attrib.append("CC-BY-SA "); - const link = document.createElement("a"); - link.href = "http://www.flickr.com/photos/bibbit/2756165489/"; - link.textContent = "by storyvillegirl"; - attrib.appendChild(link); - kitten.appendChild(attrib); - - card.appendChild(kitten); - - const seqtaLink = document.createElement("a"); - seqtaLink.href = "http://www.seqta.com.au"; - seqtaLink.textContent = "SEQTA"; - card.appendChild(seqtaLink); - - return card; + return () => { + styleEl.remove(); + document.documentElement.classList.remove(ROOT_CLASS); + document + .querySelectorAll(".bsplus-kitten-404-hidden") + .forEach((el) => el.classList.remove("bsplus-kitten-404-hidden")); + document.title = originalTitle; + card.remove(); + }; } const errorPageKittenPlugin: Plugin = { @@ -81,52 +52,7 @@ const errorPageKittenPlugin: Plugin = { disableToggle: true, defaultEnabled: true, styles, - - run: async (_api) => { - let card: HTMLElement | null = null; - let hasBuilt = false; - let originalTitle = document.title; - let stripeStyle: HTMLStyleElement | null = null; - - const render = () => { - if (!is404Page()) return; - - document.querySelector(".message")?.classList.add("bsplus-kitten-404-hidden"); - - if (hasBuilt) return; - const body = document.body; - if (!body) return; - - hasBuilt = true; - document.documentElement.classList.add(KITTEN_CLASS); - document.title = "404 Not Found"; - // Appended after the manager-injected styles so these same-specificity - // declarations win over the compiled baseline. - stripeStyle = document.createElement("style"); - stripeStyle.textContent = H1_STRIPE_CSS; - document.head.appendChild(stripeStyle); - card = buildCard(); - body.appendChild(card); - }; - - // Body may not be parsed yet at document_start; render now and again on - // DOM ready as a safety net. - render(); - document.addEventListener("DOMContentLoaded", render, { once: true }); - - return () => { - document.removeEventListener("DOMContentLoaded", render); - document.documentElement.classList.remove(KITTEN_CLASS); - document - .querySelectorAll(".bsplus-kitten-404-hidden") - .forEach((el) => el.classList.remove("bsplus-kitten-404-hidden")); - document.title = originalTitle; - stripeStyle?.remove(); - stripeStyle = null; - card?.remove(); - card = null; - }; - }, + run: async () => mountErrorPageKitten(), }; export default errorPageKittenPlugin; diff --git a/src/plugins/built-in/errorPageKitten/lazy.ts b/src/plugins/built-in/errorPageKitten/lazy.ts deleted file mode 100644 index 3bbe04a5..00000000 --- a/src/plugins/built-in/errorPageKitten/lazy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineLazyPlugin } from "../../core/dynamicLoader"; -import styles from "./styles.css?inline"; - -export default defineLazyPlugin({ - id: "error-page-kitten", - name: "Classic 404 Page", - description: "Brings back SEQTA's old kitten 404 page", - version: "1.0.0", - settings: {}, - disableToggle: true, - defaultEnabled: true, - styles, - loader: () => import("./index"), -}); diff --git a/src/plugins/built-in/errorPageKitten/styles.css b/src/plugins/built-in/errorPageKitten/styles.css index 653c2dcd..6434b443 100644 --- a/src/plugins/built-in/errorPageKitten/styles.css +++ b/src/plugins/built-in/errorPageKitten/styles.css @@ -1,10 +1,5 @@ -/* Classic SEQTA 404 page, restored one-to-one over the rebranded document. - Scoped under html.bsplus-kitten-404 so it can never leak into the SPA; - the card element itself carries the .bsplus-kitten-404 class. */ +/* Classic SEQTA 404 page — scoped under html.bsplus-kitten-404 so it never leaks into the SPA. */ -/* The rebranded page styles <body> (Arial / flex / 25vh top / line-height - 1.5 / letter-spacing). line-height and letter-spacing are reset because - the rebranded body sets them and the classic page uses browser defaults. */ html.bsplus-kitten-404 { background: #333; } @@ -20,7 +15,6 @@ html.bsplus-kitten-404 body { letter-spacing: normal; } -/* Hide the rebranded page content while the classic card is shown. */ html.bsplus-kitten-404 .bsplus-kitten-404-hidden { display: none !important; } @@ -38,10 +32,6 @@ html.bsplus-kitten-404 .bsplus-kitten-404 { html.bsplus-kitten-404 .bsplus-kitten-404 > h1 { background: #612; - /* Valid modern baseline; the reference's exact vendor-prefixed declarations - are injected at runtime (H1_STRIPE_CSS in index.ts), which win over this. - The vendor-prefixed lines can't live in this file because the CSS - minifier drops them. */ background-image: repeating-linear-gradient( 135deg, rgba(0, 0, 0, 0), diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index 6a1ec151..0719975f 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -26,9 +26,6 @@ interface StorageChange<T = any> { /** Phased plugin startup: critical UI first, light DOM next, heavy plugins last. */ const PLUGIN_START_PHASES: readonly string[][] = [ - // 404 page boot is a tiny standalone path; start it first so the kitten - // card renders before any SPA work. A no-op on normal SEQTA pages. - ["error-page-kitten"], ["themes", "animated-background"], [ "timetable", diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 9be02278..191d03cb 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -16,7 +16,7 @@ import messageFoldersPluginLazy from "./built-in/messageFolders/lazy"; import enhancedNavigationPluginLazy from "./built-in/enhancedNavigation/lazy"; import globalSearchPluginLazy from "./built-in/globalSearch/lazy"; import gradeAnalyticsPluginLazy from "./built-in/gradeAnalytics/lazy"; -import errorPageKittenPluginLazy from "./built-in/errorPageKitten/lazy"; +import errorPageKittenPlugin from "./built-in/errorPageKitten"; // Initialize plugin manager const pluginManager = PluginManager.getInstance(); @@ -37,7 +37,7 @@ pluginManager.registerPlugin(messageFoldersPluginLazy); pluginManager.registerPlugin(enhancedNavigationPluginLazy); pluginManager.registerPlugin(globalSearchPluginLazy); pluginManager.registerPlugin(gradeAnalyticsPluginLazy); -pluginManager.registerPlugin(errorPageKittenPluginLazy); +pluginManager.registerPlugin(errorPageKittenPlugin); export async function initializePlugins(): Promise<void> { await pluginManager.startAllPlugins(); diff --git a/src/test/mocks/assetStub.ts b/src/test/mocks/assetStub.ts deleted file mode 100644 index 50e1fdeb..00000000 --- a/src/test/mocks/assetStub.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Jest stub for Vite asset imports (png, etc.): Vite returns a URL string. -export default "/resources/error-page/kitten.png"; diff --git a/src/test/mocks/inlineStub.ts b/src/test/mocks/inlineStub.ts deleted file mode 100644 index 23332858..00000000 --- a/src/test/mocks/inlineStub.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Jest stub for Vite's `?inline` css imports: returns a CSS string. -export default "/* test inline css */"; diff --git a/src/test/mocks/webextension-polyfill.ts b/src/test/mocks/webextension-polyfill.ts index 3bffe4a3..21d57028 100644 --- a/src/test/mocks/webextension-polyfill.ts +++ b/src/test/mocks/webextension-polyfill.ts @@ -32,9 +32,6 @@ export default { storage: { local, onChanged }, runtime: { sendMessage: jest.fn(async () => undefined), - getURL: jest.fn( - (path: string) => `chrome-extension://test/${String(path).replace(/^\/+/, "")}`, - ), }, };