136 lines
6.4 KiB
TypeScript
136 lines
6.4 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// Route reachability and assistive navigation on the actual app, with simulated provider services.
|
|
import { expect, test } from "@playwright/test"
|
|
import { moduleSelected, OPTIONAL_MODULE_IDS } from "../modules/inclusion"
|
|
import { modulePages } from "../modules/pages"
|
|
import { makeOrganization, signIn, unique, violations } from "./support/app"
|
|
|
|
test("selected module routes, settings links and unknown pages have deliberate accessible outcomes", async ({
|
|
page,
|
|
}, info) => {
|
|
test.setTimeout(120_000)
|
|
const errors: string[] = []
|
|
let expectedRefusal = false
|
|
await signIn(page, `${unique("route-review")}@example.test`)
|
|
const scope = await makeOrganization(page)
|
|
const base = `/${scope.org}/${scope.workspace}/${scope.project}`
|
|
await page.goto(`/${scope.org}/settings`)
|
|
await page.getByRole("button", { name: "Reject analytics and replay", exact: true }).click()
|
|
// The next hard navigation must not abort the previous document's dynamic bootstrap.
|
|
await expect(page.locator('[data-guide="frame.person"]')).toBeEnabled()
|
|
page.on("pageerror", (error) => errors.push(error.message))
|
|
page.on("console", (message) => {
|
|
if (!expectedRefusal && ["warning", "error"].includes(message.type())) errors.push(message.text())
|
|
})
|
|
const settings = await page
|
|
.locator('[data-slot="settings-layout"] a[href]')
|
|
.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("href") ?? ""))
|
|
const routes = [
|
|
...new Set([
|
|
`/${scope.org}`,
|
|
`/${scope.org}/${scope.workspace}`,
|
|
base,
|
|
`/${scope.org}/${scope.workspace}/modules/notes`,
|
|
...settings,
|
|
...Object.keys(modulePages).map((id) => `${base}/${id}`),
|
|
]),
|
|
]
|
|
const outcomes: Array<{ path: string; status: number }> = []
|
|
for (const path of routes) {
|
|
const response = await page.goto(path)
|
|
outcomes.push({ path: path.replaceAll(scope.org, "<org>"), status: response?.status() ?? 0 })
|
|
expect(response?.status(), path).toBe(200)
|
|
await expect(page.locator("main h1").first()).toBeVisible()
|
|
await expect(page.locator('[data-guide="frame.person"]')).toBeEnabled()
|
|
expect(await violations(page), path).toEqual([])
|
|
}
|
|
expectedRefusal = true
|
|
for (const id of [
|
|
"missing-module",
|
|
...OPTIONAL_MODULE_IDS.filter((id) => !moduleSelected(id) && !["guides", "support"].includes(id)),
|
|
]) {
|
|
const response = await page.goto(`${base}/${id}`)
|
|
expect(response?.status(), id).toBe(404)
|
|
await expect(page.getByRole("heading", { name: "Page not found", exact: true })).toBeVisible()
|
|
expect(await violations(page), `refused ${id}`).toEqual([])
|
|
}
|
|
await info.attach("route-outcomes", {
|
|
body: Buffer.from(JSON.stringify(outcomes, null, 2)),
|
|
contentType: "application/json",
|
|
})
|
|
expect(errors).toEqual([])
|
|
})
|
|
|
|
test("keyboard skips the frame and client navigation focuses the new page in both themes", async ({
|
|
page,
|
|
browserName,
|
|
}, info) => {
|
|
await signIn(page, `${unique("keyboard-review")}@example.test`)
|
|
const scope = await makeOrganization(page)
|
|
await page.goto(`/${scope.org}/${scope.workspace}`)
|
|
await page.getByRole("button", { name: "Reject analytics and replay", exact: true }).click()
|
|
for (const colorScheme of ["light", "dark"] as const) {
|
|
await page.locator('[data-guide="frame.person"]').click()
|
|
await page.getByRole("menuitemradio", { name: "System", exact: true }).click()
|
|
await page.reload()
|
|
await page.locator('[data-guide="frame.person"]').click()
|
|
await page.getByRole("menuitemradio", { name: colorScheme === "light" ? "Light" : "Dark", exact: true }).click()
|
|
// Explicit preference wins even when a hidden document control was initially following the device.
|
|
await page.emulateMedia({ colorScheme: colorScheme === "light" ? "dark" : "light", reducedMotion: "reduce" })
|
|
if (colorScheme === "light") await expect(page.locator("html")).toHaveClass(/light/)
|
|
else await expect(page.locator("html")).not.toHaveClass(/light/)
|
|
await page.reload()
|
|
await page.locator("body").press(browserName === "webkit" ? "Alt+Tab" : "Tab")
|
|
await expect(page.getByRole("link", { name: "Skip to content", exact: true })).toBeFocused()
|
|
await page.keyboard.press("Enter")
|
|
await expect(page.locator("main")).toBeFocused()
|
|
const settings = page.getByRole("link", { name: "Organization settings", exact: true })
|
|
if (colorScheme === "light") {
|
|
let release = () => {}
|
|
const paused = new Promise<void>((resolve) => {
|
|
release = resolve
|
|
})
|
|
await page.route("**/*", async (route) => {
|
|
if (route.request().resourceType() === "fetch" && route.request().method() === "GET") await paused
|
|
await route.continue()
|
|
})
|
|
try {
|
|
await settings.press("Enter")
|
|
const skeleton = page.locator('[data-slot="page-skeleton"]')
|
|
await expect(skeleton).toBeVisible()
|
|
await expect(skeleton.getByRole("status")).toBeVisible()
|
|
const bar = skeleton.locator('[data-slot="skeleton"]').first()
|
|
await expect(bar).toBeVisible()
|
|
const painted = await bar.evaluate((el) => {
|
|
const style = getComputedStyle(el)
|
|
return { background: style.backgroundColor, animation: style.animationName }
|
|
})
|
|
expect(painted.background).not.toBe("rgba(0, 0, 0, 0)")
|
|
expect(painted.animation).toBe("none")
|
|
await page.screenshot({ path: info.outputPath("settings-loading-reduced-motion.png") })
|
|
} finally {
|
|
release()
|
|
await page.unrouteAll({ behavior: "wait" })
|
|
}
|
|
} else {
|
|
await settings.press("Enter")
|
|
}
|
|
await expect(page.locator("main h1").first()).toBeFocused()
|
|
for (const width of [360, 768, 1280]) {
|
|
await page.setViewportSize({ width, height: 900 })
|
|
const geometry = await page.evaluate(() => ({
|
|
width: innerWidth,
|
|
scroll: document.documentElement.scrollWidth,
|
|
overflow: Array.from(document.querySelectorAll("*"))
|
|
.map((el) => ({ tag: el.tagName, cls: String(el.className), right: el.getBoundingClientRect().right }))
|
|
.filter((el) => el.right > innerWidth + 1)
|
|
.slice(0, 12),
|
|
}))
|
|
expect(geometry.scroll, JSON.stringify(geometry)).toBeLessThanOrEqual(geometry.width)
|
|
expect(await violations(page)).toEqual([])
|
|
await page.screenshot({ path: info.outputPath(`settings-${colorScheme}-${width}.png`) })
|
|
}
|
|
await page.goto(`/${scope.org}/${scope.workspace}`)
|
|
}
|
|
})
|