diff --git a/extension/src/rules/__tests__/countdown-timer-redact.test.ts b/extension/src/rules/__tests__/countdown-timer-redact.test.ts index ee73cc7..1231969 100644 --- a/extension/src/rules/__tests__/countdown-timer-redact.test.ts +++ b/extension/src/rules/__tests__/countdown-timer-redact.test.ts @@ -50,6 +50,18 @@ describe("matchesTimerPattern", () => { expect(matchesTimerPattern("15 minutes remaining")).toBe(true); }); + it("matches urgency suffix synonyms (#203 item 21)", () => { + expect(matchesTimerPattern("5 hours to claim")).toBe(true); + expect(matchesTimerPattern("45 minutes to save")).toBe(true); + }); + + it("matches expiry-lead phrasings (#203 item 21)", () => { + expect(matchesTimerPattern("Sale ends in 3h")).toBe(true); + expect(matchesTimerPattern("Offer expires in 45 minutes")).toBe(true); + expect(matchesTimerPattern("Closes in 2d")).toBe(true); + expect(matchesTimerPattern("Ends in 30 seconds")).toBe(true); + }); + it("does not match arbitrary numbers", () => { expect(matchesTimerPattern("Item #123456")).toBe(false); expect(matchesTimerPattern("Save 30%")).toBe(false); @@ -125,6 +137,31 @@ describe("countdownTimerRedactRule", () => { expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).not.toBeNull(); }); + it("hides an expiry-lead countdown that decreased (#203 item 21)", () => { + document.body.innerHTML = `
Sale ends in 2h 15m
`; + countdownTimerRedactRule.apply(document.body); + + const timer = document.querySelector("#t"); + if (timer) { + timer.textContent = "Sale ends in 2h 14m"; + } + + jest.advanceTimersByTime(SNAPSHOT_DELAY_MS); + + expect(document.querySelector("#t")).toBeNull(); + expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).not.toBeNull(); + }); + + it("leaves a static expiry-lead badge alone (decrement gate)", () => { + document.body.innerHTML = `
Expires in 30 days
`; + countdownTimerRedactRule.apply(document.body); + + jest.advanceTimersByTime(SNAPSHOT_DELAY_MS); + + expect(document.querySelector("#t")).not.toBeNull(); + expect(document.querySelector(`.${PLACEHOLDER_CLASS}`)).toBeNull(); + }); + it("leaves static clock-shaped text alone", () => { document.body.innerHTML = `Posted at 12:34`; countdownTimerRedactRule.apply(document.body); diff --git a/extension/src/rules/__tests__/scarcity-redact.test.ts b/extension/src/rules/__tests__/scarcity-redact.test.ts index efd7f55..2bcb365 100644 --- a/extension/src/rules/__tests__/scarcity-redact.test.ts +++ b/extension/src/rules/__tests__/scarcity-redact.test.ts @@ -46,6 +46,21 @@ describe("matchesScarcityPattern", () => { "8 purchased in the past day", "5 have it in their cart", "3 in carts", + // Synonym evasion bypasses (#203 item 21). + "Just 3 left", + "Just 2 in stock", + "3 items left", + "5 units remaining", + "2 pieces left", + "While supplies last", + "While stocks last", + "Selling quickly", + "Going quickly", + "Flying off the shelves", + "Going off shelves", + "12 added to cart in the last hour", + "8 shoppers added this to their bag", + "5 others added to wishlist", ])("matches urgency phrasing: %s", (text) => { expect(matchesScarcityPattern(text)).toBe(true); }); @@ -67,6 +82,11 @@ describe("matchesScarcityPattern", () => { "Ships in 2 days", "Save 30%", "Free shipping", + // "Just" alone is not an urgency signal. + "Just released", + "Just for you", + // Bare "added to cart" without a count is a UI confirmation, not scarcity. + "Added to cart", ])("leaves non-urgency text alone: %s", (text) => { expect(matchesScarcityPattern(text)).toBe(false); }); diff --git a/extension/src/rules/countdown-timer-redact.ts b/extension/src/rules/countdown-timer-redact.ts index d733ece..a77fcfd 100644 --- a/extension/src/rules/countdown-timer-redact.ts +++ b/extension/src/rules/countdown-timer-redact.ts @@ -36,13 +36,20 @@ const COLON_PATTERN = /(?