mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 19:04:46 +00:00
feat(vue/primitives): implement pagination components with accessibility and testing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '.';
|
||||
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '..';
|
||||
|
||||
/* -- Test data -- */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { encodeText, encodeBinary, makeSegments, isNumeric, isAlphanumeric, QrCode, EccMap, LOW, MEDIUM, HIGH } from '.';
|
||||
import { encodeText, encodeBinary, encodeSegments, makeSegments, isNumeric, isAlphanumeric, QrCode, QrCodeDataType, EccMap, LOW, MEDIUM, QUARTILE, HIGH } from '..';
|
||||
|
||||
describe('isNumeric', () => {
|
||||
it('accepts pure digit strings', () => {
|
||||
@@ -180,3 +180,78 @@ describe('EccMap', () => {
|
||||
expect(qr).toBeInstanceOf(QrCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeSegments', () => {
|
||||
it('uses explicit mask when specified', () => {
|
||||
const qr = encodeSegments(makeSegments('Test'), LOW, 1, 40, 3);
|
||||
expect(qr.mask).toBe(3);
|
||||
});
|
||||
|
||||
it('preserves ECC level when boostEcl is false', () => {
|
||||
const qr = encodeSegments(makeSegments('Test'), LOW, 1, 40, -1, false);
|
||||
expect(qr.ecc).toBe(LOW);
|
||||
});
|
||||
|
||||
it('boosts ECC level by default when data fits', () => {
|
||||
const qr = encodeSegments(makeSegments('Test'), LOW);
|
||||
expect(qr.ecc).toBe(HIGH);
|
||||
});
|
||||
|
||||
it('forces a specific version when min equals max', () => {
|
||||
const qr = encodeSegments(makeSegments('Test'), LOW, 5, 5);
|
||||
expect(qr.version).toBe(5);
|
||||
});
|
||||
|
||||
it('throws on invalid version range', () => {
|
||||
expect(() => encodeSegments(makeSegments('Test'), LOW, 2, 1)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('throws on invalid mask value', () => {
|
||||
expect(() => encodeSegments(makeSegments('Test'), LOW, 1, 40, 8)).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeBinary edge cases', () => {
|
||||
it('encodes an empty array', () => {
|
||||
const qr = encodeBinary([], LOW);
|
||||
expect(qr).toBeInstanceOf(QrCode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeText edge cases', () => {
|
||||
it('encodes Unicode emoji text', () => {
|
||||
const qr = encodeText('Hello \uD83C\uDF0D', LOW);
|
||||
expect(qr).toBeInstanceOf(QrCode);
|
||||
expect(qr.size).toBeGreaterThanOrEqual(21);
|
||||
});
|
||||
|
||||
it('uses compact encoding for alphanumeric text', () => {
|
||||
const qr = encodeText('HELLO WORLD', LOW);
|
||||
expect(qr.version).toBe(1);
|
||||
});
|
||||
|
||||
it('selects version >= 7 for long data (triggers drawVersion)', () => {
|
||||
const qr = encodeText('a'.repeat(200), LOW);
|
||||
expect(qr.version).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getType semantics', () => {
|
||||
it('identifies finder pattern modules as Position', () => {
|
||||
const qr = encodeText('Test', LOW);
|
||||
// Top-left finder pattern
|
||||
expect(qr.getType(0, 0)).toBe(QrCodeDataType.Position);
|
||||
expect(qr.getType(3, 3)).toBe(QrCodeDataType.Position);
|
||||
expect(qr.getType(6, 6)).toBe(QrCodeDataType.Position);
|
||||
// Top-right finder pattern
|
||||
expect(qr.getType(qr.size - 1, 0)).toBe(QrCodeDataType.Position);
|
||||
// Bottom-left finder pattern
|
||||
expect(qr.getType(0, qr.size - 1)).toBe(QrCodeDataType.Position);
|
||||
});
|
||||
|
||||
it('identifies timing pattern modules as Timing', () => {
|
||||
const qr = encodeText('Test', LOW);
|
||||
// Horizontal timing row y=6, between finders
|
||||
expect(qr.getType(8, 6)).toBe(QrCodeDataType.Timing);
|
||||
});
|
||||
});
|
||||
92
core/encoding/src/qr/__test__/segment.test.ts
Normal file
92
core/encoding/src/qr/__test__/segment.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { QrSegment, makeNumeric, makeAlphanumeric, makeBytes } from '../segment';
|
||||
import { MODE_ALPHANUMERIC, MODE_BYTE, MODE_NUMERIC } from '../constants';
|
||||
|
||||
describe('QrSegment', () => {
|
||||
it('throws on negative numChars', () => {
|
||||
expect(() => new QrSegment(MODE_BYTE, -1, [])).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('accepts zero numChars', () => {
|
||||
const seg = new QrSegment(MODE_BYTE, 0, []);
|
||||
expect(seg.numChars).toBe(0);
|
||||
expect(seg.bitData).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeNumeric', () => {
|
||||
it('encodes a 5-digit string', () => {
|
||||
const seg = makeNumeric('12345');
|
||||
expect(seg.mode).toBe(MODE_NUMERIC);
|
||||
expect(seg.numChars).toBe(5);
|
||||
// "123" → 10 bits, "45" → 7 bits
|
||||
expect(seg.bitData).toHaveLength(17);
|
||||
});
|
||||
|
||||
it('encodes a single digit', () => {
|
||||
const seg = makeNumeric('0');
|
||||
expect(seg.numChars).toBe(1);
|
||||
expect(seg.bitData).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('encodes an empty string', () => {
|
||||
const seg = makeNumeric('');
|
||||
expect(seg.numChars).toBe(0);
|
||||
expect(seg.bitData).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('throws on non-numeric input', () => {
|
||||
expect(() => makeNumeric('12a3')).toThrow(RangeError);
|
||||
expect(() => makeNumeric('hello')).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeAlphanumeric', () => {
|
||||
it('encodes a character pair', () => {
|
||||
const seg = makeAlphanumeric('AB');
|
||||
expect(seg.mode).toBe(MODE_ALPHANUMERIC);
|
||||
expect(seg.numChars).toBe(2);
|
||||
// 1 pair → 11 bits
|
||||
expect(seg.bitData).toHaveLength(11);
|
||||
});
|
||||
|
||||
it('encodes a pair plus remainder', () => {
|
||||
const seg = makeAlphanumeric('ABC');
|
||||
expect(seg.numChars).toBe(3);
|
||||
// 1 pair (11 bits) + 1 remainder (6 bits)
|
||||
expect(seg.bitData).toHaveLength(17);
|
||||
});
|
||||
|
||||
it('throws on lowercase input', () => {
|
||||
expect(() => makeAlphanumeric('hello')).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('throws on invalid characters', () => {
|
||||
expect(() => makeAlphanumeric('test@email')).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeBytes', () => {
|
||||
it('encodes an empty array', () => {
|
||||
const seg = makeBytes([]);
|
||||
expect(seg.mode).toBe(MODE_BYTE);
|
||||
expect(seg.numChars).toBe(0);
|
||||
expect(seg.bitData).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('encodes two bytes', () => {
|
||||
const seg = makeBytes([0x48, 0x65]);
|
||||
expect(seg.numChars).toBe(2);
|
||||
expect(seg.bitData).toHaveLength(16);
|
||||
});
|
||||
|
||||
it('encodes 0xFF correctly', () => {
|
||||
const seg = makeBytes([0xFF]);
|
||||
expect(seg.bitData).toEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
it('encodes 0x00 correctly', () => {
|
||||
const seg = makeBytes([0x00]);
|
||||
expect(seg.bitData).toEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
});
|
||||
});
|
||||
119
core/encoding/src/qr/__test__/utils.test.ts
Normal file
119
core/encoding/src/qr/__test__/utils.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { appendBits, getBit, getNumDataCodewords, getNumRawDataModules, getTotalBits, numCharCountBits } from '../utils';
|
||||
import { HIGH, LOW, MODE_BYTE, MODE_NUMERIC } from '../constants';
|
||||
import { QrSegment } from '../segment';
|
||||
|
||||
describe('appendBits', () => {
|
||||
it('appends nothing when len is 0', () => {
|
||||
const bb: number[] = [];
|
||||
appendBits(0, 0, bb);
|
||||
expect(bb).toEqual([]);
|
||||
});
|
||||
|
||||
it('appends bits in MSB-first order', () => {
|
||||
const bb: number[] = [];
|
||||
appendBits(0b101, 3, bb);
|
||||
expect(bb).toEqual([1, 0, 1]);
|
||||
});
|
||||
|
||||
it('appends to an existing array', () => {
|
||||
const bb = [1, 0];
|
||||
appendBits(0b11, 2, bb);
|
||||
expect(bb).toEqual([1, 0, 1, 1]);
|
||||
});
|
||||
|
||||
it('throws when value exceeds bit length', () => {
|
||||
expect(() => appendBits(5, 2, [])).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('throws on negative length', () => {
|
||||
expect(() => appendBits(0, -1, [])).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBit', () => {
|
||||
it('returns correct bits for 0b10110', () => {
|
||||
expect(getBit(0b10110, 0)).toBe(false);
|
||||
expect(getBit(0b10110, 1)).toBe(true);
|
||||
expect(getBit(0b10110, 2)).toBe(true);
|
||||
expect(getBit(0b10110, 3)).toBe(false);
|
||||
expect(getBit(0b10110, 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for high bits of a small number', () => {
|
||||
expect(getBit(1, 7)).toBe(false);
|
||||
expect(getBit(1, 31)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumRawDataModules', () => {
|
||||
it('returns 208 for version 1', () => {
|
||||
expect(getNumRawDataModules(1)).toBe(208);
|
||||
});
|
||||
|
||||
it('returns correct value for version 2 (with alignment)', () => {
|
||||
expect(getNumRawDataModules(2)).toBe(359);
|
||||
});
|
||||
|
||||
it('returns correct value for version 7 (with version info)', () => {
|
||||
expect(getNumRawDataModules(7)).toBe(1568);
|
||||
});
|
||||
|
||||
it('returns 29648 for version 40', () => {
|
||||
expect(getNumRawDataModules(40)).toBe(29648);
|
||||
});
|
||||
|
||||
it('throws on version 0', () => {
|
||||
expect(() => getNumRawDataModules(0)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('throws on version 41', () => {
|
||||
expect(() => getNumRawDataModules(41)).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumDataCodewords', () => {
|
||||
it('returns 19 for version 1 LOW', () => {
|
||||
expect(getNumDataCodewords(1, LOW)).toBe(19);
|
||||
});
|
||||
|
||||
it('returns 9 for version 1 HIGH', () => {
|
||||
expect(getNumDataCodewords(1, HIGH)).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalBits', () => {
|
||||
it('returns 0 for empty segments', () => {
|
||||
expect(getTotalBits([], 1)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns Infinity when numChars overflows char count field', () => {
|
||||
// MODE_BYTE at v1 has ccbits=8, so numChars=256 overflows
|
||||
const seg = new QrSegment(MODE_BYTE, 256, []);
|
||||
expect(getTotalBits([seg], 1)).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
it('calculates total bits for a single segment', () => {
|
||||
// MODE_BYTE at v1: 4 (mode) + 8 (char count) + 8 (data) = 20
|
||||
const seg = new QrSegment(MODE_BYTE, 1, [0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(getTotalBits([seg], 1)).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('numCharCountBits', () => {
|
||||
it('returns correct bits for MODE_NUMERIC across version ranges', () => {
|
||||
expect(numCharCountBits(MODE_NUMERIC, 1)).toBe(10);
|
||||
expect(numCharCountBits(MODE_NUMERIC, 9)).toBe(10);
|
||||
expect(numCharCountBits(MODE_NUMERIC, 10)).toBe(12);
|
||||
expect(numCharCountBits(MODE_NUMERIC, 26)).toBe(12);
|
||||
expect(numCharCountBits(MODE_NUMERIC, 27)).toBe(14);
|
||||
expect(numCharCountBits(MODE_NUMERIC, 40)).toBe(14);
|
||||
});
|
||||
|
||||
it('returns correct bits for MODE_BYTE across version ranges', () => {
|
||||
expect(numCharCountBits(MODE_BYTE, 1)).toBe(8);
|
||||
expect(numCharCountBits(MODE_BYTE, 9)).toBe(8);
|
||||
expect(numCharCountBits(MODE_BYTE, 10)).toBe(16);
|
||||
expect(numCharCountBits(MODE_BYTE, 40)).toBe(16);
|
||||
});
|
||||
});
|
||||
@@ -7,15 +7,10 @@
|
||||
|
||||
import type { QrCodeEcc } from './types';
|
||||
import { QrCodeDataType } from './types';
|
||||
import { ECC_CODEWORDS_PER_BLOCK, MAX_VERSION, MIN_VERSION, NUM_ERROR_CORRECTION_BLOCKS } from './constants';
|
||||
import { ECC_CODEWORDS_PER_BLOCK, MAX_VERSION, MIN_VERSION, NUM_ERROR_CORRECTION_BLOCKS, PENALTY_N1, PENALTY_N2, PENALTY_N3, PENALTY_N4 } from './constants';
|
||||
import { assert, getBit, getNumDataCodewords, getNumRawDataModules } from './utils';
|
||||
import { computeDivisor, computeRemainder } from '../reed-solomon';
|
||||
|
||||
const PENALTY_N1 = 3;
|
||||
const PENALTY_N2 = 3;
|
||||
const PENALTY_N3 = 40;
|
||||
const PENALTY_N4 = 10;
|
||||
|
||||
/**
|
||||
* A QR Code symbol, which is a type of two-dimension barcode.
|
||||
* Invented by Denso Wave and described in the ISO/IEC 18004 standard.
|
||||
@@ -102,7 +97,7 @@ export class QrCode {
|
||||
const size = this.size;
|
||||
// Draw horizontal and vertical timing patterns
|
||||
for (let i = 0; i < size; i++) {
|
||||
const dark = i % 2 === 0 ? 1 : 0;
|
||||
const dark = (i & 1) ^ 1;
|
||||
this.setFunctionModule(6, i, dark, QrCodeDataType.Timing);
|
||||
this.setFunctionModule(i, 6, dark, QrCodeDataType.Timing);
|
||||
}
|
||||
@@ -322,10 +317,8 @@ export class QrCode {
|
||||
result++;
|
||||
}
|
||||
else {
|
||||
// finderPenaltyAddHistory inlined
|
||||
if (h0 === 0) runX += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runX; h0 = runX;
|
||||
// finderPenaltyCountPatterns inlined (only when runColor is light = 0)
|
||||
if (runColor === 0) {
|
||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
||||
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||
@@ -335,7 +328,6 @@ export class QrCode {
|
||||
runX = 1;
|
||||
}
|
||||
}
|
||||
// finderPenaltyTerminateAndCount inlined
|
||||
{
|
||||
let currentRunLength = runX;
|
||||
if (runColor === 1) {
|
||||
@@ -415,7 +407,7 @@ export class QrCode {
|
||||
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
||||
assert(k >= 0 && k <= 9);
|
||||
result += k * PENALTY_N4;
|
||||
assert(result >= 0 && result <= 2568888);
|
||||
assert(result >= 0 && result <= 2_568_888);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -423,14 +415,14 @@ export class QrCode {
|
||||
if (this.version === 1)
|
||||
return [];
|
||||
|
||||
const numAlign = ((this.version / 7) | 0)
|
||||
+ 2;
|
||||
const numAlign = ((this.version / 7) | 0) + 2;
|
||||
const step = (this.version === 32)
|
||||
? 26
|
||||
: Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
|
||||
const result = [6];
|
||||
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
|
||||
result.splice(1, 0, pos);
|
||||
const result = Array.from<number>({ length: numAlign });
|
||||
result[0] = 6;
|
||||
for (let i = numAlign - 1, pos = this.size - 7; i >= 1; i--, pos -= step)
|
||||
result[i] = pos;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { computeDivisor, computeRemainder, multiply } from '.';
|
||||
import { computeDivisor, computeRemainder, multiply } from '..';
|
||||
|
||||
describe('multiply', () => {
|
||||
it('multiplies zero by anything to get zero', () => {
|
||||
@@ -97,4 +97,19 @@ describe('computeRemainder', () => {
|
||||
expect(result).toHaveLength(degree);
|
||||
}
|
||||
});
|
||||
|
||||
it('produces correct ECC for QR Version 1-M reference data', () => {
|
||||
const data = [0x40, 0xD2, 0x75, 0x47, 0x76, 0x17, 0x32, 0x06, 0x27, 0x26, 0x96, 0xC6, 0xC6, 0x96, 0x70, 0xEC];
|
||||
const divisor = computeDivisor(10);
|
||||
const result = computeRemainder(data, divisor);
|
||||
expect(result).toEqual(Uint8Array.from([188, 42, 144, 19, 107, 175, 239, 253, 75, 224]));
|
||||
});
|
||||
|
||||
it('is deterministic', () => {
|
||||
const data = [0x10, 0x20, 0x30, 0x40, 0x50];
|
||||
const divisor = computeDivisor(7);
|
||||
const a = computeRemainder(data, divisor);
|
||||
const b = computeRemainder(data, divisor);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user