import "fake-indexeddb/auto" import "core-js/stable/structured-clone" import initWorker from "./worker" import Dexie, { EntityTable } from "dexie" function createSubscription(testToken = "HI") { return { toJSON: () => ({ subscription: testToken, }), } } const createSelf = () => ({ addEventListener: jest.fn(), skipWaiting: jest.fn(() => Promise.resolve()), registration: { pushManager: { subscribe: jest.fn(() => Promise.resolve(createSubscription())), getSubscription: jest.fn(() => Promise.resolve(createSubscription())), }, showNotification: jest.fn(), }, clients: { claim: jest.fn(() => Promise.resolve()), openWindow: jest.fn(() => Promise.resolve()), }, }) as unknown as ServiceWorkerGlobalScope let originalWindow: Window & typeof globalThis describe("service worker", () => { let self: ServiceWorkerGlobalScope beforeEach(() => { originalWindow = global.window global.window = undefined as any indexedDB = new IDBFactory() self = createSelf() fetchMock.mockResponse((req) => { if ( req.url.replace(/https?:\/\/[a-zA-Z0-9\.]*/, "") === "/api/subscription/" && req.method === "POST" ) { return Promise.resolve({ body: JSON.stringify({ subscriptionId: 123 }), }) } else if ( req.url .replace(/https?:\/\/[a-zA-Z0-9\.]*/, "") .startsWith("/api/subscription/") && req.method === "PUT" ) { return Promise.resolve({ init: { status: 200 } }) } return Promise.resolve({ init: { status: 500 } }) }) }) afterEach(() => { jest.clearAllMocks() global.window = originalWindow }) test("displays push notifications", async () => { initWorker(self) const pushHandler = getHandler("push") const pushEvent = createPushEvent( JSON.stringify({ title: "Test title", body: "Test text", icon: "Test icon", badge: "Test badge", destination: "tackupnow.com/hi", }) ) pushHandler(pushEvent) await waitUntilCalls(pushEvent) expect(self.registration.showNotification).toHaveBeenCalledWith( "Test title", { body: "Test text", icon: "Test icon", badge: "Test badge", data: { destination: "tackupnow.com/hi" }, } ) }) test("closes notification on click", async () => { initWorker(self) const notificationHandler = getHandler("notificationclick") const event = createNotificationEvent("tackupnow.com") notificationHandler(event) await waitUntilCalls(event) expect(event.notification.close).toHaveBeenCalled() }) test("opens tack up now on click", async () => { initWorker(self) const notificationHandler = getHandler("notificationclick") const event = createNotificationEvent("tackupnow.com") notificationHandler(event) await waitUntilCalls(event) expect(self.clients.openWindow).toHaveBeenCalledWith("tackupnow.com") }) test("claims on activate", async () => { initWorker(self) const activateHandler = getHandler("activate") const event = createEvent() activateHandler(event) await waitUntilCalls(event) expect(self.clients.claim).toHaveBeenCalled() }) test("skips waiting on install", async () => { initWorker(self) const installHandler = getHandler("install") const event = createEvent() installHandler(event) await waitUntilCalls(event) expect(self.skipWaiting).toHaveBeenCalled() }) describe("on subscription message", () => { test("puts push subscription if the subscription id is set", async () => { initWorker(self) await setSavedSubscription(5000) const messageHandler = getHandler("message") const messageEvent = createEvent({ data: { subscription: { subscription: "YO" }, type: "subscribed", }, }) messageHandler(messageEvent) await waitUntilCalls(messageEvent) expect(fetch).toHaveBeenCalledWith( `${process.env.BASE_URL}/api/subscription/5000`, { method: "PUT", body: JSON.stringify({ subscription: "YO" }), headers: { "Content-Type": "application/json", }, } ) }) test("posts to subscriptions when there is no user set", async () => { initWorker(self) const messageHandler = getHandler("message") const messageEvent = createEvent({ data: { subscription: { subscription: "YO" }, type: "subscribed", }, }) messageHandler(messageEvent) await waitUntilCalls(messageEvent) expect(fetch).toHaveBeenCalledWith( `${process.env.BASE_URL}/api/subscription/`, { method: "POST", body: JSON.stringify({ subscription: "YO" }), headers: { "Content-Type": "application/json", }, } ) }) test("subscription id is saved on first subscription change", async () => { initWorker(self) const messageHandler = getHandler("message") const messageEvent = createEvent({ data: { subscription: { subscription: "YO" }, type: "subscribed", }, }) messageHandler(messageEvent) await waitUntilCalls(messageEvent) expect(await getSavedSubscription()).toEqual({ id: 1, subscriptionId: 123, }) }) }) describe("on subscription change", () => { test("puts push subscription if the subscription id is set", async () => { initWorker(self) await setSavedSubscription(5000) const changeHandler = getHandler("pushsubscriptionchange") const changeEvent = createEvent() changeHandler(changeEvent) await waitUntilCalls(changeEvent) expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({ userVisibleOnly: true, applicationServerKey: expect.any(Uint8Array), }) expect(fetch).toHaveBeenCalledWith( `${process.env.BASE_URL}/api/subscription/5000`, { method: "PUT", body: JSON.stringify({ subscription: "HI" }), headers: { "Content-Type": "application/json", }, } ) }) test("posts to subscriptions when there is no user set", async () => { initWorker(self) const changeHandler = getHandler("pushsubscriptionchange") const changeEvent = createEvent() changeHandler(changeEvent) await waitUntilCalls(changeEvent) expect(self.registration.pushManager.subscribe).toHaveBeenCalledWith({ userVisibleOnly: true, applicationServerKey: expect.any(Uint8Array), }) expect(fetch).toHaveBeenCalledWith( `${process.env.BASE_URL}/api/subscription/`, { method: "POST", body: JSON.stringify({ subscription: "HI" }), headers: { "Content-Type": "application/json", }, } ) }) test("subscription id is saved on first subscription change", async () => { initWorker(self) const changeHandler = getHandler("pushsubscriptionchange") const changeEvent = createEvent() changeHandler(changeEvent) await waitUntilCalls(changeEvent) expect(await getSavedSubscription()).toEqual({ id: 1, subscriptionId: 123, }) }) }) function getHandler(eventName: string) { expect(self.addEventListener).toHaveBeenCalledWith( eventName, expect.anything() ) const [, handler] = (self.addEventListener as jest.Mock).mock.calls.find( ([event]: [string]) => event === eventName ) return handler } }) async function getSavedSubscription() { const database = new Dexie("tack-up-now", { indexedDB }) as Dexie & { subscriptions: EntityTable<{ id: number; subscriptionId: number }, "id"> } database.version(1).stores({ subscriptions: "id++, subscriptionId&" }) return (await database.subscriptions.toArray())[0] } async function setSavedSubscription(subscriptionId: number) { const database = new Dexie("tack-up-now", { indexedDB }) as Dexie & { subscriptions: EntityTable<{ id: number; subscriptionId: number }, "id"> } database.version(1).stores({ subscriptions: "id++, subscriptionId&" }) await database.subscriptions.put({ id: 1, subscriptionId }) } function createPushEvent(data: string) { const pushFields = { data: { blob: () => new Blob([data], { type: "text/plain" }), json: () => JSON.parse(data), text: () => data, arrayBuffer: () => new TextEncoder().encode(data), }, } return createEvent(pushFields) } function createNotificationEvent(destination: string): NotificationEvent & { waitUntil: jest.Mock> } { return createEvent({ notification: { close: jest.fn(), data: { destination, }, }, }) as any as NotificationEvent & { waitUntil: jest.Mock> } } function createEvent(properties?: object) { return { waitUntil: jest.fn(), ...properties, } } function waitUntilCalls(event: { waitUntil: jest.Mock> }) { return Promise.all(event.waitUntil.mock.calls.map(([promise]) => promise)) }