Files
CRAPP/test/unit/push-client.test.ts
T
davideandClaude Opus 5 d8f334007b Fa arrivare le push ad app chiusa e rende diagnosticabile quando non arrivano.
Il testo della notifica viaggia ora cifrato dentro la push (aes128gcm, RFC
8188/8291) invece di essere recuperato dal service worker con una fetch al
risveglio. Era quella fetch a non chiudersi in tempo: ad app chiusa il browser
tiene vivo il worker pochi secondi, showNotification non veniva mai chiamata e
non compariva niente, mentre ad app aperta con la rete calda sembrava tutto a
posto. La POST porta anche Urgency: high, che chiede la consegna immediata
invece di far accumulare i messaggi fino al risveglio del dispositivo.

Cadono i pezzi che esistevano solo per rimediare al payload vuoto: la route
push-messaggio, la coda promemoria_push con la sua scadenza a 12 ore,
messaggioPalloniOggi() e il timeout nel worker. Tutti e tre i mittenti avevano
gia il testo pronto prima di inviare.

Il worker si aggiorna da solo all'avvio e a ogni ritorno in primo piano
(mantieniWorkerPushAggiornato): nella webapp installata quello vecchio puo
sopravvivere a lungo, e senza questo un dispositivo resterebbe fermo alla
versione che va a cercare il testo in rete.

Profilo -> Opzioni ha "Mandami una notifica di prova", visibile solo a notifiche
attive: manda una push a questo dispositivo e riporta stato HTTP, corpo della
risposta e se l'endpoint risulta davvero in push_subscriptions. Senza, "non
arriva" era cieco: ogni prova richiedeva un admin, un evento nello stato giusto
e una seconda persona, e la risposta del servizio push veniva buttata via.
inviaPush torna { stato, corpo } e logga il corpo sui rifiuti.

Dalle prove sul campo: a parita di server, iPhone installato da Home riceve ad
app chiusa. Su Android installato come webapp resta da verificare: il WebAPK e
un'app Android a se, con permesso notifiche (Android 13+) e voce batteria
distinti da quelli di Chrome. Annotato nei limiti noti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:24:07 +02:00

136 lines
4.5 KiB
TypeScript

/**
* Check delle notifiche push lato client: `bun test/unit/push-client.test.ts`.
* Fuori dal browser (nessun `navigator.serviceWorker`) tutte le funzioni devono
* riconoscere l'assenza di supporto senza tentare rete o API del DOM.
*/
import assert from "node:assert/strict";
import {
attivaNotifiche,
disattivaNotifiche,
mantieniWorkerPushAggiornato,
notificaDiProva,
pushSupportato,
statoNotifiche,
} from "@/lib/push-client";
// --- pushSupportato: falso in ambiente server (nessun window) -----------------
assert.equal(typeof window, "undefined", "il test gira senza DOM");
assert.equal(pushSupportato(), false);
// --- statoNotifiche: nessun supporto -> nessuna sottoscrizione ----------------
assert.equal(await statoNotifiche(), false);
// --- attivaNotifiche: rifiuta subito, senza chiedere permessi o rete ----------
await assert.rejects(() => attivaNotifiche("g1"), /Notifiche non supportate su questo dispositivo/);
// --- notificaDiProva: rifiuta subito, senza toccare la rete -------------------
await assert.rejects(() => notificaDiProva(), /Notifiche non supportate su questo dispositivo/);
// --- disattivaNotifiche: no-op silenzioso, nessun errore ----------------------
await assert.doesNotReject(() => disattivaNotifiche());
// Il mantenimento del worker è innocuo anche durante SSR.
assert.doesNotThrow(mantieniWorkerPushAggiornato());
const originali = new Map(
["window", "navigator", "document"].map((nome) => [
nome,
Object.getOwnPropertyDescriptor(globalThis, nome),
]),
);
const documento = Object.assign(new EventTarget(), { visibilityState: "visible" });
let letture = 0;
let aggiornamenti = 0;
let presente = true;
let errore = false;
let sblocca: (() => void) | undefined;
let attesa: Promise<void> | undefined;
let interrompi = () => {};
const ciclo = () => new Promise((resolve) => setTimeout(resolve, 0));
const visibilita = async (stato: string) => {
documento.visibilityState = stato;
documento.dispatchEvent(new Event("visibilitychange"));
await ciclo();
};
try {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: { PushManager: {}, Notification: {} },
});
Object.defineProperty(globalThis, "document", { configurable: true, value: documento });
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
serviceWorker: {
async getRegistration(url: string) {
assert.equal(url, "/push-sw.js");
letture++;
return presente
? {
async update() {
aggiornamenti++;
if (errore) throw new Error("offline");
await attesa;
},
}
: undefined;
},
// Un aggiornamento non deve cancellare o sostituire la registrazione.
register() {
assert.fail("non deve registrare un nuovo worker");
},
},
},
});
interrompi = mantieniWorkerPushAggiornato();
await ciclo();
assert.equal(aggiornamenti, 1, "aggiorna il worker esistente all'avvio");
await visibilita("hidden");
assert.equal(letture, 1, "nessun lavoro quando l'app viene chiusa");
await visibilita("visible");
assert.equal(aggiornamenti, 2, "controlla gli aggiornamenti al ritorno nella webapp");
errore = true;
await visibilita("visible");
assert.equal(aggiornamenti, 3, "un errore offline non blocca l'app");
errore = false;
await visibilita("visible");
assert.equal(aggiornamenti, 4, "riprova dopo l'errore");
attesa = new Promise<void>((resolve) => (sblocca = resolve));
await visibilita("visible");
await visibilita("visible");
assert.equal(aggiornamenti, 5, "accorpa gli aggiornamenti sovrapposti");
sblocca!();
await ciclo();
attesa = undefined;
presente = false;
await visibilita("visible");
assert.equal(aggiornamenti, 5, "nessuna nuova iscrizione senza registrazione");
interrompi();
const prima = letture;
await visibilita("visible");
assert.equal(letture, prima, "rimuove il listener allo smontaggio");
documento.visibilityState = "hidden";
presente = true;
interrompi = mantieniWorkerPushAggiornato();
await ciclo();
assert.equal(letture, prima, "non aggiorna se parte già in background");
await visibilita("visible");
assert.equal(aggiornamenti, 6);
} finally {
interrompi();
for (const [nome, originale] of originali) {
if (originale) Object.defineProperty(globalThis, nome, originale);
else Reflect.deleteProperty(globalThis, nome);
}
}
console.log("push-client: ok");