Files
CRAPP/test/unit/push-sw.test.ts
T
davideandClaude Sonnet 5.5 985fc2fcc1 Notifica di compleanno: auguri al festeggiato, avviso agli altri (DD-047)
Ogni giorno, dalle 8:00 (Europe/Rome), un job pg_cron orario manda a chi compie
gli anni «Buon compleanno, <Nome>! 🎂» e a tutti gli altri membri attivi,
allenatori compresi, «Oggi è il compleanno di <Nome>» (con più compleanni lo
stesso giorno un solo messaggio a testa, nomi in ordine alfabetico). Solo
titolo, senza corpo né età. Canali: push, centro notifiche ed email.

- migration m31: tipi `compleanno` e `compleanno_auguri`, registro
  `compleanni_notificati` (una sola volta per destinatario e giorno, anche se la
  notifica viene eliminata), funzione `genera_avvisi_compleanno()` e job
  `avvisi-compleanno`, trigger push esteso ai nuovi tipi. Non ancora applicata
  in produzione.
- 29 febbraio non gestito: chi è nato quel giorno è avvisato solo negli anni
  bisestili (limite noto, documentato).
- test: integrazione `notifiche-compleanno` (ora e ora legale, destinatari,
  più compleanni, registro, coda push/email, 29 febbraio, job pianificato);
  unit sui nuovi tipi e sul corpo vuoto del service worker.
- documentazione: DD-047, catalogo e testi delle notifiche, DATABASE, CHANGELOG,
  ROADMAP, PROJECT_STATE.

Chiude la issue #11.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 10:18:22 +02:00

94 lines
3.5 KiB
TypeScript

/** Esegue il worker reale senza pagina, DOM, rete o stato lasciato dall'app. */
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
const codice = readFileSync(new URL("../../public/push-sw.js", import.meta.url), "utf8");
type Evento = {
data?: { json: () => unknown } | null | undefined;
waitUntil: (p: Promise<unknown>) => void;
};
// `renotify` è supportato da Chromium ma manca dai tipi DOM di TypeScript.
type OpzioniNotifica = NotificationOptions & { renotify?: boolean };
function avviaWorker() {
const listener = new Map<string, (event: Evento) => void>();
const notifiche: Array<{ titolo: string; opzioni: OpzioniNotifica }> = [];
let completa: (() => void) | undefined;
const mostrata = new Promise<void>((resolve) => (completa = resolve));
const installata = Promise.resolve();
const attivata = Promise.resolve();
runInNewContext(codice, {
self: {
addEventListener: (nome: string, callback: (event: Evento) => void) =>
listener.set(nome, callback),
skipWaiting: () => installata,
clients: {
claim: () => attivata,
matchAll: () => assert.fail("la push deve funzionare senza consultare finestre aperte"),
},
registration: {
showNotification(titolo: string, opzioni: OpzioniNotifica) {
notifiche.push({ titolo, opzioni });
return mostrata;
},
},
},
fetch: () => assert.fail("nessuna rete per mostrare la notifica"),
});
return { listener, notifiche, mostrata, completa: completa!, installata, attivata };
}
// L'installazione e l'attivazione tengono vivo il worker fino al completamento.
const avvio = avviaWorker();
for (const [nome, promessa] of [
["install", avvio.installata],
["activate", avvio.attivata],
] as const) {
let attesa: Promise<unknown> | undefined;
avvio.listener.get(nome)!({ waitUntil: (p) => (attesa = p) });
assert.equal(attesa, promessa, `${nome}: la promessa è collegata a waitUntil`);
await attesa;
}
const casi: Array<{ data: Evento["data"]; titolo: string; testo: string }> = [
{
data: { json: () => ({ title: "🏐 Palloni", body: "Sabato tocca a te." }) },
titolo: "🏐 Palloni",
testo: "Sabato tocca a te.",
},
{
// Compleanno (DD-047): solo il titolo. Il corpo vuoto resta vuoto, non diventa il testo di ripiego.
data: { json: () => ({ title: "Buon compleanno, Marco! 🎂", body: "" }) },
titolo: "Buon compleanno, Marco! 🎂",
testo: "",
},
{ data: null, titolo: "CrAPP", testo: "Apri l'app per i dettagli." },
{
data: {
json: () => {
throw new SyntaxError("payload non JSON");
},
},
titolo: "CrAPP",
testo: "Apri l'app per i dettagli.",
},
];
for (const { data, titolo, testo } of casi) {
// Nuovo contesto a ogni consegna: simula il risveglio dopo la terminazione del worker.
const worker = avviaWorker();
let attesa: Promise<unknown> | undefined;
worker.listener.get("push")!({ data, waitUntil: (p) => (attesa = p) });
assert.equal(worker.notifiche.length, 1, "mostra subito una notifica anche senza l'app");
assert.equal(attesa, worker.mostrata, "waitUntil copre tutta showNotification");
const notifica = worker.notifiche[0]!;
assert.equal(notifica.titolo, titolo);
assert.equal(notifica.opzioni.body, testo);
assert.ok(notifica.opzioni.tag, "il tag permette di sostituire la notifica precedente");
assert.equal(notifica.opzioni.renotify, true, "anche una sostituzione avvisa l'utente");
worker.completa();
await attesa;
}
console.log("push-sw: ok");