437 lines
12 KiB
TypeScript
437 lines
12 KiB
TypeScript
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()),
|
|
matchAll: jest.fn(() => Promise.resolve([])),
|
|
},
|
|
}) as unknown as ServiceWorkerGlobalScope
|
|
|
|
let originalWindow: Window & typeof globalThis
|
|
|
|
describe("service worker", () => {
|
|
let self: ServiceWorkerGlobalScope
|
|
let controlledClients: Client[]
|
|
let uncontrolledClients: Client[]
|
|
|
|
beforeEach(() => {
|
|
originalWindow = global.window
|
|
global.window = undefined as any
|
|
indexedDB = new IDBFactory()
|
|
self = createSelf()
|
|
controlledClients = []
|
|
uncontrolledClients = []
|
|
|
|
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" },
|
|
}
|
|
)
|
|
})
|
|
|
|
describe("on notification click", () => {
|
|
test("the notification is closed", 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 if no clients match the destination", 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("focuses first matching client", async () => {
|
|
addClient("https://tackupnow.com/place", "window", true)
|
|
const matchingClient = addClient(
|
|
"https://tackupnow.com/otherplace",
|
|
"window",
|
|
true
|
|
)
|
|
addClient("https://tackupnow.com/yetanotherotherplace", "window", true)
|
|
|
|
initWorker(self)
|
|
|
|
const notificationHandler = getHandler("notificationclick")
|
|
|
|
const event = createNotificationEvent("https://tackupnow.com/otherplace")
|
|
notificationHandler(event)
|
|
await waitUntilCalls(event)
|
|
|
|
expect(matchingClient.focus).toHaveBeenCalled()
|
|
})
|
|
|
|
test("focuses uncontrolled matching clients", async () => {
|
|
addClient("https://derpatious.world", "window", false)
|
|
addClient("https://tackupnow.com/1", "window", false)
|
|
const matchingClient = addClient(
|
|
"https://tackupnow.com/2",
|
|
"window",
|
|
false
|
|
)
|
|
addClient("https://tackupnow.com/controlled", "window", true)
|
|
|
|
initWorker(self)
|
|
|
|
const notificationHandler = getHandler("notificationclick")
|
|
|
|
const event = createNotificationEvent("https://tackupnow.com/2")
|
|
notificationHandler(event)
|
|
await waitUntilCalls(event)
|
|
|
|
expect(matchingClient.focus).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
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 addClient(url: string, type: ClientTypes, controlled: boolean) {
|
|
const matchAllMock = self.clients.matchAll as jest.Mock
|
|
|
|
matchAllMock.mockImplementation(
|
|
(args: { type: string; includeUncontrolled?: boolean }) => {
|
|
const clientList = args.includeUncontrolled
|
|
? [...uncontrolledClients, ...controlledClients]
|
|
: controlledClients
|
|
return clientList.filter((client) => client.type === args.type)
|
|
}
|
|
)
|
|
|
|
const clientList = controlled ? controlledClients : uncontrolledClients
|
|
|
|
const client: WindowClient = {
|
|
url,
|
|
type,
|
|
focus: jest.fn(() => Promise.resolve(client)) as WindowClient["focus"],
|
|
} as WindowClient
|
|
|
|
clientList.push(client)
|
|
|
|
return client
|
|
}
|
|
|
|
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<void, Parameters<ExtendableEvent["waitUntil"]>>
|
|
} {
|
|
return createEvent({
|
|
notification: {
|
|
close: jest.fn(),
|
|
data: {
|
|
destination,
|
|
},
|
|
},
|
|
}) as any as NotificationEvent & {
|
|
waitUntil: jest.Mock<void, Parameters<ExtendableEvent["waitUntil"]>>
|
|
}
|
|
}
|
|
|
|
function createEvent(properties?: object) {
|
|
return {
|
|
waitUntil: jest.fn(),
|
|
...properties,
|
|
}
|
|
}
|
|
|
|
function waitUntilCalls(event: {
|
|
waitUntil: jest.Mock<void, Parameters<ExtendableEvent["waitUntil"]>>
|
|
}) {
|
|
return Promise.all(event.waitUntil.mock.calls.map(([promise]) => promise))
|
|
}
|