Compare commits

...

38 Commits

Author SHA1 Message Date
Jeff Lieb f78d448a0b Fix env injection
Test / test (push) Successful in 1m4s Details
2025-03-22 16:04:07 -04:00
Jeff 683c685b7d Update public key
Test / test (push) Successful in 1m10s Details
2025-03-22 14:56:07 -04:00
Jeff d5f474768c Remove fallback notification
Test / test (push) Successful in 1m1s Details
2024-10-19 20:30:56 -04:00
Jeff 3a56b9f853 Open notification to existing tab/window 2024-10-19 20:30:37 -04:00
Jeff 37f32cce3a Replace vapid key that accidentally got committed, show notifications in app
Test / test (push) Successful in 1m2s Details
2024-10-19 19:20:51 -04:00
Jeff e9c7d7a8ef Must ignore data-test
Test / test (push) Successful in 1m2s Details
2024-10-19 18:11:15 -04:00
Jeff 75087f2121 Pin postgres to 16
Test / test (push) Successful in 1m14s Details
2024-10-19 17:53:30 -04:00
Jeff a719706dff Fix local test port
Test / test (push) Successful in 59s Details
2024-10-19 17:44:38 -04:00
Jeff be55d46391 Separate ci env from test env
Test / test (push) Successful in 1m3s Details
2024-10-19 17:42:24 -04:00
Jeff 277ee9f2b5 Still confused, try changing port, again
Test / test (push) Successful in 1m2s Details
2024-10-19 17:36:23 -04:00
Jeff d1f20c4a2b Update test database host name
Test / test (push) Successful in 1m2s Details
2024-10-19 17:34:42 -04:00
Jeff b992443a99 Make a separate npm script for ci
Test / test (push) Successful in 1m1s Details
2024-10-19 17:27:00 -04:00
Jeff 8feb9bd37a Setup action doesn't work, try defining as a service I guess
Test / test (push) Failing after 31s Details
2024-10-19 17:25:17 -04:00
Jeff d66c72ce96 Don't set postgres version
Test / test (push) Failing after 15s Details
2024-10-19 17:21:20 -04:00
Jeff e88b953c58 Give up trying to docker in docker, just install postgres
Test / test (push) Failing after 15s Details
2024-10-19 17:18:44 -04:00
Jeff fdbf8c5ca5 Why the heck is docker ps showing containers on the host???
Test / test (push) Successful in 1m37s Details
2024-10-19 14:22:29 -04:00
Jeff 2031e8a3e7 Verify the problem is that the service hasn't started before tests execute
Test / test (push) Successful in 1m31s Details
2024-10-19 14:03:10 -04:00
Jeff 06e82c4f3f Run docker ps before migrating
Test / test (push) Successful in 1m24s Details
2024-10-19 13:56:58 -04:00
Jeff 5c230f8e5c Try different port? Not sure why the database connection is being refused
Test / test (push) Successful in 1m23s Details
2024-10-19 13:53:31 -04:00
Jeff 8aad5028ab Remove orphans
Test / test (push) Successful in 1m37s Details
2024-10-19 13:49:20 -04:00
Jeff 6f6a1bc2dc Try migrating again
Test / test (push) Successful in 1m22s Details
2024-10-19 13:38:56 -04:00
Jeff 6334b63631 Try reset again
Test / test (push) Successful in 1m23s Details
2024-10-19 13:36:36 -04:00
Jeff 26344fbdb2 Update playwright test version
Test / test (push) Successful in 1m25s Details
2024-10-19 13:34:04 -04:00
Jeff d2860dc454 Check sanity more closely
Test / test (push) Failing after 1m49s Details
2024-10-19 13:29:07 -04:00
Jeff cfd6a40859 Try logging something during tests
Test / test (push) Has been cancelled Details
2024-10-19 13:25:07 -04:00
Jeff 206ce42655 Destroy old db instance during subscription test when resetting db instance
Test / test (push) Has been cancelled Details
2024-10-19 13:22:27 -04:00
Jeff 20e7099317 Try re-enabling subscription test
Test / test (push) Has been cancelled Details
2024-10-19 13:16:10 -04:00
Jeff cbdaea7f75 Try a less suck action
Test / test (push) Successful in 1m54s Details
2024-10-19 13:12:27 -04:00
Jeff 384a65f2dc Yo dawg, I heard you like docker, so I put a docker in your docker
Test / test (push) Failing after 27s Details
2024-10-19 13:08:28 -04:00
Jeff 28bfe4c755 Try using the image again
Test / test (push) Failing after 1m34s Details
2024-10-19 12:54:13 -04:00
Jeff dfb20a1cbe Try usig playwright docker image
Test / test (push) Has been cancelled Details
2024-10-19 12:44:19 -04:00
Jeff 16530e0505 Install playwright deps
Test / test (push) Failing after 3m39s Details
2024-10-19 12:29:44 -04:00
Jeff 71d5706320 Run all playwright tests
Test / test (push) Failing after 2m11s Details
2024-10-19 12:25:40 -04:00
Jeff d7ef727558 See if skipping subscription tests succeeds
Test / test (push) Failing after 1m14s Details
2024-10-19 12:23:09 -04:00
Jeff 45ec236839 Try increasing node head space
Test / test (push) Failing after 1m43s Details
2024-10-19 12:18:39 -04:00
Jeff a475f7f4bf Remove some console logs
Test / test (push) Failing after 1m49s Details
2024-10-19 12:11:54 -04:00
Jeff c0821776c1 Remove db port mapping
Test / test (push) Failing after 1m51s Details
2024-10-18 11:42:42 -04:00
Jeff 34372bdb89 Fix jest not exiting, post subscription to worker to send to the server
Test / test (push) Has been cancelled Details
2024-10-18 11:38:41 -04:00
21 changed files with 493 additions and 167 deletions

