// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The account page in a browser (joint contract ยง12): a person exports their data -- every installed // module's section in one file -- then deletes the account with the keyboard, and is signed out on // every device; the browser modules' stored state is gone, and the same address signs up again as // a new person with nothing. axe on each state the page shows. import { readFileSync } from "node:fs" import { expect, test } from "@playwright/test" import { callRaw, makeOrganization, openPersonMenu, signIn, unique, violations } from "./support/app" test("a person exports their data, then deletes the account and is signed out everywhere", async ({ page, browser, }) => { const email = `${unique("leaver")}@example.test` await signIn(page, email) const { org } = await makeOrganization(page, "leaver") // A second device of the same person. const elsewhere = await browser.newContext() const other = await elsewhere.newPage() await signIn(other, email) // What the browser modules keep for a signed-in person, as their providers store it. await page.evaluate((slug) => { sessionStorage.setItem(`conusai.guides.${slug}.offline`, "{}") // kit:guides sessionStorage.setItem("conusai.support.session.v1:spk_e2e", "{}") // kit:support sessionStorage.setItem(`unrelated.${slug}`, "kept") }, org) // The shell links to the page from every page. await page.goto("/") await openPersonMenu(page) await page.getByRole("menuitem", { name: "Account", exact: true }).click() await expect(page).toHaveURL(/\/settings\/account$/) const account = page.getByRole("region", { name: "Your account", exact: true }) await expect(account.getByRole("heading", { level: 2, name: "Your account", exact: true })).toBeVisible() await expect(account.getByText(email, { exact: true })).toBeVisible() await expect(page.getByText(/You are the only member of Org leaver-/)).toBeVisible() expect(await violations(page)).toEqual([]) // Export: one JSON file, every section named. const download = page.waitForEvent("download") await page.getByRole("button", { name: "Export my data" }).click() const file = await download expect(file.suggestedFilename()).toMatch(/^account-export-\d{4}-\d{2}-\d{2}\.json$/) const exported = JSON.parse(readFileSync(await file.path(), "utf8")) as { format: string complete: boolean sections: Record } expect(exported.format).toBe("conusai.account-export.v1") expect(exported.complete).toBe(true) expect(exported.sections.account).toMatchObject({ status: "ok", data: { profile: { email }, organizations: [{ slug: org, role: "owner" }] }, }) // Offline (the fake platform), the services hold nothing of the person, and say so. expect(exported.sections.guides).toEqual({ status: "ok", data: null }) // kit:guides expect(exported.sections.support).toEqual({ status: "ok", data: null }) // kit:support await expect(page.getByRole("status").filter({ hasText: "Your export is downloaded." })).toBeVisible() // The analytics erasure asks first, starts on Cancel, and gives focus back when it is cancelled. const erase = page.getByRole("button", { name: "Erase my analytics data" }) await erase.click() const erasure = page.getByRole("alertdialog", { name: "Erase my analytics data" }) await expect(erasure.getByText("cannot be undone")).toBeVisible() await expect(erasure.getByRole("button", { name: "Cancel" })).toBeFocused() expect(await violations(page)).toEqual([]) await page.keyboard.press("Escape") await expect(erasure).toBeHidden() await expect(erase).toBeFocused() // A refusal stays in the dialog, which stays open; confirming again goes through. await page.route("**/account.analytics.request", (route) => route.fulfill({ status: 503, contentType: "application/problem+json", body: JSON.stringify({ title: "Unavailable", status: 503, code: "unavailable", detail: "no answer" }), }), ) await erase.click() await erasure.getByRole("button", { name: "Yes, erase it" }).click() await expect(erasure.getByRole("alert")).toHaveText("That did not work: no answer") await page.unroute("**/account.analytics.request") await erasure.getByRole("button", { name: "Yes, erase it" }).click() await expect(erasure).toBeHidden() await expect(page.getByRole("status").filter({ hasText: "Your request is done." })).toBeVisible() // A wrong address is refused, and nothing happens. const confirm = page.getByLabel("Type your e-mail address to confirm") await confirm.fill("somebody@example.test") await page.getByRole("button", { name: "Delete my account" }).click() await expect( page.getByRole("alert").filter({ hasText: "type the account's e-mail address to confirm" }), ).toBeVisible() expect(await violations(page)).toEqual([]) // The address, and Enter: the keyboard alone deletes the account. await confirm.fill(email) await confirm.press("Enter") await expect( page.getByRole("status").filter({ hasText: "Your account is deleted, and you are signed out." }), ).toBeVisible() expect(await violations(page)).toEqual([]) // The browser modules' lifecycle handlers ran: their keys are gone, nobody else's. const left = await page.evaluate(() => Object.keys(sessionStorage).sort()) expect(left).toEqual([`unrelated.${org}`]) // Signed out here, and on the other device. await page.goto("/settings/account") await expect(page).toHaveURL(/\/auth\/sign-in/) await other.goto("/") await expect(other.getByRole("heading", { level: 1, name: "Sign in", exact: true })).toBeVisible() // The other device's cookie, sent to the API: its session is revoked, so it is nobody. expect((await callRaw(other.request, "", "account.deletion.status", undefined)).status()).toBe(401) await elsewhere.close() // The same address is a new person now, with no organization. await signIn(page, email) await page.goto("/") await expect(page.getByRole("heading", { level: 1, name: "Create your organization" })).toBeVisible() await expect(page.getByRole("link", { name: `Org ${org}` })).toHaveCount(0) }) for (const theme of ["light", "dark"] as const) { test(`WCAG audits wait for finite dialog motion and still refuse bad contrast (${theme})`, async ({ page }, info) => { await signIn(page, `${unique("motion-audit")}@example.test`) await makeOrganization(page, "motion-audit") await page.goto("/") await openPersonMenu(page) await page.getByRole("menuitemradio", { name: theme === "light" ? "Light" : "Dark", exact: true }).click() await openPersonMenu(page) await page.getByRole("menuitem", { name: "Account", exact: true }).click() await page.emulateMedia({ reducedMotion: "no-preference" }) // Extend the real entrance to expose sampling during motion deterministically. // Keep unrelated continuous motion running: an audit must not wait forever or disable it. await page.evaluate(() => { document.documentElement.style.setProperty("--kit-motion-standard", "2000ms") const continuous = document.body.animate([{ opacity: 1 }, { opacity: 1 }], { duration: 1000, iterations: Infinity, }) continuous.id = "audit-continuous" }) const erase = page.getByRole("button", { name: "Erase my analytics data", exact: true }) await erase.click() const dialog = page.getByRole("alertdialog", { name: "Erase my analytics data", exact: true }) expect( await dialog.evaluate((el) => el.getAnimations().some((animation) => animation.playState === "running")), ).toBe(true) expect(await violations(page)).toEqual([]) expect( await page.evaluate(() => document .getAnimations() .some((animation) => animation.id === "audit-continuous" && animation.playState === "running"), ), ).toBe(true) const button = dialog.getByRole("button", { name: "Yes, erase it", exact: true }) const settled = await button.evaluate((el) => { const style = getComputedStyle(el) const dialog = el.closest('[role="alertdialog"]') if (!dialog) throw new Error("The confirmation dialog is missing") const surface = getComputedStyle(dialog) return { color: style.color, background: style.backgroundColor, opacity: surface.opacity, fontSize: style.fontSize, fontWeight: style.fontWeight, } }) await info.attach("settled-dialog-style", { body: Buffer.from(JSON.stringify({ theme, ...settled }, null, 2)), contentType: "application/json", }) await page.screenshot({ path: info.outputPath(`motion-audit-${theme}.png`) }) // A genuine steady-state violation must still be reported after synchronization. await page.evaluate(() => { for (const animation of document.getAnimations()) if (animation.id === "audit-continuous") animation.cancel() }) // Keep the known low-opacity rendering static: settling motion must not hide a real violation. await dialog.evaluate((el) => { el.style.opacity = "0.4" }) const refused = await violations(page) await info.attach("deliberate-contrast-refusal", { body: Buffer.from(JSON.stringify(refused)), contentType: "application/json", }) expect(refused.some((issue) => issue.startsWith("color-contrast:"))).toBe(true) await dialog.evaluate((el) => { el.style.removeProperty("opacity") }) expect(await violations(page)).toEqual([]) await dialog.getByRole("button", { name: "Cancel", exact: true }).click() await expect(dialog).toBeHidden() await expect(erase).toBeFocused() }) }