From d60c4ddd3acdbeb83aef9f9c52f1cf103d27f83a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 12:51:20 +0000 Subject: [PATCH 1/4] Fix three bugs: escapeAttr ampersand, ExtensionAuth double fetch, candidate page title - #244: Escape `&` to `&` before other replacements in escapeAttr() - #243: Remove `error` from useEffect dependency array to prevent double POST on token fetch failure - #242: Use stored tab title from chrome.tabs.query instead of popup's document.title for candidate "Track this" button Closes #242, closes #243, closes #244 https://claude.ai/code/session_01AmZyEy5PWRPk34fqgyyUQ9 --- client/src/pages/ExtensionAuth.tsx | 2 +- extension/src/popup/popup.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/pages/ExtensionAuth.tsx b/client/src/pages/ExtensionAuth.tsx index c7961f0..3425998 100644 --- a/client/src/pages/ExtensionAuth.tsx +++ b/client/src/pages/ExtensionAuth.tsx @@ -35,7 +35,7 @@ export default function ExtensionAuth() { setError("Something went wrong. Please try again."); } })(); - }, [user, tokenSent, error]); + }, [user, tokenSent]); if (isLoading) { return ( diff --git a/extension/src/popup/popup.ts b/extension/src/popup/popup.ts index d3b036e..4f3af98 100644 --- a/extension/src/popup/popup.ts +++ b/extension/src/popup/popup.ts @@ -39,6 +39,7 @@ let userInfo: UserInfo | null = null; let selection: Selection | null = null; let candidates: Candidate[] = []; let currentTabUrl = ""; +let currentTabTitle = ""; let currentTabId = 0; let errorMessage = ""; let createdMonitorName = ""; @@ -57,6 +58,7 @@ async function init(): Promise { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (tab) { currentTabUrl = tab.url || ""; + currentTabTitle = tab.title || ""; currentTabId = tab.id || 0; } @@ -308,7 +310,7 @@ function renderAuthenticated(): void { selector: c.selector, currentValue: c.text, url: currentTabUrl, - pageTitle: document.title, + pageTitle: currentTabTitle, }; state = "confirm"; render(); @@ -539,7 +541,7 @@ function escapeHtml(str: string): string { } function escapeAttr(str: string): string { - return str.replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); + return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); } const KNOWN_TIERS = ["free", "pro", "power"]; From 57167d43c5deea2f4cebfe0e530c7ace3cb68a59 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 13:02:20 +0000 Subject: [PATCH 2/4] Extract escapeAttr/sanitizeTier into testable utils module with tests Move pure utility functions from popup.ts into popup/utils.ts for testability. Add comprehensive test coverage for escapeAttr (including ampersand escaping) and sanitizeTier. Move import to top of file following codebase conventions. https://claude.ai/code/session_01AmZyEy5PWRPk34fqgyyUQ9 --- extension/src/popup/popup.ts | 9 +----- extension/src/popup/utils.test.ts | 53 +++++++++++++++++++++++++++++++ extension/src/popup/utils.ts | 8 +++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 extension/src/popup/utils.test.ts create mode 100644 extension/src/popup/utils.ts diff --git a/extension/src/popup/popup.ts b/extension/src/popup/popup.ts index 4f3af98..f1f65af 100644 --- a/extension/src/popup/popup.ts +++ b/extension/src/popup/popup.ts @@ -1,5 +1,6 @@ import { BASE_URL, MSG } from "../shared/constants"; import { getToken, clearToken, isTokenValid } from "../auth/token"; +import { escapeAttr, sanitizeTier } from "./utils"; // ────────────────────────────────────────────────────────────────── // State @@ -540,14 +541,6 @@ function escapeHtml(str: string): string { return div.innerHTML; } -function escapeAttr(str: string): string { - return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); -} - -const KNOWN_TIERS = ["free", "pro", "power"]; -function sanitizeTier(tier: string): string { - return KNOWN_TIERS.includes(tier) ? tier : "free"; -} // ────────────────────────────────────────────────────────────────── // Start diff --git a/extension/src/popup/utils.test.ts b/extension/src/popup/utils.test.ts new file mode 100644 index 0000000..e3d1432 --- /dev/null +++ b/extension/src/popup/utils.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { escapeAttr, sanitizeTier } from "./utils"; + +describe("escapeAttr", () => { + it("escapes ampersand before other entities", () => { + expect(escapeAttr("A & B")).toBe("A & B"); + }); + + it("escapes double quotes", () => { + expect(escapeAttr('say "hello"')).toBe("say "hello""); + }); + + it("escapes single quotes", () => { + expect(escapeAttr("it's")).toBe("it's"); + }); + + it("escapes angle brackets", () => { + expect(escapeAttr("