// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // A slow current module must not replace the server-rendered link during its // first gesture. Real app/Postgres; fake managed platform, not Cloud acceptance. import { writeFile } from "node:fs/promises" import { expect, test } from "@playwright/test" import { call, makeOrganization, ready, signIn, unique } from "./support/app" for (const scope of ["workspace", "workspace-row", "project"] as const) { test(`${scope} server link survives hydration while its view code is delayed`, async ({ page }) => { await signIn(page, `${unique("owner")}@example.test`) const slugs = await makeOrganization(page) const notes = `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes` const workspace = `/${slugs.org}/${slugs.workspace}/modules/notes` let path = `${workspace}?q=no+matching+project` let name = "Clear filter" if (scope === "workspace-row") { path = workspace name = "Review" } if (scope === "project") { const { noteId } = await call<{ noteId: string }>(page, "notes.note.create", { ...slugs, title: "Hydration note", body: "A server-rendered detail.", }) path = `${notes}/${noteId}` name = "All notes" } let release = (): void => {} const code = new Promise((resolve) => { release = resolve }) let requested = false const chunk = scope === "project" ? "**/assets/pages-*.js" : "**/assets/workspace-page-*.js" await page.route(chunk, async (route) => { requested = true await code await route.continue() }) try { await page.goto(path, { waitUntil: "commit" }) const link = page.getByRole("link", { name, exact: true }) await expect(link).toBeVisible() await expect.poll(() => requested).toBe(true) const original = await page.evaluateHandle( (label) => Array.from(document.querySelectorAll("a[href]")).find((node) => node.textContent === label) ?? null, name, ) expect(await original.evaluate((node) => Boolean(node?.isConnected && node.getClientRects().length > 0))).toBe( true, ) const element = original.asElement() if (!element) throw new Error("the server did not retain its link") const box = await element.boundingBox() if (!box) throw new Error("the server link has no hit target") await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) await page.mouse.down() // Drive a real browser paint while the module response remains blocked. // The current server node must remain a usable hit target, not fallback. await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))), ) expect(await original.evaluate((node) => Boolean(node?.isConnected && node.getClientRects().length > 0))).toBe( true, ) // Nothing has hydrated while the page's code is held, so the document does not claim it is interactive. expect(await page.locator("html").getAttribute("data-hydrated")).toBeNull() release() // A live page keeps its stream open, so the network never goes idle: wait for hydration itself. await ready(page) expect(await original.evaluate((node) => Boolean(node?.isConnected && node.getClientRects().length > 0))).toBe( true, ) // Hydration and late provider styles must preserve the original hit target. // Do not move the pointer again: that would hide the lost-click defect. expect(await element.boundingBox()).toEqual(box) expect( await original.evaluate((node, point) => node?.contains(document.elementFromPoint(point.x, point.y)) ?? false, { x: box.x + box.width / 2, y: box.y + box.height / 2, }), ).toBe(true) await page.mouse.up() await expect(page).toHaveURL(scope === "workspace" ? workspace : notes) await expect(page.getByRole("heading", { level: 1 })).toBeVisible() } finally { release() } }) } test("JS-only selection stays disabled until the matched workspace view is hydrated", async ({ page }) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) let release = (): void => {} const code = new Promise((resolve) => { release = resolve }) let requested = false await page.route("**/assets/workspace-page-*.js", async (route) => { requested = true await code await route.continue() }) try { await page.goto(`/${org}/${workspace}/modules/notes`, { waitUntil: "commit" }) await expect.poll(() => requested).toBe(true) const status = page.locator("[data-slot=resource-count]") await expect(status).toHaveText("0 projects selected") const checkbox = page.getByRole("checkbox", { name: "Review", exact: true }) await expect(checkbox).toBeDisabled() const select = page.locator("label").filter({ has: checkbox }).click() release() await select await expect(status).toHaveText("1 project selected") } finally { release() } }) test("a failed initial module has a recovery page and can reload after the network recovers", async ({ page, }, info) => { await signIn(page, `${unique("owner")}@example.test`) const { org, workspace } = await makeOrganization(page) const errors: string[] = [] const consoleMessages: string[] = [] const responses: { path: string; status: number }[] = [] let offline = true page.on("pageerror", (error) => errors.push(error.message)) page.on("console", (message) => { if (message.type() === "error" || message.type() === "warning") consoleMessages.push(message.text()) }) page.on("response", (response) => { responses.push({ path: new URL(response.url()).pathname, status: response.status() }) }) await page.route("**/assets/workspace-page-*.js", (route) => offline ? route.fulfill({ status: 503, contentType: "text/javascript", body: "// module temporarily unavailable" }) : route.continue(), ) await page.goto(`/${org}/${workspace}/modules/notes`) await expect(page.getByRole("heading", { level: 1, name: "Something went wrong" })).toBeVisible() await expect(page.getByRole("alert")).toContainText("Try again") await expect(page.getByRole("link", { name: "Back to the start" })).toHaveAttribute("href", "/") await page.screenshot({ path: info.outputPath("module-download-recovery.png") }) expect(responses.some((response) => response.path.includes("workspace-page-") && response.status === 503)).toBe(true) // React reports its fallback from concurrent to synchronous rendering when // this deliberately broken import reaches the boundary. Keep that diagnostic // in the evidence, while rejecting unrelated errors and any after recovery. expect( errors.filter( (message) => message !== "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root." && !message.startsWith("Minified React error #520;"), ), ).toEqual([]) const recoveryReports = errors.length offline = false const retry = page.getByRole("button", { name: "Try again", exact: true }) await retry.focus() await retry.press("Enter") await expect(page.getByRole("heading", { level: 1, name: "Notes in this workspace" })).toBeVisible() await expect(page.getByRole("checkbox", { name: "Review", exact: true })).toBeEnabled() await page.waitForLoadState("networkidle") expect(errors).toHaveLength(recoveryReports) await page.screenshot({ path: info.outputPath("module-download-recovered.png") }) const networkPath = info.outputPath("module-download-network.json") const consolePath = info.outputPath("module-download-console.json") await writeFile(networkPath, JSON.stringify(responses, null, 2)) await writeFile(consolePath, JSON.stringify({ recoverableErrors: errors, messages: consoleMessages }, null, 2)) await info.attach("module-download-network.json", { path: networkPath, contentType: "application/json", }) await info.attach("module-download-console.json", { path: consolePath, contentType: "application/json", }) })