返回 CodeWhale
qr.test.mjs
1 import test from "node:test";
2 import assert from "node:assert/strict";
3
4 import { encodeQr, renderQrToText } from "../src/qr.mjs";
5
6 // The rendered QR is the login credential, so a wrong matrix is worse than no
7 // QR at all. These checks pin the structural invariants a scanner relies on:
8 // finder patterns, timing patterns, and the format-information copies.
9
10 function finderOk(matrix, row, col) {
11 for (let r = 0; r < 7; r++) {
12 for (let c = 0; c < 7; c++) {
13 const expected =
14 r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4);
15 if (Boolean(matrix[row + r][col + c]) !== expected) return false;
16 }
17 }
18 return true;
19 }
20
21 const SAMPLE = "https://liteapp.weixin.qq.com/q/7GiQu1?qrcode=c09677d820dc2b705c2ba8c89dee2b4c&bot_type=3";
22
23 test("encodeQr picks a valid version size", () => {
24 // Version 1 is 21x21 and each version adds 4 modules.
25 for (const text of ["A", "HELLO", SAMPLE]) {
26 const size = encodeQr(text).length;
27 assert.equal((size - 17) % 4, 0, `size ${size} is not a valid QR size`);
28 assert.ok(size >= 21, `size ${size} below version 1`);
29 }
30 });
31
32 test("encodeQr places the three finder patterns", () => {
33 const matrix = encodeQr(SAMPLE);
34 const size = matrix.length;
35 assert.ok(finderOk(matrix, 0, 0), "top-left finder");
36 assert.ok(finderOk(matrix, 0, size - 7), "top-right finder");
37 assert.ok(finderOk(matrix, size - 7, 0), "bottom-left finder");
38 });
39
40 test("encodeQr writes the alternating timing patterns", () => {
41 const matrix = encodeQr(SAMPLE);
42 const size = matrix.length;
43 for (let i = 8; i < size - 8; i++) {
44 assert.equal(matrix[6][i], i % 2 === 0 ? 1 : 0, `row timing at ${i}`);
45 assert.equal(matrix[i][6], i % 2 === 0 ? 1 : 0, `column timing at ${i}`);
46 }
47 });
48
49 test("encodeQr sets the fixed dark module", () => {
50 const matrix = encodeQr(SAMPLE);
51 assert.equal(matrix[matrix.length - 8][8], 1);
52 });
53
54 test("format information is mirrored between its two copies", () => {
55 const matrix = encodeQr(SAMPLE);
56 const size = matrix.length;
57 // Bits 0-5 run down column 8; the second copy of bits 8-14 runs down column 8
58 // near the bottom-left finder. Pin that the split exists and is populated.
59 const firstCopy = [0, 1, 2, 3, 4, 5].map((r) => matrix[r][8]);
60 const secondCopy = [8, 9, 10, 11, 12, 13, 14].map((i) => matrix[size - 15 + i][8]);
61 assert.ok(
62 firstCopy.every((v) => v === 0 || v === 1),
63 "first format copy must be fully written"
64 );
65 assert.ok(
66 secondCopy.every((v) => v === 0 || v === 1),
67 "second format copy must be fully written"
68 );
69 });
70
71 test("encodeQr round-trips a UTF-8 payload without throwing", () => {
72 // 18 UTF-8 bytes needs version 2 (25x25), not version 1.
73 const matrix = encodeQr("微信扫码测试");
74 assert.equal(matrix.length, 25);
75 assert.equal((matrix.length - 17) % 4, 0);
76 });
77
78 test("encodeQr rejects payloads beyond the supported versions", () => {
79 assert.throws(() => encodeQr("x".repeat(400)), /too long/);
80 });
81
82 test("renderQrToText produces half-block rows covering the whole matrix", () => {
83 const text = renderQrToText(SAMPLE);
84 const lines = text.split("\n");
85 const matrix = encodeQr(SAMPLE);
86 const quietZone = 2;
87 const padded = matrix.length + quietZone * 2;
88 const expectedRows = padded % 2 === 0 ? padded : padded + 1;
89
90 assert.equal(lines.length, expectedRows / 2, "one text row per two module rows");
91 for (const line of lines) {
92 assert.equal([...line].length, padded, "every row is padded to the same width");
93 }
94 });
95
96 test("renderQrToText emits only half-block glyphs and spaces", () => {
97 const text = renderQrToText(SAMPLE);
98 assert.match(text, /^[\u2580\u2584\u2588 \n]+$/);
99 });
100
100 lines Plain Text