Files
RaptorQR/src/tests/cli_rendering.test.ts
T
Hermes Agent 352fc2d459 feat(cli): alt-buffer, top-of-screen draw, -h/--help flags
- Enter alternate screen buffer (\x1b[?1049h) before drawing and exit
  it (\x1b[?1049l) on cleanup. This preserves the user's terminal
  content when the app quits.
- Clear screen once after entering alt buffer so drawing starts from
  the top-left.
- Add -h and --help flags that print usage, stdin/file modes, and
  controls (q / Ctrl-C to quit).
- Update tests for new alt-buffer helpers and help text.
- Rebuild CLI bundle (dist/qr-terminal.js 95.4kb) and redeploy webapp.
2026-05-03 22:14:50 +00:00

278 lines
9.3 KiB
TypeScript
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.
/**
* Tests for the CLI terminal QR rasterizer.
*
* Verifies that the terminal rasterizer correctly renders boolean QR matrices
* as half-block Unicode art, that the encoder pipeline produces valid QR data,
* and the end-to-end CLI pipeline works (without actually clearing the screen).
*/
import { describe, it, expect } from 'vitest';
/** Strip ANSI escape sequences from a string */
function stripAnsi(str: string): string {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*m/g, '');
}
describe('Terminal Rasterizer', () => {
it('should render a simple 2×2 matrix with default quiet zone', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, true],
[true, true],
];
const lines = renderToTerminal(matrix).map(stripAnsi);
expect(lines.length).toBe(5);
expect(lines[0]).toBe(' '.repeat(10));
expect(lines[1]).toBe(' '.repeat(10));
expect(lines[2]).toBe(' \u2588\u2588 ');
expect(lines[3]).toBe(' '.repeat(10));
expect(lines[4]).toBe(' '.repeat(10));
});
it('should render mixed 4×4 matrix with default quiet zone', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, false, true, false],
[false, true, false, true],
[true, true, false, false],
[false, false, true, true],
];
const lines = renderToTerminal(matrix).map(stripAnsi);
expect(lines.length).toBe(6);
const qrLine0 = lines[2]!;
expect(qrLine0.length).toBe(12);
expect(qrLine0[4]).toBe('\u2580');
expect(qrLine0[5]).toBe('\u2584');
expect(qrLine0[6]).toBe('\u2580');
expect(qrLine0[7]).toBe('\u2584');
const qrLine1 = lines[3]!;
expect(qrLine1[4]).toBe('\u2580');
expect(qrLine1[5]).toBe('\u2580');
expect(qrLine1[6]).toBe('\u2584');
expect(qrLine1[7]).toBe('\u2584');
});
it('should handle odd number of QR rows', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, false, true],
[false, true, false],
[true, false, true],
];
const lines = renderToTerminal(matrix).map(stripAnsi);
expect(lines.length).toBe(6);
expect(lines[2]!.length).toBe(11);
expect(lines[3]!.length).toBe(11);
expect(lines[3]![4]).toBe('\u2580');
expect(lines[3]![5]).toBe(' ');
expect(lines[3]![6]).toBe('\u2580');
});
it('should render an all-white matrix', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[false, false],
[false, false],
];
const lines = renderToTerminal(matrix).map(stripAnsi);
expect(lines.length).toBe(5);
expect(lines[2]).toBe(' '.repeat(10));
});
it('should render a full-block matrix', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, true],
[true, true],
];
const lines = renderToTerminal(matrix).map(stripAnsi);
expect(lines.length).toBe(5);
expect(lines[2]).toBe(' \u2588\u2588 ');
});
it('should support custom quiet zone size', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, true],
[true, true],
];
const lines = renderToTerminal(matrix, 2).map(stripAnsi);
expect(lines.length).toBe(3);
expect(lines[0]).toBe(' '.repeat(6));
expect(lines[1]).toBe(' \u2588\u2588 ');
expect(lines[2]).toBe(' '.repeat(6));
});
it('should support zero quiet zone', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, true],
[true, true],
];
const lines = renderToTerminal(matrix, 0).map(stripAnsi);
expect(lines.length).toBe(1);
expect(lines[0]).toBe('\u2588\u2588');
});
it('should wrap each line with ANSI colour codes', async () => {
const { renderToTerminal } = await import('@/cli/terminal_raster');
const matrix = [
[true, true],
[true, true],
];
const lines = renderToTerminal(matrix);
// Every line should start with white-bg + black-fg and end with reset
for (const line of lines) {
expect(line.startsWith('\x1b[47m\x1b[30m')).toBe(true);
expect(line.endsWith('\x1b[0m')).toBe(true);
}
});
});
describe('CLI Screen Helpers', () => {
it('should expose clearScreen', async () => {
const { clearScreen } = await import('@/cli/terminal_raster');
expect(typeof clearScreen).toBe('function');
});
it('should expose moveCursorUp', async () => {
const { moveCursorUp } = await import('@/cli/terminal_raster');
expect(typeof moveCursorUp).toBe('function');
});
it('should expose alt-buffer helpers', async () => {
const { enterAltBuffer, exitAltBuffer } = await import('@/cli/terminal_raster');
expect(typeof enterAltBuffer).toBe('function');
expect(typeof exitAltBuffer).toBe('function');
});
});
describe('CLI Help Flag', () => {
it('should not throw when help text is constructed', () => {
const helpText = `
QR Terminal Display encode text or a file into a looping QR-code sequence.
Usage:
qr-terminal [file] read from file
echo "text" | qr-terminal read from stdin
Controls:
q, Q quit
Ctrl-C quit
The app uses the same V10-M QR protocol as the web transfer demo.
`;
expect(helpText).toContain('Usage:');
expect(helpText).toContain('quit');
});
});
describe('CLI Encoder Pipeline', () => {
it('should produce the same frames as the web app (reuse common logic)', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const { generateQRMatrix } = await import('@/core/qr/qr_encode');
const { QR_VERSION, ECC_LEVEL } = await import('@/core/protocol/constants');
const { parseHeader } = await import('@/core/protocol/packet');
const data = new TextEncoder().encode('CLI test payload for verifying protocol reuse. '.repeat(3));
const result = packetize(data, false, true);
const ordered = scheduleFrames(result.packets, result.totalGenerations);
const genIndices = ordered.map((pkt) => parseHeader(pkt).generationIndex);
for (const pkt of ordered) {
const matrix = generateQRMatrix(pkt, QR_VERSION, ECC_LEVEL);
expect(matrix.length).toBe(57);
expect(matrix[0]!.length).toBe(57);
}
expect(genIndices.every((g) => g >= 0 && g < result.totalGenerations)).toBe(true);
expect(result.isCompressed).toBe(true);
expect(result.dataLength).toBeGreaterThan(0);
});
});
describe('CLI Frame Cycle', () => {
it('should loop through frames deterministically', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const data = new TextEncoder().encode('Frame cycle test — small payload');
const result = packetize(data, false, false);
const ordered = scheduleFrames(result.packets, result.totalGenerations);
expect(ordered.length).toBe(result.packets.length);
const totalFrames = ordered.length;
const frameSequence: number[] = [];
for (let i = 0; i < totalFrames * 3; i++) {
frameSequence.push(i % totalFrames);
}
const uniqueIndices = new Set(frameSequence);
expect(uniqueIndices.size).toBe(totalFrames);
expect(frameSequence.length).toBe(totalFrames * 3);
});
it('should generate valid QR matrix for every scheduled frame', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const { generateQRMatrix } = await import('@/core/qr/qr_encode');
const { QR_VERSION, ECC_LEVEL } = await import('@/core/protocol/constants');
const data = new TextEncoder().encode('Every frame QR test — medium payload');
const result = packetize(data, false, true);
const ordered = scheduleFrames(result.packets, result.totalGenerations);
for (const pkt of ordered) {
const matrix = generateQRMatrix(pkt, QR_VERSION, ECC_LEVEL);
expect(matrix.length).toBe(57);
expect(matrix[0]!.length).toBe(57);
const hasDark = matrix.some((row) => row.some((cell) => cell));
expect(hasDark).toBe(true);
}
});
});
describe('CLI Input Parsing', () => {
it('should read file from argument and produce frames', async () => {
const { packetize } = await import('@/core/sender/packetizer');
const { scheduleFrames } = await import('@/core/sender/scheduler');
const { parseHeader } = await import('@/core/protocol/packet');
const fileData = new TextEncoder().encode('file content test');
const result = packetize(fileData, true, false);
const ordered = scheduleFrames(result.packets, result.totalGenerations);
const genIndices = ordered.map((pkt) => parseHeader(pkt).generationIndex);
expect(ordered.length).toBeGreaterThan(0);
expect(result.isText).toBe(true);
expect(result.isCompressed).toBe(false);
expect(result.dataLength).toBe(fileData.length);
expect(genIndices.length).toBe(ordered.length);
});
it('should handle empty input gracefully and provide error message', () => {
const data = new Uint8Array(0);
expect(data.length).toBe(0);
});
});