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);