157 lines
3.6 KiB
JavaScript
157 lines
3.6 KiB
JavaScript
import assert from "node:assert/strict"
|
|
import {after, before, beforeEach, test} from "node:test"
|
|
|
|
const originalSelf = globalThis.self
|
|
const listeners = new Map()
|
|
const shownNotifications = []
|
|
|
|
let windowClients = []
|
|
let openedWindow = null
|
|
|
|
before(async () => {
|
|
globalThis.self = {
|
|
addEventListener(type, handler) {
|
|
listeners.set(type, handler)
|
|
},
|
|
location: {origin: "https://whoneedhelp.com"},
|
|
registration: {
|
|
async showNotification(title, options) {
|
|
shownNotifications.push({title, options})
|
|
}
|
|
},
|
|
clients: {
|
|
async matchAll(options) {
|
|
assert.deepEqual(options, {type: "window", includeUncontrolled: true})
|
|
return windowClients
|
|
},
|
|
async openWindow(url) {
|
|
openedWindow = url
|
|
return {url}
|
|
}
|
|
}
|
|
}
|
|
|
|
await import("../../priv/static/sw.js")
|
|
})
|
|
|
|
beforeEach(() => {
|
|
shownNotifications.length = 0
|
|
windowClients = []
|
|
openedWindow = null
|
|
})
|
|
|
|
after(() => {
|
|
globalThis.self = originalSelf
|
|
})
|
|
|
|
test("push keeps a safe same-origin path in the operating-system notification", async () => {
|
|
const event = pushEvent({
|
|
title: "Request accepted",
|
|
body: "Open the private request chat.",
|
|
path: "/requests/7f84fc06-0fae-4e9d-b266-041ca87678d1?section=chat",
|
|
tag: "request-update"
|
|
})
|
|
|
|
listeners.get("push")(event)
|
|
await event.completion
|
|
|
|
assert.deepEqual(shownNotifications, [
|
|
{
|
|
title: "Request accepted",
|
|
options: {
|
|
body: "Open the private request chat.",
|
|
icon: "/images/pwa-192.png",
|
|
badge: "/images/favicon-48.png",
|
|
tag: "request-update",
|
|
data: {
|
|
path: "/requests/7f84fc06-0fae-4e9d-b266-041ca87678d1?section=chat"
|
|
}
|
|
}
|
|
}
|
|
])
|
|
})
|
|
|
|
test("push rejects an external path instead of leaving the application origin", async () => {
|
|
const event = pushEvent({
|
|
title: "Unsafe target",
|
|
path: "//attacker.example/redirect"
|
|
})
|
|
|
|
listeners.get("push")(event)
|
|
await event.completion
|
|
|
|
assert.equal(shownNotifications[0].options.data.path, "/notifications")
|
|
})
|
|
|
|
test("notification click navigates and focuses an existing application window", async () => {
|
|
const navigation = []
|
|
let focused = false
|
|
|
|
windowClients = [
|
|
{
|
|
url: "https://whoneedhelp.com/notifications?section=settings",
|
|
async navigate(url) {
|
|
navigation.push(url)
|
|
},
|
|
async focus() {
|
|
focused = true
|
|
}
|
|
},
|
|
{url: "https://example.com/"}
|
|
]
|
|
|
|
const event = clickEvent("/requests/request-id")
|
|
listeners.get("notificationclick")(event)
|
|
await event.completion
|
|
|
|
assert.equal(event.closed, true)
|
|
assert.deepEqual(navigation, ["https://whoneedhelp.com/requests/request-id"])
|
|
assert.equal(focused, true)
|
|
assert.equal(openedWindow, null)
|
|
})
|
|
|
|
test("notification click opens the application when no application window exists", async () => {
|
|
windowClients = [{url: "https://example.com/"}]
|
|
|
|
const event = clickEvent("/notifications")
|
|
listeners.get("notificationclick")(event)
|
|
await event.completion
|
|
|
|
assert.equal(event.closed, true)
|
|
assert.equal(openedWindow, "https://whoneedhelp.com/notifications")
|
|
})
|
|
|
|
function pushEvent(payload) {
|
|
return waitableEvent({
|
|
data: {
|
|
json() {
|
|
return payload
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function clickEvent(path) {
|
|
const event = waitableEvent({
|
|
closed: false,
|
|
notification: {
|
|
data: {path},
|
|
close() {
|
|
event.closed = true
|
|
}
|
|
}
|
|
})
|
|
|
|
return event
|
|
}
|
|
|
|
function waitableEvent(properties) {
|
|
return {
|
|
...properties,
|
|
completion: Promise.resolve(),
|
|
waitUntil(completion) {
|
|
this.completion = completion
|
|
}
|
|
}
|
|
}
|