mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
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>
78 lines
3.9 KiB
JavaScript
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 ✅');
|