Files
template-app/e2e/notifications.spec.ts
2026-11-14 09:56:45 +00:00

80 lines
4.8 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Real Postgres and the app worker's invitation relay; provider services remain simulated.
import { expect, test } from "@playwright/test"
import { call, callRaw, makeOrganization, signIn, unique, violations } from "./support/app"
type Inbox = { items: Array<{ id: string; code: string; readAt: string | null }>; unreadCount: number }
test("invitations reach only their recipients and the shared inbox marks them read", async ({
page,
browser,
}, testInfo) => {
await page.setViewportSize({ width: 1280, height: 900 })
await signIn(page, `${unique("inbox-owner")}@example.test`)
const { org } = await makeOrganization(page)
const email = `${unique("inbox-viewer")}@example.test`
const invitation = await call<{ token: string }>(page, "tenancy.member.invite", { org, email, role: "viewer" })
const context = await browser.newContext({ baseURL: new URL(page.url()).origin })
try {
const member = await context.newPage()
await signIn(member, email)
await call(member, "tenancy.invitation.accept", { org, token: invitation.token })
await expect
.poll(async () => (await call<Inbox>(page, "notifications.inbox.list", { org })).items.map((item) => item.code))
.toContain("invitation_accepted")
await expect
.poll(async () => (await call<Inbox>(member, "notifications.inbox.list", { org })).items.map((item) => item.code))
.toContain("member_joined")
const ownerInbox = await call<Inbox>(page, "notifications.inbox.list", { org })
const accepted = ownerInbox.items.find((item) => item.code === "invitation_accepted")
if (!accepted) throw new Error("the invitation relay did not deliver its notification")
const csrf = await member.locator('meta[name="csrf-token"]').getAttribute("content")
const denial = await callRaw(member.request, csrf ?? "", "notifications.inbox.markRead", { org, id: accepted.id })
expect(denial.status()).toBe(404)
expect(
(await call<Inbox>(page, "notifications.inbox.list", { org })).items.find((item) => item.id === accepted.id)
?.readAt,
).toBeNull()
await page.goto(`/${org}`)
await page.locator('[data-guide="frame.notifications"]').click()
const panel = page.locator('[data-slot="side-panel"]')
await expect(panel.getByText("An invitation was accepted", { exact: true })).toBeVisible()
await panel.getByRole("button", { name: "Mark as read: An invitation was accepted", exact: true }).click()
await expect
.poll(
async () =>
(await call<Inbox>(page, "notifications.inbox.list", { org })).items.find((item) => item.id === accepted.id)
?.readAt,
)
.not.toBeNull()
const bar = await page.locator('[data-slot="frame-bar"]').boundingBox()
const head = await panel.locator(".kit-side-panel-head").boundingBox()
expect(head?.y ?? -1).toBeGreaterThanOrEqual((bar?.y ?? 0) + (bar?.height ?? 0))
await page.getByRole("button", { name: "Reject analytics and replay", exact: true }).click({ trial: true })
expect(await violations(page)).toEqual([])
await page.screenshot({ path: testInfo.outputPath("notification-invitation-desktop.png") })
await panel.getByRole("button", { name: "Close", exact: true }).click()
await expect(page.locator('[data-guide="frame.notifications"]')).toBeFocused()
await page.setViewportSize({ width: 768, height: 900 })
await page.locator('[data-guide="frame.notifications"]').click()
const overlay = page.locator('[data-slot="side-panel"]')
// The entrance animation must not make the document wider at the overlay breakpoint.
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
const overlayHead = await overlay.locator(".kit-side-panel-head").boundingBox()
const overlayBar = await page.locator('[data-slot="frame-bar"]').boundingBox()
expect(overlayHead?.y ?? -1).toBeGreaterThanOrEqual((overlayBar?.y ?? 0) + (overlayBar?.height ?? 0))
await page.getByRole("button", { name: "Reject analytics and replay", exact: true }).click({ trial: true })
await page.screenshot({ path: testInfo.outputPath("notification-invitation-tablet.png") })
await overlay.getByRole("button", { name: "Close", exact: true }).click()
await page.setViewportSize({ width: 360, height: 800 })
await page.locator('[data-guide="frame.notifications"]').click()
const sheet = page.getByRole("dialog")
await expect(sheet.getByRole("button", { name: "Close", exact: true })).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.screenshot({ path: testInfo.outputPath("notification-invitation-mobile.png") })
await sheet.getByRole("button", { name: "Close", exact: true }).click()
} finally {
await context.close()
}
})