import { describe, it, expect } from "vitest"; import QRCode from "qrcode"; import { extractUrl, decodeQrFromImageData, } from "../src/qrscan.js"; /** * 用 qrcode 库把文本渲染成 RGBA 像素数据(放大每个模块以贴近真实拍摄清晰度)。 * @param {string} text 待编码文本。 * @param {number} scale 每个二维码模块放大的像素倍数。 * @param {number} quiet 静区(模块数)。 * @returns {{data:Uint8ClampedArray,width:number,height:number}} 画布像素数据。 */ function renderQrImageData(text, scale = 8, quiet = 4) { const qr = QRCode.create(text, { errorCorrectionLevel: "M" }); const size = qr.modules.size; const modules = qr.modules.data; const dim = (size + quiet * 2) * scale; const data = new Uint8ClampedArray(dim * dim * 4); // 先整体填白(含静区)。 data.fill(255); for (let row = 0; row < size; row += 1) { for (let col = 0; col < size; col += 1) { if (!modules[row * size + col]) continue; // 0 = 浅色,跳过 const x0 = (col + quiet) * scale; const y0 = (row + quiet) * scale; for (let dy = 0; dy < scale; dy += 1) { for (let dx = 0; dx < scale; dx += 1) { const idx = ((y0 + dy) * dim + (x0 + dx)) * 4; data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255; } } } } return { data, width: dim, height: dim }; } describe("qrscan", () => { // 4-1 网址校验:接受 http/https,拒绝其它内容。 it("4-1 extractUrl 仅接受 http/https 网址", () => { expect(extractUrl("https://example.com/inv?id=1")).toBe( "https://example.com/inv?id=1" ); expect(extractUrl(" http://a.b/c ")).toBe("http://a.b/c"); expect(extractUrl("ftp://x")).toBe(""); expect(extractUrl("hello")).toBe(""); expect(extractUrl(null)).toBe(""); }); // 4-2 从生成的清晰二维码像素中解码,验证扫描核心逻辑正确。 it("4-2 decodeQrFromImageData 能识别清晰二维码", () => { const url = "https://fapiao.example.com/open?id=20240701"; const imageData = renderQrImageData(url); expect(decodeQrFromImageData(imageData)).toBe(url); }); // 4-3 无法识别时返回 null,而不是抛错。 it("4-3 无二维码时返回 null", () => { const blank = { data: new Uint8ClampedArray(64 * 64 * 4).fill(255), width: 64, height: 64, }; expect(decodeQrFromImageData(blank)).toBeNull(); expect(decodeQrFromImageData(null)).toBeNull(); }); });