Skip to content
Open
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
60 changes: 59 additions & 1 deletion src/__test__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {QRCodeSVG, QRCodeCanvas} from '..';
import {describe, expect, test} from '@jest/globals';
import type {QRCodeCanvasUpdateAPIs} from '..';
import {describe, expect, test, jest, beforeAll, afterAll} from '@jest/globals';
import {render} from '@testing-library/react';
import '@testing-library/jest-dom';

Expand Down Expand Up @@ -170,3 +171,60 @@ describe('`style` is passed to rendered nodes and merged correctly', () => {
expect(container.firstChild).toMatchSnapshot();
});
});

describe('onUpdate prop', () => {
beforeAll(() => {
jest.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
fillRect: jest.fn(),
scale: jest.fn(),
drawImage: jest.fn(),
} as unknown as CanvasRenderingContext2D);
});

afterAll(() => {
jest.restoreAllMocks();
});

test('calls onUpdate with canvas APIs', () => {
const onUpdateMock = jest.fn();
render(<QRCodeCanvas {...BASIC_PROPS} onUpdate={onUpdateMock} />);
expect(onUpdateMock).toHaveBeenCalledTimes(1);
expect(onUpdateMock).toHaveBeenCalledWith(
expect.objectContaining({
toDataURL: expect.any(Function),
toBlob: expect.any(Function),
})
);
});

test('keeps api reference stable on rerenders to prevent infinite loops', () => {
const onUpdateMock = jest.fn();
const {rerender} = render(
<QRCodeCanvas {...BASIC_PROPS} onUpdate={onUpdateMock} />
);

expect(onUpdateMock).toHaveBeenCalledTimes(1);
const firstApis = onUpdateMock.mock.calls[0][0] as QRCodeCanvasUpdateAPIs;

// Rerender with different props (e.g. value changes)
rerender(
<QRCodeCanvas
{...BASIC_PROPS}
value="https://example.com/updated"
onUpdate={onUpdateMock}
/>
);

expect(onUpdateMock).toHaveBeenCalledTimes(2);
const secondApis = onUpdateMock.mock.calls[1][0] as QRCodeCanvasUpdateAPIs;

// The API object reference must remain identical
expect(firstApis).toBe(secondApis);
expect(firstApis.toDataURL).toBe(secondApis.toDataURL);
expect(firstApis.toBlob).toBe(secondApis.toBlob);

// Verify calling toDataURL works and returns a string
const dataUrl = firstApis.toDataURL();
expect(typeof dataUrl).toBe('string');
});
});
30 changes: 29 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,14 @@ type QRProps = {
*/
imageSettings?: ImageSettings;
};
type QRPropsCanvas = QRProps & React.CanvasHTMLAttributes<HTMLCanvasElement>;
export type QRCodeCanvasUpdateAPIs = {
toDataURL: HTMLCanvasElement['toDataURL'];
toBlob: HTMLCanvasElement['toBlob'];
};
type QRPropsCanvas = QRProps &
React.CanvasHTMLAttributes<HTMLCanvasElement> & {
onUpdate?: (apis: QRCodeCanvasUpdateAPIs) => void;
};
type QRPropsSVG = QRProps & React.SVGAttributes<SVGSVGElement>;

const DEFAULT_SIZE = 128;
Expand Down Expand Up @@ -358,13 +365,30 @@ const QRCodeCanvas = React.forwardRef<HTMLCanvasElement, QRPropsCanvas>(
boostLevel,
marginSize,
imageSettings,
onUpdate,
...extraProps
} = props;
const {style, ...otherProps} = extraProps;
const imgSrc = imageSettings?.src;
const _canvas = React.useRef<HTMLCanvasElement | null>(null);
const _image = React.useRef<HTMLImageElement>(null);

const apis = React.useMemo<QRCodeCanvasUpdateAPIs>(() => {
return {
toDataURL: (type?: string, quality?: number) => {
return _canvas.current?.toDataURL(type, quality) ?? '';
},
toBlob: (callback: BlobCallback, type?: string, quality?: number) => {
_canvas.current?.toBlob(callback, type, quality);
},
};
}, []);

const onUpdateRef = React.useRef(onUpdate);
React.useEffect(() => {
onUpdateRef.current = onUpdate;
});

// Set the local ref (_canvas) and also the forwarded ref from outside
const setCanvasRef = React.useCallback(
(node: HTMLCanvasElement | null) => {
Expand Down Expand Up @@ -464,6 +488,10 @@ const QRCodeCanvas = React.forwardRef<HTMLCanvasElement, QRPropsCanvas>(
calculatedImageSettings.h
);
}

if (onUpdateRef.current) {
onUpdateRef.current(apis);
}
}
});

Expand Down