Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/components/QRCode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<QRCode value={value} size={0} />);

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(<QRCode value={value} size={-10} />);

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(<QRCode value={value} size={0} />);

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(<QRCode value={value} size={120} />);

expect(warn).not.toHaveBeenCalledWith("[QRCode] size must be > 0");

warn.mockRestore();
});
});

it("accepts a className on the outer wrapper", () => {
const { container } = render(<QRCode value={value} className="my-qr" />);
const wrapper = container.firstElementChild;
Expand Down
14 changes: 12 additions & 2 deletions src/components/QRCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -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;

Expand Down Expand Up @@ -100,7 +110,7 @@ export function QRCode({
return () => {
active = false;
};
}, [value, size, canvasBackground, canvasForeground, renderError]);
}, [value, size, canvasBackground, canvasForeground, renderError, hasInvalidSize]);

return (
<figure className={cn("flex flex-col items-center gap-3", className)}>
Expand All @@ -122,7 +132,7 @@ export function QRCode({
</div>
) : (
<>
{isLoading && (
{isLoading && !hasInvalidSize && (
<div
className="flex items-center justify-center"
style={{ width: size, height: size }}
Expand Down
Loading