screentinker/Examples/PIP-QR-Rotator/test.js
ScreenTinker ab771ec595 Add PiP overlay example recipes
Self-contained examples for the PiP overlay API (POST /api/pip), each
with a CSP-safe query-param overlay (external JS), config.example.json,
zero runtime deps, an offline test, and a README:

- PIP-Announce-Broadcast    manual one-shot message to a screen/group
- PIP-Weather-Widget        Open-Meteo current conditions (keyless)
- PIP-Air-Quality           Open-Meteo US AQI widget (keyless)
- PIP-Crypto-Ticker         CoinGecko price strip (keyless)
- PIP-News-Ticker           scrolling RSS/Atom headlines
- PIP-Room-Status-Calendar  ICS-driven Available/Busy room sign
- PIP-Event-Countdown       client-side countdown, auto-clears at zero
- PIP-Welcome-Board         rotating welcome/birthday cards from CSV
- PIP-Fundraiser-Thermometer goal-progress bar from local/URL JSON
- PIP-QR-Rotator            rotating QR codes, encoded client-side
- PIP-Incident-Webhook      event-driven: red on firing, clear on resolved

Also includes the CAP-AU (NSW RFS) and US NWS/NOAA emergency-alert
monitors that push expiry-aware PiP overlays.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 20:17:38 -05:00

73 lines
3.9 KiB
JavaScript

'use strict';
// Offline test. No network, no player. Covers:
// - qr.js pure helpers (entry validation, overlay-uri build/round-trip, rotation wrap)
// - the embedded QR encoder's Reed-Solomon core, checked against the published QR
// generator polynomials (degree 7 and 10) — this catches GF(256) math errors without
// needing a QR decoder — plus structural invariants of a generated matrix.
const { validateEntries, overlayUri, nextIndex } = require('./qr');
const QR = require('./qr-overlay');
let ok = true;
function check(name, cond) { console.log(`${cond ? '•' : '✗'} ${name}`); if (!cond) ok = false; }
// ---- qr.js pure helpers ----
const v = validateEntries([
{ label: 'A', data: 'https://x.test/1' },
{ label: 'B', data: ' ' }, // blank -> rejected
{ data: 'WIFI:T:WPA;S:Net;P:pw;;' }, // no label -> ok, label defaults to ''
{ label: 'C' }, // no data -> rejected
]);
check('validateEntries keeps the 2 valid entries', v.entries.length === 2);
check('validateEntries reports the 2 bad entries', v.errors.length === 2);
check('validateEntries defaults missing label to ""', v.entries[1].label === '');
check('validateEntries non-array -> error', validateEntries('nope').errors.length === 1);
const entry = { label: 'Guest Wi-Fi & More', data: 'WIFI:T:WPA;S:Lobby Guest;P:p@ss=1;;' };
const uri = overlayUri('https://s.example.com/qr-overlay.html', entry);
const back = new URLSearchParams(uri.split('?')[1]);
check('overlayUri round-trips data exactly', back.get('data') === entry.data);
check('overlayUri round-trips label exactly', back.get('label') === entry.label);
check('overlayUri encodes (no raw spaces/&/;)', !/[ &;]/.test(uri.split('?')[1].replace(/&data=|&label=/, '')));
check('overlayUri joins with & when base already has ?',
overlayUri('https://s/x?a=1', { data: 'd' }).includes('?a=1&'));
check('nextIndex wraps around', nextIndex(2, 3) === 0 && nextIndex(0, 3) === 1 && nextIndex(1, 3) === 2);
check('nextIndex guards empty list', nextIndex(0, 0) === 0);
// ---- Reed-Solomon core vs published QR generator polynomials ----
// Build GF(256) exp/log tables from the encoder's own multiply, then convert the computed
// divisor coefficients back to alpha-exponent form to compare with the spec's values.
const exp = new Array(256), log = new Array(256);
exp[0] = 1;
for (let i = 1; i < 256; i++) exp[i] = QR.rsMul(exp[i - 1], 2);
for (let i = 0; i < 255; i++) log[exp[i]] = i;
function toAlpha(coeffs) { return coeffs.map((c) => log[c]); }
// Published non-leading generator-polynomial exponents (Thonky / ISO 18004 Annex A).
const GEN7 = [87, 229, 146, 149, 238, 102, 21];
const GEN10 = [251, 67, 46, 61, 118, 70, 64, 94, 32, 45];
const d7 = toAlpha(QR.rsDivisor(7));
const d10 = toAlpha(QR.rsDivisor(10));
check('RS generator poly (deg 7) matches spec', JSON.stringify(d7) === JSON.stringify(GEN7));
check('RS generator poly (deg 10) matches spec', JSON.stringify(d10) === JSON.stringify(GEN10));
// ---- encoder structural invariants ----
const tiny = QR.encodeBytes(QR.utf8Bytes('hi'), 'M'); // tiny -> version 1
check('tiny payload -> 21x21 (version 1)', tiny.size === 21 && tiny.modules.length === 21);
// finder patterns: dark outer ring at the three corners, white separator beside them.
check('top-left finder corner dark', tiny.modules[0][0] === true);
check('top-left separator light', tiny.modules[0][7] === false);
check('top-left finder centre dark', tiny.modules[3][3] === true);
check('top-right finder present', tiny.modules[0][tiny.size - 1] === true);
check('bottom-left finder present', tiny.modules[tiny.size - 1][0] === true);
// timing pattern alternates along row/col 6
check('timing pattern alternates', tiny.modules[6][8] !== tiny.modules[6][9]);
const url = QR.encodeBytes(QR.utf8Bytes('https://example.com/menu'), 'M');
check('longer URL bumps the version (size > 21)', url.size > 21 && (url.size - 17) % 4 === 0);
console.log('\nRESULT:', ok ? 'PASS ✅' : 'FAIL ❌');
process.exit(ok ? 0 : 1);