screentinker/Examples/PIP-Welcome-Board/test.js
screentinker 0b138f10c6
Add PiP overlay example recipes (#132)
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:20:37 -05:00

78 lines
3.9 KiB
JavaScript

'use strict';
// Offline test for the welcome board. No network, no PiP push — exercises the
// pure functions against a FIXED 'now' so date filtering is deterministic.
const assert = require('assert');
const w = require('./welcome');
let checks = 0;
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(`${msg}`); checks++; };
// FIXED now: March 14 2026, 09:00 local. Built from local components and read
// back via local getters, so it's timezone-independent.
const NOW = new Date(2026, 2, 14, 9, 0, 0);
// --- CSV parsing (incl. a quoted field containing a comma) ----------------
const csv =
'type,name,date,note\n' +
'welcome,Visitors,,"Thanks, room 204"\n' +
'birthday,Priya Nair,03-14,\n' +
'birthday,Marcus Webb,07-04,Cake!\n' +
'anniversary,Dana Olsen,2019-03-14,"Lead, infra"\n';
const rows = w.parseCsv(csv);
ok(rows.length === 4, 'parseCsv reads 4 data rows (header skipped)');
ok(rows[0].note === 'Thanks, room 204', 'quoted field with a comma is preserved');
ok(rows[1].type === 'birthday' && rows[1].date === '03-14', 'columns map to header keys');
// --- date helpers ---------------------------------------------------------
ok(w.mmddOf(NOW) === '03-14', 'mmddOf(NOW) === 03-14');
ok(w.mmdd('2019-03-14') === '03-14', 'mmdd strips the year from YYYY-MM-DD');
ok(w.mmdd('3-14') === '03-14', 'mmdd zero-pads MM-DD');
ok(w.yearOf('2019-03-14') === 2019, 'yearOf reads the year');
ok(w.yearOf('03-14') === null, 'yearOf is null when no year present');
// --- todaysEntries filtering ----------------------------------------------
const today = w.todaysEntries(rows, NOW, { showAllWhenEmpty: true });
const names = today.map((r) => r.name);
ok(names.includes('Priya Nair'), "today includes the 03-14 birthday");
ok(names.includes('Dana Olsen'), "today includes the 03-14 anniversary");
ok(names.includes('Visitors'), 'today always includes welcome rows');
ok(!names.includes('Marcus Webb'), 'the 07-04 birthday is excluded today');
// fall back to all rows when nothing qualifies (no welcomes, no date match)
const datedOnly = [
{ type: 'birthday', name: 'Nobody', date: '01-01', note: '' },
];
const fb = w.todaysEntries(datedOnly, NOW, { showAllWhenEmpty: true });
ok(fb.length === 1 && fb[0].name === 'Nobody', 'show_all_when_empty falls back to all rows');
const noFb = w.todaysEntries(datedOnly, NOW, { showAllWhenEmpty: false });
ok(noFb.length === 0, 'with show_all_when_empty=false, nothing shows when nothing qualifies');
// --- message building -----------------------------------------------------
const bday = w.buildMessage({ type: 'birthday', name: 'Priya Nair' }, NOW);
ok(bday.emoji === '🎂' && bday.greeting === 'Happy Birthday' && bday.name === 'Priya Nair', 'birthday message');
const anniv = w.buildMessage({ type: 'anniversary', name: 'Dana Olsen', date: '2019-03-14' }, NOW);
ok(anniv.greeting === '7 Years!', 'anniversary with a year computes "7 Years!" (2026-2019)');
const anniv1 = w.buildMessage({ type: 'anniversary', name: 'One Yr', date: '2025-03-14' }, NOW);
ok(anniv1.greeting === '1 Year!', 'anniversary singular: "1 Year!"');
const annivNoYr = w.buildMessage({ type: 'anniversary', name: 'X', date: '03-14' }, NOW);
ok(annivNoYr.greeting === 'Happy Work Anniversary', 'anniversary without a year falls back to generic greeting');
const welc = w.buildMessage({ type: 'welcome', name: 'Visitors' }, NOW);
ok(welc.emoji === '👋' && welc.greeting === 'Welcome', 'welcome message');
// --- overlay URI ----------------------------------------------------------
const uri = w.buildOverlayUri('https://x/welcome-overlay.html', bday);
const parsed = new URLSearchParams(uri.split('?')[1]);
ok(parsed.get('name') === 'Priya Nair', 'overlay uri carries the name through URLSearchParams');
ok(parsed.get('emoji') === '🎂', 'overlay uri carries the emoji');
ok(/^[0-9A-Fa-f]{6}$/.test(parsed.get('color')), 'overlay uri color is exactly 6 hex digits');
console.log(`\n${checks} checks passed`);
console.log('\nRESULT: PASS ✅');