// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // A module's pages above a project on a real Postgres (F10): the four answers of // `///modules/`, the query string reaching the server, the navigation at that scope, // and a project that cannot take the reserved segment. // // `E2E_BUILD_MODE=production pnpm exec playwright test -c e2e/playwright.config.ts workspace-pages` runs this // file on a production `vite build` (e2e/support/stack.ts). import { type Browser, expect, type Page, test } from "@playwright/test" import { call, callRaw, chooseLanguage, makeOrganization, signIn, unique, violations } from "./support/app" const base = `http://127.0.0.1:${process.env.E2E_WEB_PORT ?? 3100}` async function person(browser: Browser, email: string): Promise { const context = await browser.newContext({ baseURL: base }) const page = await context.newPage() await signIn(page, email) return page } const pathOf = (org: string, workspace: string, query = ""): string => `/${org}/${workspace}/modules/notes${query}` test("signed out is a 302 to sign in, and the path comes back", async ({ browser, page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) const context = await browser.newContext({ baseURL: base }) const response = await context.request.get(pathOf(org, workspace), { maxRedirects: 0 }) expect(response.status()).toBe(302) expect(response.headers().location).toContain("/auth/sign-in") expect(decodeURIComponent(response.headers().location ?? "")).toContain(pathOf(org, workspace)) await context.close() }) test("signed in is a 200 with the page, the navigation and the query string on the server", async ({ page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace, project } = await makeOrganization(page) await call(page, "tenancy.project.create", { org, workspace, slug: "other", name: "Other", moduleKey: "notes" }) const response = await page.goto(pathOf(org, workspace)) expect(response?.status()).toBe(200) await expect(page.getByRole("heading", { level: 1, name: "Notes in this workspace" })).toBeVisible() await expect(page.locator("main").getByRole("link", { name: "Review" })).toHaveAttribute( "href", `/${org}/${workspace}/${project}/notes`, ) expect(await violations(page)).toEqual([]) // The first byte already holds the filtered list: `load` read `?q=`, the browser did not. const filtered = await page.request.get(pathOf(org, workspace, "?q=oth")) expect(filtered.status()).toBe(200) const html = await filtered.text() expect(html).toContain(">Other<") expect(html).not.toContain(">Review<") // A search can be submitted with the keyboard and recovered without editing the URL. await page.getByRole("searchbox", { name: "Project name" }).fill("no matching project") await page.getByRole("searchbox", { name: "Project name" }).press("Enter") await expect(page.getByText("No project matches.")).toBeVisible() await page.getByRole("link", { name: "Clear filter" }).click() await expect(page.getByRole("link", { name: "Review" })).toBeVisible() // `ui.nav` with `scope: "workspace"`, on the workspace's own page and on a project's. await page.goto(`/${org}/${workspace}`) const nav = page.getByRole("navigation", { name: "Main navigation" }) await expect(nav.getByRole("link", { name: "Notes in this workspace" })).toHaveAttribute( "href", pathOf(org, workspace), ) await page.goto(`/${org}/${workspace}/${project}`) await expect(nav.getByRole("link", { name: "Notes in this workspace" })).toBeVisible() }) test("a count only the browser knows reads right in English and in Lithuanian", async ({ page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) await call(page, "tenancy.project.create", { org, workspace, slug: "other", name: "Other", moduleKey: "notes" }) await page.goto(pathOf(org, workspace)) const status = page.locator("[data-slot=resource-count]") await expect(status).toHaveText("0 projects selected") await page .locator("label") .filter({ has: page.getByRole("checkbox", { name: "Review", exact: true }) }) .click() await expect(status).toHaveText("1 project selected") await page .locator("label") .filter({ has: page.getByRole("checkbox", { name: "Other", exact: true }) }) .click() await expect(status).toHaveText("2 projects selected") // The language changes in place: the selection stays, and the count reads in Lithuanian, in each plural form. await chooseLanguage(page, "Lietuvių") await expect(page.getByRole("heading", { level: 1, name: "Užrašai šioje darbo srityje" })).toBeVisible() await expect(status).toHaveText("Pasirinkti 2 projektai") await page .locator("label") .filter({ has: page.getByRole("checkbox", { name: "Other", exact: true }) }) .click() await expect(status).toHaveText("Pasirinktas 1 projektas") await page .locator("label") .filter({ has: page.getByRole("checkbox", { name: "Review", exact: true }) }) .click() await expect(status).toHaveText("Pasirinkta 0 projektų") }) test("selection controls stay square and keyboard accessible in both themes", async ({ page }, testInfo) => { const errors: string[] = [] const responses: { path: string; status: number }[] = [] page.on("pageerror", (error) => errors.push(error.message)) page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()) }) page.on("requestfailed", (request) => errors.push(`${new URL(request.url()).pathname}: ${request.failure()?.errorText}`), ) page.on("response", (response) => responses.push({ path: new URL(response.url()).pathname, status: response.status() }), ) await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) await call(page, "tenancy.project.create", { org, workspace, slug: "other", name: "Other", moduleKey: "notes" }) await page.goto(pathOf(org, workspace)) await page.waitForLoadState("networkidle") const privacy = page.getByRole("checkbox", { name: /Also allow session replay/ }) if (await privacy.isVisible()) { const size = await privacy.boundingBox() expect(size?.width).toBe(size?.height) const reject = page.getByRole("button", { name: "Reject analytics and replay" }) await reject.click() // the choice applies in place: the banner closes and the document stays await expect(page.getByRole("region", { name: "Privacy choices" })).toHaveCount(0) } for (const theme of ["light", "dark"]) { await page.context().addCookies([{ name: "app_theme", value: theme, url: base }]) for (const width of [375, 778, 1280]) { await page.setViewportSize({ width, height: 853 }) await page.reload() await page.waitForLoadState("networkidle") const boxes = await page.locator(".kit-data-table-box").evaluateAll((elements) => elements.map((element) => { const rect = element.getBoundingClientRect() return { width: rect.width, height: rect.height } }), ) expect(boxes.length).toBe(3) for (const box of boxes) expect(box.width).toBe(box.height) const first = page.locator("tbody").getByRole("checkbox").first() // A React Aria grid has one Tab stop; arrows reach its cells and controls. await page.getByRole("button", { name: "Filter", exact: true }).focus() await page.keyboard.press("Tab") await page.keyboard.press("ArrowRight") await expect(first).toBeFocused() await page.keyboard.press("Space") await expect(first).toBeChecked() const all = page.getByRole("checkbox", { name: "Select all", exact: true }) expect(await all.evaluate((control) => (control as HTMLInputElement).indeterminate)).toBe(true) await expect(page.locator("[data-slot=resource-count]")).toHaveText("1 project selected") await page.screenshot({ path: testInfo.outputPath(`selection-${theme}-${width}.png`) }) await page.keyboard.press("Space") await expect(first).not.toBeChecked() } expect(await violations(page)).toEqual([]) } expect(errors).toEqual([]) expect(responses.filter((response) => response.status >= 400)).toEqual([]) await testInfo.attach("selection-network", { body: JSON.stringify(responses, null, 2), contentType: "application/json", }) }) test("another organization is the 404 of a workspace that does not exist", async ({ browser, page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) const stranger = await person(browser, `${unique("stranger")}@example.test`) const theirs = await makeOrganization(stranger, "other") const refused = await stranger.request.get(pathOf(org, workspace), { maxRedirects: 0 }) const missing = await stranger.request.get(pathOf(theirs.org, "nowhere"), { maxRedirects: 0 }) expect(refused.status()).toBe(404) expect(missing.status()).toBe(404) expect((await stranger.request.get(pathOf(theirs.org, theirs.workspace))).status()).toBe(200) await stranger.context().close() }) test("a member without the permission gets a 403 and no link to it", async ({ browser, page }) => { const owner = `${unique("owner")}@example.test` const member = `${unique("member")}@example.test` await signIn(page, owner) const { org, workspace } = await makeOrganization(page) // A custom role that reaches the organization and nothing of notes: a member who may not read them. const role = await call<{ key: string }>(page, "tenancy.role.create", { org, name: "Guest", level: "org", permissions: ["tenancy.org.read", "tenancy.workspace.read"], }) const invited = await call<{ token: string }>(page, "tenancy.member.invite", { org, email: member, role: "viewer", roleKey: role.key, }) const memberPage = await person(browser, member) await call(memberPage, "tenancy.invitation.accept", { org, token: invited.token }) const response = await memberPage.goto(pathOf(org, workspace)) expect(response?.status()).toBe(403) await expect(memberPage.getByRole("heading", { level: 1, name: "You do not have access" })).toBeVisible() await memberPage.goto(`/${org}/${workspace}`) await expect(memberPage.getByRole("navigation", { name: "This workspace" })).toHaveCount(0) await memberPage.context().close() }) test("no project can take the route's segment", async ({ page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) await page.goto(`/${org}/${workspace}`) const csrf = (await page.locator('meta[name="csrf-token"]').getAttribute("content")) ?? "" const refused = await callRaw(page.request, csrf, "tenancy.project.create", { org, workspace, slug: "modules", name: "Modules", moduleKey: "notes", }) expect(refused.status()).toBe(409) })