diff --git a/extension/src/lib/subtree-watcher.ts b/extension/src/lib/subtree-watcher.ts index 2717194..e9eb305 100644 --- a/extension/src/lib/subtree-watcher.ts +++ b/extension/src/lib/subtree-watcher.ts @@ -43,11 +43,14 @@ const IGNORE_TAGS: ReadonlySet = new Set(["STYLE", "BR"]); // (Pattern from Ghostery's adblocker DOMMonitor.) const BURST_FLUSH_THRESHOLD = 512; -// `id` and `class` are the only attributes the selector-token-index -// dispatcher keys on. Limiting the filter at the MO level keeps the -// burst from page JS toggling unrelated attributes (style, data-*, -// aria-*) off the hot path. -const OBSERVED_ATTRIBUTES = ["id", "class"]; +// `id` and `class` drive the selector-token-index dispatcher; `content` +// drives meta-injection-strip's in-place re-scrub when a framework or +// page script overwrites a ``'s `content=` after we blanked it +// (since #176 stopped detaching the meta, an unobserved overwrite would +// leave the new payload visible until the next route change). Limiting +// the filter at the MO level keeps the burst from page JS toggling +// unrelated attributes (style, data-*, aria-*) off the hot path. +const OBSERVED_ATTRIBUTES = ["id", "class", "content"]; interface SubtreeWatcherOptions { // Called once per throttle window with all the (still-connected) subtree diff --git a/extension/src/rules/__tests__/meta-injection-strip.test.ts b/extension/src/rules/__tests__/meta-injection-strip.test.ts index 86c3355..899385d 100644 --- a/extension/src/rules/__tests__/meta-injection-strip.test.ts +++ b/extension/src/rules/__tests__/meta-injection-strip.test.ts @@ -193,6 +193,46 @@ describe("meta-injection-strip lazy subtrees", () => { expect(meta.getAttribute("content")).toBe(""); }); + it("re-scrubs a meta whose content is overwritten in place after blanking", async () => { + // Initial scrub blanks the existing payload but leaves the element + // attached. A subsequent framework / page-script write to `content=` + // would, pre-fix, sit visible until the next route change. With + // `content` in OBSERVED_ATTRIBUTES + observeAttributes on the watcher, + // the rewrite reaches scrubMeta and the new payload is blanked too. + const meta = appendMetaToHead({ + name: "description", + content: FIXTURES.IGNORE_HACKED, + }); + + metaInjectionStripRule.apply(document.body); + expect(meta.getAttribute("content")).toBe(""); + + meta.setAttribute("content", FIXTURES.DAN); + + await flushMutations(); + jest.advanceTimersByTime(MUTATION_THROTTLE_MS); + + expect(meta.isConnected).toBe(true); + expect(meta.getAttribute("content")).toBe(""); + }); + + it("leaves a meta with a clean in-place content rewrite alone", async () => { + // The attribute-mutation hook is opt-in for the rule, not for the + // page — a benign rewrite stays exactly as the page wrote it. + const meta = appendMetaToHead({ + name: "description", + content: "RiverMart skillets", + }); + + metaInjectionStripRule.apply(document.body); + meta.setAttribute("content", "RiverMart cookware"); + + await flushMutations(); + jest.advanceTimersByTime(MUTATION_THROTTLE_MS); + + expect(meta.getAttribute("content")).toBe("RiverMart cookware"); + }); + it("teardown stops both watchers", async () => { metaInjectionStripRule.apply(document.body); metaInjectionStripRule.teardown(); diff --git a/extension/src/rules/__tests__/noscript-strip.test.ts b/extension/src/rules/__tests__/noscript-strip.test.ts index de27dba..049bed5 100644 --- a/extension/src/rules/__tests__/noscript-strip.test.ts +++ b/extension/src/rules/__tests__/noscript-strip.test.ts @@ -112,6 +112,29 @@ describe("noscript-strip", () => { expect(noscript?.textContent).toBe(""); }); + it("re-blanks a noscript when a child is later rendered into it", async () => { + // After initial blanking the