// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // What every spec does: sign in through the dev adapter, call a procedure the way the page's own // forms do (with the page's CSRF token), and make an organization to work in. import { randomBytes } from "node:crypto" import { type APIRequestContext, expect, type Page } from "@playwright/test" import axe from "axe-core" export const unique = (prefix: string): string => `${prefix}-${randomBytes(3).toString("hex")}` /** * Waits until the page is interactive: the shell sets `data-hydrated` on once React has hydrated it * (`apps/web/src/ui/hydrated.ts`). A click or a key before that is lost, so act after this, not in a retry loop. */ export async function ready(page: Page): Promise { await expect(page.locator("html")).toHaveAttribute("data-hydrated", "true") } /** Signs in as `email` (created on first use) and lands on the home page, ready to be used. */ export async function signIn(page: Page, email: string): Promise { await page.goto(`/auth/dev-login?email=${encodeURIComponent(email)}`) await expect(page.locator('meta[name="csrf-token"]')).toHaveCount(1) await ready(page) } /** The CSRF token of the page the browser is on: what the page's own forms send back. */ const csrfOf = (page: Page): Promise => page .locator('meta[name="csrf-token"]') .getAttribute("content") .then((value) => value ?? "") /** Calls a procedure with the session of `page`. Throws on a refusal, with the problem's own words. */ export async function call(page: Page, procedure: string, input: unknown): Promise { const response = await callRaw(page.request, await csrfOf(page), procedure, input) const body = (await response.json().catch(() => null)) as T if (!response.ok()) throw new Error(`${procedure} answered ${response.status()}: ${JSON.stringify(body)}`) return body } export const callRaw = (request: APIRequestContext, csrf: string, procedure: string, input: unknown) => request.post(`/api/v1/${procedure}`, { data: input, headers: { "x-csrf-token": csrf, "content-type": "application/json", "sec-fetch-site": "same-origin" }, }) export interface Workspace { /** The organization's id: the app tenant a business-service binding names. */ orgId: string org: string workspace: string project: string } /** The signed-in user's own organization with one workspace and one project. */ export async function makeOrganization(page: Page, prefix = "acme"): Promise { const org = unique(prefix) const { orgId } = await call<{ orgId: string }>(page, "tenancy.org.create", { slug: org, name: `Org ${org}` }) const workspace = ( await call<{ slug: string }>(page, "tenancy.workspace.create", { org, slug: "main", name: "Main", visibility: "org", }) ).slug const project = ( await call<{ slug: string }>(page, "tenancy.project.create", { org, workspace, slug: "review", name: "Review", moduleKey: "documents", }) ).slug return { orgId, org, workspace, project } } /** * Binds an organization to the deployment on the fake platform, with exactly the service scopes the app's selected * business modules declare (`declaredBusinessScopes`, as `pnpm dev` binds its demo organizations). In Cloud this is a * project administrator's explicit act, and a lookup never creates a binding; a spec does it on purpose, after it has * seen the refusal an unbound organization gets. */ export async function bindBusinessServices( orgId: string, grants: Readonly>, ): Promise { const base = process.env.E2E_PLATFORM_URL if (!base) throw new Error("e2e: E2E_PLATFORM_URL is set by the global setup") const response = await fetch(`${base}/__e2e/app-bindings`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ app_tenant_id: orgId, grants }), }) if (!response.ok) throw new Error(`e2e: the fake platform refused the binding (${response.status}: ${await response.text()})`) } /** Puts axe into the page. */ async function injectAxe(page: Page): Promise { await page.addScriptTag({ content: axe.source }) } /** WCAG 2.2 AA violations on the page the browser is on, as axe finds them. */ export async function violations(page: Page): Promise { await injectAxe(page) // Finite entrance/hover transitions temporarily blend text and backgrounds. // Audit their settled paint without disabling motion or waiting on continuous busy indicators. await page.waitForFunction( () => document .getAnimations() .every( (animation) => !Number.isFinite(animation.effect?.getComputedTiming().endTime) || animation.playState === "finished" || animation.playState === "idle", ), undefined, { timeout: 5000 }, ) const results = await page.evaluate(() => ( globalThis as unknown as { axe: { run( context: unknown, options: unknown, ): Promise<{ violations: Array<{ id: string; nodes: Array<{ target: unknown[] }> }> }> } } ).axe.run(document, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"] } }), ) return results.violations.map( (violation) => `${violation.id}: ${violation.nodes.map((node) => node.target.join(" ")).join(", ")}`, ) } /** What the fake platform has recorded: the audit events and usage facts the worker sent, and the blobs it stored. */ export interface PlatformState { audit: Array<{ action: string; resource_id: string | null }> usage: Array> blobs: Array<{ pathname: string; contentType: string; size: number }> otel: { logs: Array>; traces: Array> } } export async function platformState(): Promise { const base = process.env.E2E_PLATFORM_URL if (!base) throw new Error("e2e: E2E_PLATFORM_URL is set by the global setup") const response = await fetch(`${base}/__e2e/state`) if (!response.ok) throw new Error(`e2e: the fake platform answered ${response.status}`) const state = (await response.json()) as PlatformState // Audit and objects use the fake token’s tenant. Billing is scoped to the // server-pinned deployment organization, as it is on Cloud. const organization = process.env.E2E_USAGE_ORGANIZATION if (!organization) throw new Error("e2e: the global setup pins E2E_USAGE_ORGANIZATION") const billed = await fetch(`${base}/__e2e/state`, { headers: { "x-conusai-organization": organization } }) if (!billed.ok) throw new Error(`e2e: the fake billing state answered ${billed.status}`) return { ...state, usage: ((await billed.json()) as PlatformState).usage } } /** Opens the person's menu at the foot of the frame's sidebar. */ export async function openPersonMenu(page: Page): Promise { await ready(page) await page.locator('[data-guide="frame.person"]').click() } /** * Chooses a language from the person's menu. The page changes language in place (the loaders run again, nothing * reloads), so this waits for the document's `lang` to move off the one it had. */ export async function chooseLanguage(page: Page, language: string): Promise { const before = await page.locator("html").getAttribute("lang") await openPersonMenu(page) await page.getByRole("menuitemradio", { name: language }).click() await expect(page.locator("html")).not.toHaveAttribute("lang", before ?? "") } /** Opens the scope switcher in the frame's sidebar. */ export async function openScope(page: Page): Promise { await ready(page) await page.locator('[data-guide="frame.scope"]').click() }