From 80df1fb317691d79d249e17cc083b04ea0b40332 Mon Sep 17 00:00:00 2001 From: KayProject Date: Wed, 29 Jul 2026 21:25:33 +0100 Subject: [PATCH] fix(QRCode): guard against a non-positive size prop `size` defaulted to 160 but was never validated, so `size={0}` or a negative value gave the canvas invalid dimensions and rendered as a broken box with no indication of why. Warn and skip the draw instead. The invalid-size check is derived rather than stored so the spinner isn't left running while waiting on a draw that will never be attempted. Resolves #310 --- src/components/QRCode.test.tsx | 48 ++++++++++++++++++++++++++++++++++ src/components/QRCode.tsx | 14 ++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/components/QRCode.test.tsx b/src/components/QRCode.test.tsx index feb3ec4..3d0056f 100644 --- a/src/components/QRCode.test.tsx +++ b/src/components/QRCode.test.tsx @@ -77,6 +77,54 @@ describe("QRCode", () => { expect(document.querySelector("canvas")).toBeInTheDocument(); }); + describe("size validation", () => { + it("warns and skips drawing when size is 0", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const getContext = vi.spyOn(HTMLCanvasElement.prototype, "getContext"); + + render(); + + expect(warn).toHaveBeenCalledWith("[QRCode] size must be > 0"); + expect(getContext).not.toHaveBeenCalled(); + + warn.mockRestore(); + getContext.mockRestore(); + }); + + it("warns and skips drawing when size is negative", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const getContext = vi.spyOn(HTMLCanvasElement.prototype, "getContext"); + + render(); + + expect(warn).toHaveBeenCalledWith("[QRCode] size must be > 0"); + expect(getContext).not.toHaveBeenCalled(); + + warn.mockRestore(); + getContext.mockRestore(); + }); + + it("does not leave a loading spinner running for an invalid size", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const { container } = render(); + + expect(container.querySelector(".animate-spin")).not.toBeInTheDocument(); + + warn.mockRestore(); + }); + + it("does not warn for a valid size", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render(); + + expect(warn).not.toHaveBeenCalledWith("[QRCode] size must be > 0"); + + warn.mockRestore(); + }); + }); + it("accepts a className on the outer wrapper", () => { const { container } = render(); const wrapper = container.firstElementChild; diff --git a/src/components/QRCode.tsx b/src/components/QRCode.tsx index 87f1d82..7156f01 100644 --- a/src/components/QRCode.tsx +++ b/src/components/QRCode.tsx @@ -46,6 +46,11 @@ export function QRCode({ const [isLoading, setIsLoading] = useState(true); const [prevProps, setPrevProps] = useState({ value, size, canvasBackground, canvasForeground }); + // A zero or negative size gives the canvas invalid dimensions and renders as + // a broken box. Derived rather than stored so the spinner doesn't sit + // running while waiting on a draw that will never be attempted. + const hasInvalidSize = !size || size <= 0; + if ( prevProps.value !== value || prevProps.size !== size || @@ -59,6 +64,11 @@ export function QRCode({ useEffect(() => { if (renderError || !value) return; + if (hasInvalidSize) { + console.warn("[QRCode] size must be > 0"); + return; + } + const canvas = canvasRef.current; if (!canvas) return; @@ -100,7 +110,7 @@ export function QRCode({ return () => { active = false; }; - }, [value, size, canvasBackground, canvasForeground, renderError]); + }, [value, size, canvasBackground, canvasForeground, renderError, hasInvalidSize]); return (
@@ -122,7 +132,7 @@ export function QRCode({ ) : ( <> - {isLoading && ( + {isLoading && !hasInvalidSize && (