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>
136 lines
4.5 KiB
TypeScript
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");
|