View File

@ -1 +1,2 @@
data/
data/
data-test/

6
.env.ci Normal file
View File

@ -0,0 +1,6 @@
DATABASE="postgres"
POSTGRES_PASSWORD="testpassword"
POSTGRES_HOST="database"
POSTGRES_PORT=5432
VAPID_PRIVATE_KEY="privatekey"
BASE_URL="http://localhost:5173"

View File

@ -1,4 +1,6 @@
DATABASE="postgres"
POSTGRES_PASSWORD="testpassword"
POSTGRES_HOST="localhost"
POSTGRES_PORT=5434
VAPID_PRIVATE_KEY="privatekey"
BASE_URL="http://localhost:5173"

View File

@ -5,6 +5,27 @@ on: [push]
jobs:
test:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.48.1-jammy
services:
database:
image: postgres:16
env:
POSTGRES_PASSWORD: testpassword
POSTGRES_USER: postgres
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5434:5432
steps:
- uses: actions/checkout@v4
- run: npm install && npm run test && npx playwright install && npx playwright test
- name: Run Tests
env: NODE_OPTIONS=--max-old-space-size=8192
run: |
npm install
npm run ci
npm ci
npx playwright test

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ build/
/playwright-report/
/blob-report/
/playwright/.cache/
send-it.js

View File

