tauri-android-tax/tests/qrscan.test.js

132 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from "vitest";
import QRCode from "qrcode";
import {
extractUrl,
decodeQrFromImageData,
scaleImageData,
} 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;
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 };
}
/**
* 将小二维码嵌入大画布中央,模拟手机拍摄的远景照片(二维码占比小)。
* @param {string} text 二维码内容。
* @param {number} canvasW 大画布宽度。
* @param {number} canvasH 大画布高度。
* @returns {ImageData} 大画布像素数据。
*/
function embedSmallQr(text, canvasW = 600, canvasH = 400) {
const qr = QRCode.create(text, { errorCorrectionLevel: "M" });
const size = qr.modules.size;
const modules = qr.modules.data;
const scale = 3; // 每个模块 3px二维码约 80px
const quiet = 1;
const qrW = (size + quiet * 2) * scale;
const qrH = qrW;
const ox = Math.floor((canvasW - qrW) / 2);
const oy = Math.floor((canvasH - qrH) / 2);
const data = new Uint8ClampedArray(canvasW * canvasH * 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;
const x0 = ox + (col + quiet) * scale;
const y0 = oy + (row + quiet) * scale;
for (let dy = 0; dy < scale; dy += 1) {
for (let dx = 0; dx < scale; dx += 1) {
const idx = ((y0 + dy) * canvasW + (x0 + dx)) * 4;
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
data[idx + 3] = 255;
}
}
}
}
return { data, width: canvasW, height: canvasH };
}
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();
});
// 4-4 多级缩放回退:大画布中小二维码,原始尺寸失败但缩小后成功。
it("4-4 多级缩放回退能识别大图中的小二维码", () => {
const url = "https://inv.example.com/small";
const imageData = embedSmallQr(url, 800, 600);
const result = decodeQrFromImageData(imageData);
expect(result).toBe(url);
});
// 4-5 scaleImageData 等比缩放正确。
it("4-5 scaleImageData 等比缩放", () => {
const src = renderQrImageData("test", 4);
const half = scaleImageData(src, 0.5);
expect(half.width).toBe(Math.floor(src.width * 0.5));
expect(half.height).toBe(Math.floor(src.height * 0.5));
expect(half.data).toBeInstanceOf(Uint8ClampedArray);
expect(half.data.length).toBe(half.width * half.height * 4);
expect(half.height).toBe(Math.floor(src.height * 0.5));
});
});