diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 219482b0..3450a32c 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -141,11 +141,6 @@ async function init() { } } -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; @@ -159,13 +154,17 @@ function inject404FlashHide(): void { async function bootErrorPage() { if (IsSEQTAPage) return; + const { isSeqta404Page, mountErrorPageKitten } = await import( + "@/plugins/built-in/errorPageKitten" + ); + const storagePromise = browser.storage.local.get([ "onoff", "plugin.error-page-kitten.settings", ]); const watchTitle = () => { - if (shouldBootKitten404()) inject404FlashHide(); + if (isSeqta404Page()) inject404FlashHide(); }; watchTitle(); if (document.readyState === "loading") { @@ -179,7 +178,7 @@ async function bootErrorPage() { } document.removeEventListener("readystatechange", watchTitle); - if (!shouldBootKitten404()) return; + if (!isSeqta404Page()) return; const stored = await storagePromise; if ((stored.onoff ?? true) === false) return; @@ -189,9 +188,6 @@ async function bootErrorPage() { if (kittenSettings?.enabled === false) return; try { - const { mountErrorPageKitten } = await import( - "@/plugins/built-in/errorPageKitten" - ); mountErrorPageKitten(); document.getElementById("bsplus-404-flash-hide")?.remove(); } catch (error) { diff --git a/src/plugins/built-in/errorPageKitten/index.test.ts b/src/plugins/built-in/errorPageKitten/index.test.ts index f3144e3f..a677f288 100644 --- a/src/plugins/built-in/errorPageKitten/index.test.ts +++ b/src/plugins/built-in/errorPageKitten/index.test.ts @@ -2,8 +2,6 @@ * @jest-environment jsdom */ /// -import type { PluginAPI } from "@/plugins/core/types"; - jest.mock("@/lib/extensionAssetUrl", () => ({ resolveExtensionAssetUrl: (url: string) => `chrome-extension://test/${String(url).replace(/^\/+/, "")}`, @@ -17,7 +15,10 @@ jest.mock("./styles.css?inline", () => ({ default: "/* test */", })); -import { mountErrorPageKitten } from "./index"; +import errorPageKittenPlugin, { + isSeqta404Page, + mountErrorPageKitten, +} from "./index"; const REBRANDED_404_BODY = `
@@ -43,18 +44,33 @@ describe("errorPageKitten", () => { const cleanup = mountErrorPageKitten(); - const card = getCard(); - expect(card).not.toBeNull(); + expect(getCard()).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"); + }); + + it("detects standalone 404 pages by .message and title", () => { + document.title = "Page not found"; + document.body.innerHTML = REBRANDED_404_BODY; + expect(isSeqta404Page()).toBe(true); + }); + + it("does not detect the SPA", () => { + document.title = "SEQTA Learn"; + document.body.innerHTML = '
'; + expect(isSeqta404Page()).toBe(false); + }); + + it("plugin run is a no-op outside a 404 page", async () => { + document.title = "SEQTA Learn"; + document.body.innerHTML = '
'; + + const cleanup = await errorPageKittenPlugin.run(); + + expect(getCard()).toBeNull(); + expect(cleanup).toBeDefined(); + cleanup?.(); }); }); diff --git a/src/plugins/built-in/errorPageKitten/index.ts b/src/plugins/built-in/errorPageKitten/index.ts index 624a06b0..c99c7021 100644 --- a/src/plugins/built-in/errorPageKitten/index.ts +++ b/src/plugins/built-in/errorPageKitten/index.ts @@ -16,7 +16,14 @@ const CARD_HTML = `

404 Not Found

SEQTA`; -/** Boot path in SEQTA.ts gates when this runs; keep render-only here. */ +/** Standalone SEQTA 404 document — not the SPA (#container). */ +export function isSeqta404Page(): 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); +} + export function mountErrorPageKitten(): () => void { const styleEl = document.createElement("style"); styleEl.textContent = styles; @@ -52,7 +59,7 @@ const errorPageKittenPlugin: Plugin = { disableToggle: true, defaultEnabled: true, styles, - run: async () => mountErrorPageKitten(), + run: async () => (isSeqta404Page() ? mountErrorPageKitten() : () => {}), }; export default errorPageKittenPlugin; diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index 0719975f..9cc8d508 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -43,6 +43,9 @@ const PLUGIN_START_PHASES: readonly string[][] = [ ["global-search"], ]; +/** Booted only from SEQTA.ts on standalone 404 documents — never auto-start in the SPA. */ +const PLUGIN_BOOT_ONLY_IDS = new Set(["error-page-kitten"]); + /** * Singleton class responsible for the entire lifecycle of plugins. * This includes registration, starting, stopping, event dispatching, @@ -263,7 +266,7 @@ export class PluginManager { const phasedIds = new Set(PLUGIN_START_PHASES.flat()); const remainingIds = Array.from(this.plugins.keys()).filter( - (id) => !phasedIds.has(id), + (id) => !phasedIds.has(id) && !PLUGIN_BOOT_ONLY_IDS.has(id), ); if (remainingIds.length > 0) { await this.startPluginPhase(remainingIds);