@ -16,6 +16,13 @@ const dialect = new PostgresDialect({
}),
})
export const db = new Kysely<Database>({
export let db = new Kysely<Database>({
dialect,
})
export async function resetDbInstance() {
await db.destroy()
db = new Kysely<Database>({
dialect,
})
}

View File

@ -1,7 +1,7 @@
export default {
database: process.env.DATABASE ?? "postgres",
host: "database",
host: process.env.POSTGRES_HOST ?? "database",
user: "postgres",
password: process.env.POSTGRES_PASSWORD,
port: 5432,
port: process.env.POSTGRES_PORT ?? 5432,
}

View File

@ -1,4 +1,4 @@
import { db } from "./database"
import { db, resetDbInstance } from "./database"
import migrateToLatest, { resetAll } from "./migrate"
import {
createSubscription,
@ -26,17 +26,29 @@ const pushSubscription2 = {
}
jest.mock("./settings", () => ({
port: 5434,
port: process.env.POSTGRES_PORT,
user: "postgres",
password: process.env.POSTGRES_PASSWORD,
database: "test",
host: "localhost",
host: process.env.POSTGRES_HOST,
}))
describe("subscriptions", () => {
beforeEach(migrateToLatest)
beforeAll(async () => {
await resetDbInstance()
})
afterEach(resetAll)
beforeEach(async () => {
await migrateToLatest()
})
afterEach(async () => {
await resetAll()
})
afterAll(async () => {
await db.destroy()
})
test("createSubscription", async () => {
const { id } = await createSubscription(pushSubscription1)

View File

@ -31,7 +31,18 @@ function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
useEffect(() => {
if (!canSendPush) return
subscribeToPush(urlB64ToUint8Array(pushPublicKey) as any, onSubscribe)
subscribeToPush(
urlB64ToUint8Array(pushPublicKey) as any,
async (subscription) => {
await navigator.serviceWorker.ready.then((registration) => {
registration.active?.postMessage({
type: "subscribed",
subscription: subscription.toJSON(),
})
})
onSubscribe()
}
)
}, [canSendPush])
return <button onClick={subscribe}>Enable Notifications</button>

View File

@ -3,64 +3,167 @@ 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({ subscription: "HI" })),
getSubscription: jest.fn(() => Promise.resolve({ subscription: "HI" })),
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()
// fetchMock.mockIf(/http:\/\/localhost\/api\/subscribe\/?/, (req) => {
// return Promise.resolve(
// req.method === "POST"
// ? {
// body: JSON.stringify({ subscriptionId: 123 }),
// }
// : req.method === "PUT"
// ? { init: { status: 200 } }
// : { init: { status: 405 } }
// )
// })
controlledClients = []
uncontrolledClients = []
fetchMock.mockResponse((req) => {
return Promise.resolve(
if (
req.url.replace(/https?:\/\/[a-zA-Z0-9\.]*/, "") ===
"/api/subscription/" &&
req.method === "POST"
? {
body: JSON.stringify({ subscriptionId: 123 }),
}
: req.method === "PUT"
? { init: { status: 200 } }
: { init: { status: 405 } }
)
) {
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
})
xtest("displays push notifications", () => {
test("displays push notifications", async () => {
initWorker(self)
const pushHandler = getHandler("push")
const pushEvent = createEvent({ data: "HI" })
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 () => {
@ -87,64 +190,175 @@ describe("service worker", () => {
expect(self.skipWaiting).toHaveBeenCalled()
})
test("puts push subscription if the subscription id is set", async () => {
initWorker(self)
await setSavedSubscription(5000)
describe("on subscription message", () => {
test("puts push subscription if the subscription id is set", async () => {
initWorker(self)
await setSavedSubscription(5000)
const changeHandler = getHandler("pushsubscriptionchange")
const messageHandler = getHandler("message")
const changeEvent = createEvent()
changeHandler(changeEvent)
await waitUntilCalls(changeEvent)
const messageEvent = createEvent({
data: {
subscription: { subscription: "YO" },
type: "subscribed",
},
})
messageHandler(messageEvent)
await waitUntilCalls(messageEvent)
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: "YO" }),
headers: {
"Content-Type": "application/json",
},
}
)
})
expect(fetch).toHaveBeenCalledWith(
`${process.env.BASE_URL}/api/subscription/5000`,
{
method: "PUT",
body: JSON.stringify({ subscription: "HI" }),
}
)
})
test("posts to subscriptions when there is no user set", async () => {
initWorker(self)
test("posts to subscriptions when there is no user set", async () => {
initWorker(self)
const messageHandler = getHandler("message")
const changeHandler = getHandler("pushsubscriptionchange")
const messageEvent = createEvent({
data: {
subscription: { subscription: "YO" },
type: "subscribed",
},
})
messageHandler(messageEvent)
await waitUntilCalls(messageEvent)
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: "YO" }),
headers: {
"Content-Type": "application/json",
},
}
)
})
expect(fetch).toHaveBeenCalledWith(
`${process.env.BASE_URL}/api/subscription/`,
{
method: "POST",
body: JSON.stringify({ subscription: "HI" }),
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)
}
)
})
test("subscription id is saved on first subscription change", async () => {
initWorker(self)
const clientList = controlled ? controlledClients : uncontrolledClients
const changeHandler = getHandler("pushsubscriptionchange")
const client: WindowClient = {
url,
type,
focus: jest.fn(() => Promise.resolve(client)) as WindowClient["focus"],
} as WindowClient
const changeEvent = createEvent()
changeHandler(changeEvent)
await waitUntilCalls(changeEvent)
clientList.push(client)
expect(await getSavedSubscription()).toEqual({ id: 1, subscriptionId: 123 })
})
return client
}
function getHandler(eventName: string) {
expect(self.addEventListener).toHaveBeenCalledWith(
@ -193,8 +407,19 @@ function createPushEvent(data: string) {
return createEvent(pushFields)
}
function createMessageEvent(message: any) {
return createEvent({ data: message })
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) {

View File

@ -16,65 +16,88 @@ export default function start(self: ServiceWorkerGlobalScope) {
event.waitUntil(self.clients.claim())
})
// self.addEventListener("push", function (event: PushEvent) {
// console.log("[Service Worker] Push Received.")
// console.log(`[Service Worker] Push had this data: "${event.data?.text()}"`)
self.addEventListener("push", function (event: PushEvent) {
const { title, body, badge, icon, destination } = event.data?.json()
// const title = "Push Codelab"
// const options = {
// body: "Yay it works.",
// icon: "images/icon.png",
// badge: "images/badge.png",
// }
event.waitUntil(
self.registration.showNotification(title, {
body,
badge,
icon,
data: { destination },
})
)
})
// event.waitUntil(self.registration.showNotification(title, options))
// })
self.addEventListener("notificationclick", function (event) {
event.notification.close()
const destination = event.notification.data.destination
// self.addEventListener("notificationclick", function (event) {
// console.log("[Service Worker] Notification click Received.")
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
})
// event.notification.close()
const existingClient = clients.find(
(client) => client.url === event.notification.data.destination
)
// event.waitUntil(
// self.clients.openWindow("https://developers.google.com/web/")
// )
// })
if (existingClient === undefined) {
await self.clients.openWindow(destination)
} else {
await existingClient.focus()
}
})()
)
})
self.addEventListener("message", function (event) {
const waitEvent = event as ExtendableEvent
if ("type" in event.data && event.data.type === "subscribed") {
waitEvent.waitUntil(submitSubscription(event.data.subscription))
}
})
self.addEventListener("pushsubscriptionchange", function (event) {
const waitEvent = event as ExtendableEvent
waitEvent.waitUntil(submitSubscription(self.registration))
waitEvent.waitUntil(
(async () => {
const applicationServerKey = urlB64ToUint8Array(pushPublicKey)
const newSubscription = await self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
})
self.registration.pushManager.getSubscription()
await submitSubscription(newSubscription.toJSON())
})()
)
})
}
async function submitSubscription(registration: ServiceWorkerRegistration) {
async function submitSubscription(subscription: PushSubscriptionJSON) {
const db = database()
const subscription = await registration.pushManager.getSubscription()
if (subscription === null) return
const existingSubscriptionId = await db.subscriptions.get(1)
const applicationServerKey = urlB64ToUint8Array(pushPublicKey)
const newSubscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
})
await (existingSubscriptionId === undefined
? postSubscription(newSubscription, db)
: putSubscription(newSubscription, existingSubscriptionId.subscriptionId))
? postSubscription(subscription, db)
: putSubscription(subscription, existingSubscriptionId.subscriptionId))
}
async function postSubscription(
subscription: PushSubscription,
subscription: PushSubscriptionJSON,
db: ReturnType<typeof database>
) {
const response = await fetch(`${process.env.BASE_URL}/api/subscription/`, {
method: "POST",
body: JSON.stringify(subscription),
headers: {
"Content-Type": "application/json",
},
})
db.subscriptions.put({
@ -83,10 +106,13 @@ async function postSubscription(
})
}
function putSubscription(subscription: PushSubscription, id: number) {
function putSubscription(subscription: PushSubscriptionJSON, id: number) {
return fetch(`${process.env.BASE_URL}/api/subscription/${id}`, {
method: "PUT",
body: JSON.stringify(subscription),
headers: {
"Content-Type": "application/json",
},
})
}

View File

@ -2,7 +2,7 @@ export default function urlB64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/")
const rawData = window.atob(base64)
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {

View File

@ -1,6 +1,6 @@
services:
database:
image: postgres
image: postgres:16
ports:
- "5434:5432"
environment:

View File

@ -3,10 +3,9 @@ services:
build: .
ports:
- "9000:3000"
env_file: ".env"
database:
image: postgres
ports:
- "5432:5432"
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres

View File

@ -1,5 +1,6 @@
import { test, expect, Page } from "@playwright/test"
import matchPath from "./urlMatcher"
import { messageSW } from "@remix-pwa/sw"
const { describe, beforeEach, skip, use } = test
@ -89,6 +90,23 @@ describe("when user is on iOS", () => {
await expect(yourNotificationsHeading).toBeVisible()
})
test("the subscription is submitted", async ({ page }) => {
await page.goto("/")
await page.getByText(/Enable Notifications/).click()
const postedMessages = await page.evaluate(() => {
return (window as any).postedMessages
})
expect(postedMessages).toContainEqual({
type: "subscribed",
subscription: {
subscription: "HI",
},
})
})
})
})
@ -212,20 +230,37 @@ function stubNotifications(
function stubServiceWorker(page: Page) {
return page.addInitScript(() => {
const serviceWorker = {}
function createSubscription(testToken = "HI") {
return {
toJSON: () => ({
subscription: testToken,
}),
}
}
let postedMessages: any[] = []
const serviceWorker = {
postMessage: (message: any) => postedMessages.push(message),
}
const registration = {
pushManager: {
getSubscription() {
return Promise.resolve({ hi: "subscription" })
return Promise.resolve(createSubscription())
},
subscribe(args: Parameters<PushManager["subscribe"]>[0]) {
return Promise.resolve({ hi: "subscription" })
return Promise.resolve(createSubscription())
},
},
active: serviceWorker,
}
Object.defineProperty(window, "postedMessages", {
value: postedMessages,
writable: true,
})
Object.defineProperty(navigator, "serviceWorker", {
value: {
ready: Promise.resolve(registration),

30
package-lock.json generated
View File

@ -31,7 +31,7 @@
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.48.1",
"@remix-pwa/dev": "^3.1.0",
"@remix-run/dev": "^2.9.0",
"@remix-run/testing": "^2.9.1",
@ -3324,18 +3324,18 @@
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"version": "1.48.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
"dev": true,
"dependencies": {
"playwright": "1.44.1"
"playwright": "1.48.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/@remix-pwa/dev": {
@ -13843,33 +13843,33 @@
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"version": "1.48.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
"dev": true,
"dependencies": {
"playwright-core": "1.44.1"
"playwright-core": "1.48.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"version": "1.48.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {

View File

@ -9,8 +9,9 @@
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"watch": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d && jest --watch --config=jest.config.ts",
"test": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d && jest --config=jest.config.ts"
"watch": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d --remove-orphans && jest --watch --config=jest.config.ts",
"test": "export $(cat .env.test | xargs) && docker compose -f docker-compose-test.yaml up database -d --remove-orphans && sleep 5 && jest --config=jest.config.ts",
"ci": "export $(cat .env.ci | xargs) && jest --config=jest.config.ts"
},
"dependencies": {
"@remix-pwa/sw": "^3.0.9",
@ -38,7 +39,7 @@
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.48.1",
"@remix-pwa/dev": "^3.1.0",
"@remix-run/dev": "^2.9.0",
"@remix-run/testing": "^2.9.1",

View File

@ -1 +1 @@
export default "BKc02U2-z7PkTbYgYLlxELWqzTVE631fs4IPuMLbxY_rxdo9VaduthqwkPOiblwjETl99uXes2Nc9EtPbS5x4uA"
export default "BNwr5jYXxBwSRGooQWx6nzsrG5XejgAskVqowkc_O0rutL9PgRIkQAnDh4nz1yoPQEw40juMuqL2NEHSaD3XcxA"

View File

@ -1,22 +0,0 @@
import webPush from "web-push"
const subscription = {
endpoint:
"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABm9FlbY5vMro60hF2iJF3UUBzweuFcg5NRSHXPSpHfUpjo5jKGVRnUxR4ekg0-FsvdQCP89cu__IFd06Tu2TJZ649YyqivRUBnAav0DgOLHGx5-t943QLS-wLvBqyJRCuuLlM1bLz6S9ph9AWJ8CG7rQuTabsHvw--s_w2KDQo3GcQXIM",
expirationTime: null,
keys: {
auth: "dDUqtFo26ekGEAmNzvmJAw",
p256dh:
"BHIT3J6xSRiHfz0m-QRHagDThiOZGVIANtPzOasrOBYG0s_yUnshTVharX5dZcq8GA5OkyMm3mqmA7_o_lFR4WE",
},
}
webPush.setVapidDetails(
"https://tackupnow.com",
"BKc02U2-z7PkTbYgYLlxELWqzTVE631fs4IPuMLbxY_rxdo9VaduthqwkPOiblwjETl99uXes2Nc9EtPbS5x4uA",
"Xmv5Pc4mqr138V3sCxXq7UmsbL5UgSOY43UuJ50nxPw"
)
webPush
.sendNotification(subscription, "Hi what's up?", {})
.then((x) => console.log("It sent", x))
.catch((error) => console.log("It did not send", error))

View File

@ -1,7 +1,4 @@
#!/bin/bash
set -a
source ./.env
set +a
npx tsx api/data/upgrade.ts
npx tsx server.ts
npx tsx server.ts

View File

@ -10,6 +10,10 @@ export default defineConfig({
plugins: [
remix({ ignoredRouteFiles: ["**/*.test.*"] }),
tsconfigPaths(),
remixPWA(),
remixPWA({
buildVariables: {
"process.env.BASE_URL": process.env.BASE_URL ?? "http://localhost:5173",
},
}),
],
})