diff --git a/apps/chrome-extension/package.json b/apps/chrome-extension/package.json index 51e4d378813..ba008400c9d 100644 --- a/apps/chrome-extension/package.json +++ b/apps/chrome-extension/package.json @@ -33,6 +33,7 @@ "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "4.4.1", "autoprefixer": "^10.4.16", + "jsdom": "^26.1.0", "postcss": "^8.4.31", "tailwindcss": "^3.4.0", "typescript": "^5.8.3", diff --git a/apps/chrome-extension/src/content/blur-overlay.test.tsx b/apps/chrome-extension/src/content/blur-overlay.test.tsx new file mode 100644 index 00000000000..be2064879c6 --- /dev/null +++ b/apps/chrome-extension/src/content/blur-overlay.test.tsx @@ -0,0 +1,125 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { BlurOverlay } from "./blur-overlay"; + +let root: Root; +let host: HTMLDivElement; +let target: HTMLDivElement; +const onDone = vi.fn(); + +beforeEach(() => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + host = document.createElement("div"); + target = document.createElement("div"); + document.body.append(host, target); + Object.defineProperty(document, "elementFromPoint", { + configurable: true, + value: vi.fn(() => target), + }); + root = createRoot(host); + act(() => root.render()); +}); + +afterEach(() => { + act(() => root.unmount()); + host.remove(); + target.remove(); + Reflect.deleteProperty(document, "elementFromPoint"); + vi.unstubAllGlobals(); +}); + +const clickTarget = () => { + const event = new MouseEvent("click", { bubbles: true, cancelable: true }); + act(() => target.dispatchEvent(event)); + return event; +}; + +describe("blur style restoration", () => { + it.each([ + { + filter: "contrast(2)", + filterPriority: "important", + select: "text", + selectPriority: "important", + }, + { + filter: "grayscale(1)", + filterPriority: "", + select: "all", + selectPriority: "", + }, + { + filter: "", + filterPriority: "", + select: "none", + selectPriority: "important", + }, + ])( + "restores the original inline values and priorities: $select/$filter", + (styles) => { + target.style.setProperty("filter", styles.filter, styles.filterPriority); + target.style.setProperty( + "user-select", + styles.select, + styles.selectPriority, + ); + target.style.setProperty("color", "red", "important"); + for (let cycle = 0; cycle < 2; cycle++) { + expect(clickTarget().defaultPrevented).toBe(true); + expect(target.style.getPropertyValue("filter")).toBe("blur(12px)"); + expect(target.style.getPropertyPriority("filter")).toBe("important"); + expect(target.style.getPropertyValue("user-select")).toBe("none"); + expect(target.style.getPropertyPriority("user-select")).toBe( + "important", + ); + clickTarget(); + expect(target.style.getPropertyValue("filter")).toBe(styles.filter); + expect(target.style.getPropertyPriority("filter")).toBe( + styles.filterPriority, + ); + expect(target.style.getPropertyValue("user-select")).toBe( + styles.select, + ); + expect(target.style.getPropertyPriority("user-select")).toBe( + styles.selectPriority, + ); + expect(target.style.getPropertyValue("color")).toBe("red"); + expect(target.style.getPropertyPriority("color")).toBe("important"); + expect(Object.keys(target.dataset)).toEqual([]); + } + }, + ); + + it("removes temporary styles when the target had no inline declarations", () => { + clickTarget(); + clickTarget(); + expect(target.style.getPropertyValue("filter")).toBe(""); + expect(target.style.getPropertyValue("user-select")).toBe(""); + expect(Object.keys(target.dataset)).toEqual([]); + }); + + it("retains saved styles when the blur tool closes and reopens", () => { + target.style.setProperty("user-select", "text", "important"); + clickTarget(); + act(() => root.render()); + expect(clickTarget().defaultPrevented).toBe(false); + expect(target.style.getPropertyValue("filter")).toBe("blur(12px)"); + act(() => root.render()); + clickTarget(); + expect(target.style.getPropertyValue("user-select")).toBe("text"); + expect(target.style.getPropertyPriority("user-select")).toBe("important"); + expect(Object.keys(target.dataset)).toEqual([]); + }); + + it("ignores extension controls", () => { + target.id = "cap-extension-recorder-overlay"; + target.style.setProperty("user-select", "text", "important"); + expect(clickTarget().defaultPrevented).toBe(false); + expect(target.style.getPropertyValue("user-select")).toBe("text"); + expect(target.style.getPropertyPriority("user-select")).toBe("important"); + expect(Object.keys(target.dataset)).toEqual([]); + }); +}); diff --git a/apps/chrome-extension/src/content/blur-overlay.tsx b/apps/chrome-extension/src/content/blur-overlay.tsx index 3512d93360d..f2f32a8ade5 100644 --- a/apps/chrome-extension/src/content/blur-overlay.tsx +++ b/apps/chrome-extension/src/content/blur-overlay.tsx @@ -71,15 +71,37 @@ export function BlurOverlay({ active, onDone }: BlurOverlayProps) { if (target.dataset.capBlurred === "true") { const orig = target.dataset.capOrigFilter ?? ""; if (orig) { - target.style.filter = orig; + target.style.setProperty( + "filter", + orig, + target.dataset.capOrigFilterPriority ?? "", + ); } else { target.style.removeProperty("filter"); } - target.style.removeProperty("user-select"); + const originalSelection = target.dataset.capOrigUserSelect ?? ""; + if (originalSelection) { + target.style.setProperty( + "user-select", + originalSelection, + target.dataset.capOrigUserSelectPriority ?? "", + ); + } else { + target.style.removeProperty("user-select"); + } delete target.dataset.capBlurred; delete target.dataset.capOrigFilter; + delete target.dataset.capOrigFilterPriority; + delete target.dataset.capOrigUserSelect; + delete target.dataset.capOrigUserSelectPriority; } else { target.dataset.capOrigFilter = target.style.filter || ""; + target.dataset.capOrigFilterPriority = + target.style.getPropertyPriority("filter"); + target.dataset.capOrigUserSelect = + target.style.getPropertyValue("user-select"); + target.dataset.capOrigUserSelectPriority = + target.style.getPropertyPriority("user-select"); target.style.setProperty("filter", "blur(12px)", "important"); target.style.setProperty("user-select", "none", "important"); target.dataset.capBlurred = "true"; diff --git a/bun.lock b/bun.lock index 5a6fb2c3053..710b246b228 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,7 @@ "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "4.4.1", "autoprefixer": "^10.4.16", + "jsdom": "^26.1.0", "postcss": "^8.4.31", "tailwindcss": "^3.4.0", "typescript": "^5.8.3",