Skip to content

Commit bef2e27

Browse files
MeoRinLimeanlyyao
andauthored
test(Qrcode): add qrcode's unit test (#731)
* test(Qrcode): add qrcode's unit test * fix: resolve conflicts --------- Co-authored-by: anlyyao <[email protected]>
1 parent 2270812 commit bef2e27

File tree

4 files changed

+300
-3
lines changed

4 files changed

+300
-3
lines changed

site/test-coverage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module.exports = {
2424
form: { statements: '2.8%', branches: '0%', functions: '0%', lines: '2.96%' },
2525
grid: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
2626
guide: { statements: '3.46%', branches: '0%', functions: '0%', lines: '3.77%' },
27-
hooks: { statements: '48.41%', branches: '23.88%', functions: '56.25%', lines: '48.33%' },
27+
hooks: { statements: '69.04%', branches: '34.32%', functions: '71.87%', lines: '70%' },
2828
image: { statements: '97.72%', branches: '100%', functions: '92.3%', lines: '97.61%' },
2929
imageViewer: { statements: '8.47%', branches: '2.87%', functions: '0%', lines: '8.84%' },
3030
indexes: { statements: '95.65%', branches: '69.81%', functions: '100%', lines: '96.94%' },
@@ -43,7 +43,7 @@ module.exports = {
4343
popup: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
4444
progress: { statements: '100%', branches: '97.36%', functions: '100%', lines: '100%' },
4545
pullDownRefresh: { statements: '100%', branches: '98.43%', functions: '100%', lines: '100%' },
46-
qrcode: { statements: '9.89%', branches: '0%', functions: '0%', lines: '10%' },
46+
qrcode: { statements: '100%', branches: '92.5%', functions: '100%', lines: '100%' },
4747
radio: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
4848
rate: { statements: '5.71%', branches: '0%', functions: '0%', lines: '5.71%' },
4949
result: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },

src/qrcode/QRCodeCanvas.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const QRCodeCanvas = React.forwardRef<HTMLCanvasElement, QRPropsCanvas>((props,
5757
});
5858

5959
React.useEffect(() => {
60+
/* istanbul ignore else */
6061
if (canvasRef.current) {
6162
const canvas = canvasRef.current;
6263

@@ -74,6 +75,7 @@ const QRCodeCanvas = React.forwardRef<HTMLCanvasElement, QRPropsCanvas>((props,
7475
image.naturalHeight !== 0 &&
7576
image.naturalWidth !== 0;
7677

78+
/* istanbul ignore else */
7779
if (haveImageToRender) {
7880
if (calculatedImageSettings.excavation != null) {
7981
cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation);
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import React from 'react';
2+
import { describe, it, expect, render, fireEvent, waitFor, vi, cleanup, afterEach } from '@test/utils';
3+
import QRCode from '../QRCode';
4+
import { QRCodeSVG } from '../QRCodeSVG';
5+
6+
describe('QRCode', () => {
7+
it('should render nothing when value is empty', () => {
8+
const { container } = render(<QRCode />);
9+
expect(container.firstChild).toBeNull();
10+
});
11+
12+
it('should render canvas by default with size and bgColor styles applied', async () => {
13+
const { container } = render(<QRCode value="hello" size={200} bgColor="#ff0000" />);
14+
const root = container.firstElementChild as HTMLElement;
15+
expect(root).toBeTruthy();
16+
expect(root.className).toContain('t-qrcode');
17+
// default type is canvas
18+
expect(root.querySelector('canvas')).toBeTruthy();
19+
// inline styles
20+
expect(root).toHaveStyle('width: 200px');
21+
expect(root).toHaveStyle('height: 200px');
22+
expect(root).toHaveStyle('background-color: #ff0000');
23+
});
24+
25+
it('should render svg when type is svg', () => {
26+
const { container } = render(<QRCode value="world" type="svg" />);
27+
const root = container.firstElementChild as HTMLElement;
28+
expect(root.className).toContain('t-qrcode-svg');
29+
expect(root.querySelector('svg')).toBeTruthy();
30+
});
31+
32+
it('should apply borderless class', () => {
33+
const { container } = render(<QRCode value="hello" borderless />);
34+
const root = container.firstElementChild as HTMLElement;
35+
expect(root.className).toContain('t-borderless');
36+
});
37+
38+
describe('status mask', () => {
39+
it('no mask when status is active', () => {
40+
const { container } = render(<QRCode value="foo" status="active" />);
41+
expect(container.querySelector('.t-mask')).toBeNull();
42+
});
43+
44+
it('expired shows text and triggers onRefresh', async () => {
45+
const onRefresh = vi.fn();
46+
const { container, getByText } = render(<QRCode value="foo" status="expired" onRefresh={onRefresh} />);
47+
// mask exists
48+
expect(container.querySelector('.t-mask')).toBeTruthy();
49+
// zh-CN default locale text
50+
expect(getByText('二维码过期')).toBeTruthy();
51+
const refresh = getByText('点击刷新');
52+
fireEvent.click(refresh);
53+
await waitFor(() => expect(onRefresh).toHaveBeenCalledTimes(1));
54+
});
55+
56+
it('scanned shows text', () => {
57+
const { getByText } = render(<QRCode value="foo" status="scanned" />);
58+
expect(getByText('已扫描')).toBeTruthy();
59+
});
60+
61+
it('loading shows mask', () => {
62+
const { container } = render(<QRCode value="foo" status="loading" />);
63+
expect(container.querySelector('.t-mask')).toBeTruthy();
64+
});
65+
});
66+
67+
describe('icon rendering', () => {
68+
it('renders hidden img for canvas type when icon provided with default iconSize', () => {
69+
const icon = 'https://example.com/icon.png';
70+
const { container } = render(<QRCode value="bar" type="canvas" icon={icon} />);
71+
const img = container.querySelector('img');
72+
expect(img).toBeTruthy();
73+
expect(img?.getAttribute('src')).toBe(icon);
74+
expect(img?.style.display).toBe('none');
75+
});
76+
77+
it('renders hidden img for canvas type with number iconSize', () => {
78+
const icon = 'https://example.com/icon.png';
79+
const iconSize = 32;
80+
const { container } = render(<QRCode value="bar" type="canvas" icon={icon} iconSize={iconSize} />);
81+
const img = container.querySelector('img');
82+
expect(img).toBeTruthy();
83+
expect(img?.getAttribute('src')).toBe(icon);
84+
expect(img?.style.display).toBe('none');
85+
});
86+
87+
it('passes iconSize correctly to imageSettings for object type', () => {
88+
const icon = 'https://example.com/icon.png';
89+
const iconSize = {};
90+
91+
const { container } = render(
92+
<QRCode style={{ height: 60, width: 60 }} value="test" type="canvas" icon={icon} iconSize={iconSize} />,
93+
);
94+
expect(container.querySelector('canvas')).toBeTruthy();
95+
expect(container.querySelector('img')).toBeTruthy();
96+
});
97+
98+
it('renders <image> element inside svg when icon provided', () => {
99+
const icon = 'https://example.com/icon.svg';
100+
const { container } = render(<QRCode value="bar" type="svg" icon={icon} iconSize={{ height: 32, width: 32 }} />);
101+
const image = container.querySelector('svg image');
102+
expect(image).toBeTruthy();
103+
// The actual height/width in SVG are calculated values based on QR code scale, not direct iconSize values
104+
expect(image?.getAttribute('height')).toBeTruthy();
105+
expect(image?.getAttribute('width')).toBeTruthy();
106+
expect(parseFloat(image?.getAttribute('height') || '0')).toBeGreaterThan(0);
107+
expect(parseFloat(image?.getAttribute('width') || '0')).toBeGreaterThan(0);
108+
// Some environments expose it as href/baseVal; keep a tolerant check
109+
const href = (image as any)?.getAttribute('href') || (image as any)?.href?.baseVal;
110+
expect(href).toBe(icon);
111+
});
112+
});
113+
114+
describe('QRCodeSVG title rendering', () => {
115+
it('renders title element inside svg when title provided', () => {
116+
const titleText = 'My QR Code';
117+
const { container } = render(<QRCodeSVG value="test" title={titleText} />);
118+
const svg = container.querySelector('svg');
119+
const titleElement = container.querySelector('svg title');
120+
121+
expect(svg).toBeTruthy();
122+
expect(titleElement).toBeTruthy();
123+
expect(titleElement?.textContent).toBe(titleText);
124+
});
125+
126+
it('does not render title element when title is not provided', () => {
127+
const { container } = render(<QRCodeSVG value="test" />);
128+
const titleElement = container.querySelector('svg title');
129+
130+
expect(titleElement).toBeNull();
131+
});
132+
});
133+
});
134+
135+
// Helpers to mock canvas 2d context
136+
const createMockCtx = () => {
137+
const calls: Record<string, any[]> = {};
138+
const record = (name: string, ...args: any[]) => {
139+
calls[name] = calls[name] || [];
140+
calls[name].push(args);
141+
};
142+
143+
let fillStyleCache: any;
144+
let alphaCache: any;
145+
146+
const ctx: any = {
147+
scale: vi.fn((x: number, y: number) => record('scale', x, y)),
148+
fillRect: vi.fn((x: number, y: number, w: number, h: number) => record('fillRect', x, y, w, h)),
149+
drawImage: vi.fn((...args: any[]) => record('drawImage', ...args)),
150+
fill: vi.fn((...args: any[]) => record('fill', ...args)),
151+
};
152+
Object.defineProperty(ctx, 'fillStyle', {
153+
get: () => fillStyleCache,
154+
set: (v) => {
155+
fillStyleCache = v;
156+
record('fillStyle', v);
157+
},
158+
configurable: true,
159+
});
160+
Object.defineProperty(ctx, 'globalAlpha', {
161+
get: () => alphaCache,
162+
set: (v) => {
163+
alphaCache = v;
164+
record('globalAlpha', v);
165+
},
166+
configurable: true,
167+
});
168+
169+
return { ctx, calls };
170+
};
171+
172+
describe('QRCodeCanvas - canvas drawing and refs (merged)', () => {
173+
afterEach(() => {
174+
vi.restoreAllMocks();
175+
cleanup();
176+
});
177+
178+
it('assigns canvas to object ref and draws using fallback rectangles when Path2D unsupported', async () => {
179+
const { ctx, calls } = createMockCtx();
180+
vi.spyOn(HTMLCanvasElement.prototype as any, 'getContext').mockImplementation(() => ctx);
181+
182+
const { QRCodeCanvas } = await import('../QRCodeCanvas');
183+
184+
const ref = React.createRef<HTMLCanvasElement>();
185+
const { container } = render(<QRCodeCanvas value="hello-canvas" size={128} includeMargin={false} ref={ref} />);
186+
187+
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
188+
expect(canvas).toBeTruthy();
189+
expect(ref.current).toBe(canvas);
190+
191+
await waitFor(() => {
192+
expect(calls.fillRect?.length).toBeGreaterThan(0);
193+
expect(calls.scale?.length).toBe(1);
194+
});
195+
});
196+
197+
it('supports function ref', async () => {
198+
const { ctx } = createMockCtx();
199+
vi.spyOn(HTMLCanvasElement.prototype as any, 'getContext').mockImplementation(() => ctx);
200+
201+
const { QRCodeCanvas } = await import('../QRCodeCanvas');
202+
203+
let received: HTMLCanvasElement | null = null;
204+
const setRef = (el: HTMLCanvasElement | null) => {
205+
received = el;
206+
};
207+
const { container } = render(<QRCodeCanvas value="ref-test" size={64} ref={setRef} />);
208+
209+
const canvas = container.querySelector('canvas');
210+
expect(canvas).toBe(received);
211+
});
212+
});
213+
214+
describe('QRCodeCanvas - image settings, excavation and crossOrigin (merged)', () => {
215+
afterEach(() => {
216+
vi.restoreAllMocks();
217+
cleanup();
218+
});
219+
220+
it('sets globalAlpha and draws image when icon loads; excavates modules', async () => {
221+
const { ctx, calls } = createMockCtx();
222+
vi.spyOn(HTMLCanvasElement.prototype as any, 'getContext').mockImplementation(() => ctx);
223+
224+
const utils = await import('../../_common/js/qrcode/utils');
225+
const excavateSpy = vi.spyOn(utils, 'excavateModules');
226+
227+
const { QRCodeCanvas } = await import('../QRCodeCanvas');
228+
229+
const { container } = render(
230+
<QRCodeCanvas
231+
value="with-image"
232+
size={128}
233+
includeMargin
234+
imageSettings={{
235+
src: 'https://example.com/icon.png',
236+
width: 20,
237+
height: 20,
238+
excavate: true,
239+
opacity: 0.5,
240+
crossOrigin: 'use-credentials',
241+
}}
242+
/>,
243+
);
244+
245+
const img = container.querySelector('img') as HTMLImageElement;
246+
expect(img).toBeTruthy();
247+
expect(img.getAttribute('crossorigin') || (img as any).crossOrigin).toBe('use-credentials');
248+
249+
Object.defineProperty(img, 'complete', { configurable: true, get: () => true });
250+
Object.defineProperty(img, 'naturalWidth', { configurable: true, get: () => 10 });
251+
Object.defineProperty(img, 'naturalHeight', { configurable: true, get: () => 10 });
252+
253+
img.dispatchEvent(new Event('load'));
254+
255+
await waitFor(() => {
256+
const alphaSets = calls.globalAlpha || [];
257+
expect(alphaSets.some((args) => args?.[0] === 0.5)).toBe(true);
258+
expect(calls.drawImage?.length).toBeGreaterThan(0);
259+
expect(excavateSpy).toHaveBeenCalled();
260+
});
261+
});
262+
263+
it('uses Path2D branch when supported', async () => {
264+
class Path2DStub {
265+
d: string | undefined;
266+
267+
constructor(d?: string) {
268+
this.d = d;
269+
}
270+
271+
addPath() {
272+
return this;
273+
}
274+
}
275+
vi.stubGlobal('Path2D', Path2DStub as any);
276+
277+
vi.resetModules();
278+
vi.doMock('../../_common/js/qrcode/utils', async () => {
279+
const actual: any = await vi.importActual('../../_common/js/qrcode/utils');
280+
return { ...actual, isSupportPath2d: true };
281+
});
282+
283+
const { ctx, calls } = createMockCtx();
284+
vi.spyOn(HTMLCanvasElement.prototype as any, 'getContext').mockImplementation(() => ctx);
285+
286+
const { QRCodeCanvas } = await import('../QRCodeCanvas');
287+
288+
render(<QRCodeCanvas value="p2d" size={96} />);
289+
290+
await waitFor(() => {
291+
expect(calls.fill?.length).toBeGreaterThan(0);
292+
expect(calls.fillRect?.length).toBeGreaterThan(0);
293+
});
294+
});
295+
});

src/qrcode/hooks/useQRCode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type QRProps = {
2727
*/
2828
bgColor?: string;
2929
/**
30-
* The foregtound color used to render the QR Code.
30+
* The foreground color used to render the QR Code.
3131
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
3232
* @defaultValue #000000
3333
*/

0 commit comments

Comments
 (0)