import { expect, test, type Page } from "@playwright/test"; async function captureTransitions(page: Page) { await page.evaluate(() => { const state = window as typeof window & { __nvrEvents?: Array<{ direction: string | null; presentation: string | null; }>; __nvrObserver?: MutationObserver; }; state.__nvrObserver?.disconnect(); state.__nvrEvents = []; const view = document.querySelector(".nvr-router-view"); if (!view) throw new Error("Native router view did not render"); state.__nvrObserver = new MutationObserver(() => { if (view.classList.contains("nvr-router-view--interactive")) { state.__nvrEvents?.push({ direction: view.getAttribute("data-native-direction"), presentation: view.getAttribute("data-native-presentation"), }); } }); state.__nvrObserver.observe(view, { attributes: true }); }); } async function recordedTransitions(page: Page) { return await page.evaluate( () => ( window as typeof window & { __nvrEvents?: Array<{ direction: string | null; presentation: string | null; }>; } ).__nvrEvents ?? [], ); } async function waitForTransition(page: Page) { await expect(page.locator(".nvr-router-view")).not.toHaveClass( /nvr-router-view--interactive/, ); } async function flickToNextTab(page: Page, leaveSlowSpring = false) { // Start on the route header, outside conversation-owned drag targets. const surface = await page.locator(".nvr-router-view").boundingBox(); const header = await page .locator( '[data-native-role="active"] .app-header, [data-native-role="to"] .app-header', ) .last() .boundingBox(); if (!surface || !header) throw new Error("Active route header did not render"); const y = header.y + header.height * 0.5; await page.mouse.move(surface.x + surface.width * 0.72, y); await page.mouse.down(); if (leaveSlowSpring) { await page.mouse.move(surface.x + surface.width * 0.3, y, { steps: 10 }); await page.waitForTimeout(90); } await page.mouse.move(surface.x + surface.width * 0.27, y); await page.mouse.up(); } test("navigates a conversation and returns through the native runtime", async ({ page, }) => { await page.goto("/inbox"); await expect(page.getByRole("heading", { name: "Messages" })).toBeVisible(); await page.getByText("Maya Chen").last().click(); await expect(page).toHaveURL(/\/chat\/maya$/); await expect(page.getByRole("heading", { name: "Maya Chen" })).toBeVisible(); await page.getByRole("button", { name: "Back" }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); await expect(page.getByRole("heading", { name: "Messages" })).toBeVisible(); }); test("switches sibling routes without growing the primary history flow", async ({ page, }) => { await page.goto("/inbox"); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/stories$/); await waitForTransition(page); await expect(page.getByRole("heading", { name: "Stories" })).toBeVisible(); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await expect(page.getByRole("heading", { name: "You" })).toBeVisible(); }); test("uses route order for tab direction and does not animate the active tab", async ({ page, }) => { await page.goto("/inbox"); await captureTransitions(page); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/stories$/); await waitForTransition(page); expect(await recordedTransitions(page)).toContainEqual({ direction: "forward", presentation: "slide", }); await captureTransitions(page); await page.getByRole("link", { name: /Inbox/ }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); expect(await recordedTransitions(page)).toContainEqual({ direction: "back", presentation: "slide", }); await captureTransitions(page); await page.getByRole("link", { name: /Inbox/ }).click(); await page.waitForTimeout(100); expect(await recordedTransitions(page)).toEqual([]); await expect(page).toHaveURL(/\/inbox$/); }); test("interrupts an active tab animation when another tab is tapped", async ({ page, }) => { await page.goto("/inbox"); const routerView = page.locator(".nvr-router-view"); await page.getByRole("link", { name: /Stories/ }).click(); await expect(routerView).toHaveClass(/nvr-router-view--interactive/); const firstTransaction = await routerView.getAttribute( "data-native-transaction", ); expect(firstTransaction).not.toBeNull(); await page.getByRole("link", { name: /You/ }).click(); await expect(routerView).not.toHaveAttribute( "data-native-transaction", firstTransaction!, { timeout: 250 }, ); await expect(page.locator('[data-native-role="from"]')).toHaveAttribute( "data-native-route", "/stories", ); await expect(page.locator('[data-native-role="to"]')).toHaveAttribute( "data-native-route", "/profile", ); await expect(page).toHaveURL(/\/profile$/); }); test("interrupts a settling push animation with an edge-back gesture", async ({ page, }) => { await page.goto("/inbox"); await page.getByText("Maya Chen").last().click(); await expect(page).toHaveURL(/\/chat\/maya$/); const routerView = page.locator(".nvr-router-view"); await expect(routerView).toHaveClass(/nvr-router-view--interactive/); const pushTransaction = await routerView.getAttribute( "data-native-transaction", ); const frame = await page.locator(".app-frame").boundingBox(); if (!frame) throw new Error("App frame did not render"); await page.mouse.move(frame.x + 2, frame.y + frame.height * 0.5); await page.mouse.down(); await page.mouse.move( frame.x + frame.width * 0.34, frame.y + frame.height * 0.5, { steps: 16 }, ); await expect(routerView).not.toHaveAttribute( "data-native-transaction", pushTransaction!, { timeout: 250 }, ); await expect(page.locator('[data-native-role="from"]')).toHaveAttribute( "data-native-route", "/chat/maya", ); await expect(page.locator('[data-native-role="to"]')).toHaveAttribute( "data-native-route", "/inbox", ); await page.mouse.up(); }); test("moves sibling screens edge-to-edge at one-to-one drag progress", async ({ page, }) => { await page.goto("/stories"); const routerView = page.locator(".nvr-router-view"); const frame = await routerView.boundingBox(); if (!frame) throw new Error("Native router view did not render"); await page.mouse.move( frame.x + frame.width * 0.55, frame.y + frame.height * 0.45, ); await page.mouse.down(); await page.mouse.move( frame.x + frame.width * 0.8, frame.y + frame.height * 0.45, { steps: 18 }, ); await expect(routerView).toHaveAttribute("data-native-presentation", "slide"); await expect(routerView).toHaveAttribute("data-native-direction", "back"); const from = await page.locator('[data-native-role="from"]').boundingBox(); const to = await page.locator('[data-native-role="to"]').boundingBox(); if (!from || !to) throw new Error("Both sibling pages must be live during a drag"); expect(Math.abs(from.x - (to.x + to.width))).toBeLessThan(3); await page.mouse.up(); }); test("accepts a second fast tab flick while the first spring is still settling", async ({ page, }) => { await page.goto("/inbox"); const routerView = page.locator(".nvr-router-view"); // Commit by distance with a deliberately slow final sample, leaving enough // baseline spring for the second fast gesture to interrupt deterministically. await flickToNextTab(page, true); await expect(page).toHaveURL(/\/stories$/); await expect(routerView).toHaveClass(/nvr-router-view--interactive/); await flickToNextTab(page); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); // A stale pointer-up cleanup used to leave an orphaned transaction here, // permanently blocking both subsequent swipes and imperative tab links. await page.getByRole("link", { name: /Inbox/ }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); await expect(routerView).not.toHaveAttribute("data-native-transaction"); }); test("renders a suspended pushed sibling and evicts it after backing out", async ({ page, }) => { await page.goto("/profile"); await page.getByRole("link", { name: /Runtime stress lab/ }).click(); await expect(page.getByTestId("async-data-loading")).toBeVisible(); await expect(page).toHaveURL(/\/profile\/runtime-lab$/); await expect( page.getByRole("navigation", { name: "Primary navigation" }), ).toBeVisible(); await expect(page.getByRole("link", { name: /You/ })).toHaveAttribute( "aria-current", "page", ); await expect(page.getByTestId("async-data-ready")).toBeVisible({ timeout: 2_000, }); const lab = page.getByTestId("runtime-lab-view"); const firstMountId = await lab.getAttribute("data-mount-id"); await page.getByRole("button", { name: "Back" }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); // Popping a pushed route removes it after the exit animation. Keeping the // descriptor allows browser-forward navigation without retaining its DOM. await expect(page.getByTestId("runtime-lab-view")).toHaveCount(0); // browser forward exists only because this sibling opted into push history. await page.evaluate(() => history.forward()); await expect(page).toHaveURL(/\/profile\/runtime-lab$/); await expect(page.getByTestId("async-data-loading")).toBeVisible(); await expect(page.getByTestId("async-data-ready")).toBeVisible({ timeout: 2_000, }); await expect(page.getByTestId("runtime-lab-view")).not.toHaveAttribute( "data-mount-id", firstMountId!, ); }); test("clicking the root tab from a pushed sibling collapses its back history", async ({ page, }) => { await page.goto("/profile"); await page.getByRole("link", { name: /Runtime stress lab/ }).click(); await expect(page).toHaveURL(/\/profile\/runtime-lab$/); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await expect(page.locator(".nvr-navigator")).toHaveAttribute( "data-native-can-go-back", "false", ); await expect(page.getByTestId("runtime-lab-view")).toHaveCount(0); }); test("lazily caches a visited sibling and pauses its active work while hidden", async ({ page, }) => { await page.goto("/stories"); const stories = page.getByTestId("stories-view"); const mountId = await stories.getAttribute("data-mount-id"); await expect .poll(async () => Number(await stories.getAttribute("data-active-ticks"))) .toBeGreaterThan(1); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await expect(stories).toHaveCount(1); const hiddenTicks = Number(await stories.getAttribute("data-active-ticks")); await page.waitForTimeout(600); await expect(stories).toHaveAttribute( "data-active-ticks", String(hiddenTicks), ); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/stories$/); await waitForTransition(page); await expect(stories).toHaveAttribute("data-mount-id", mountId!); await expect .poll(async () => Number(await stories.getAttribute("data-active-ticks"))) .toBeGreaterThan(hiddenTicks); }); test("evicts a cached sibling when its dynamic entry guard rejects it", async ({ page, }) => { await page.goto("/stories"); const stories = page.getByTestId("stories-view"); const firstMountId = await stories.getAttribute("data-mount-id"); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await expect(stories).toHaveCount(1); const guardToggle = page.getByRole("button", { name: "Block Stories re-entry", }); await guardToggle.click(); await expect(guardToggle).toHaveAttribute("aria-pressed", "true"); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/profile$/); await expect(page.getByTestId("story-guard-status")).toHaveText("blocked"); await waitForTransition(page); await expect(page.getByTestId("stories-view")).toHaveCount(0); await guardToggle.click(); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/stories$/); await waitForTransition(page); await expect(page.getByTestId("stories-view")).not.toHaveAttribute( "data-mount-id", firstMountId!, ); }); test("manually unloads an inactive route through the public API demo", async ({ page, }) => { await page.goto("/stories"); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await expect(page.getByTestId("stories-view")).toHaveCount(1); await page.getByRole("link", { name: /Navigation lab/ }).click(); await expect(page).toHaveURL(/\/settings$/); await waitForTransition(page); await page.getByTestId("unload-stories").click(); await expect(page.getByTestId("unload-stories")).toHaveText( "Unloaded 1 Stories view", ); await expect(page.getByTestId("stories-view")).toHaveCount(0); }); test("exercises Vue built-ins, lifecycle hooks, injection, and scoped route state", async ({ page, }) => { await page.goto("/profile"); await page.getByRole("link", { name: /Vue compatibility lab/ }).click(); await expect(page).toHaveURL( /\/profile\/vue-lab\/alpha\?mode=manual#route-state$/, ); const lab = page.getByTestId("vue-compatibility-view"); const firstInstance = await lab.getAttribute("data-instance-id"); await expect(page.getByTestId("compat-param")).toHaveText("alpha"); await expect(page.getByTestId("compat-query")).toContainText("manual"); await expect(page.getByTestId("compat-hash")).toHaveText("#route-state"); await expect(page.getByTestId("options-lifecycle-probe")).toContainText( "/profile/vue-lab/alpha?mode=manual#route-state", ); await expect(page.getByTestId("inject-probe-route-tree")).toContainText( "Injected from the demo application root", ); await page.getByRole("button", { name: "Increment A" }).click(); await expect(page.getByTestId("keep-alive-A")).toContainText("Counter: 1"); await page.getByTestId("compat-switch-keepalive").click(); await expect(page.getByTestId("keep-alive-B")).toBeVisible(); await page.getByTestId("compat-switch-keepalive").click(); await expect(page.getByTestId("keep-alive-A")).toContainText("Counter: 1"); await page.getByTestId("compat-update-probes").click(); await expect(page.getByTestId("compat-event-log")).toContainText("onUpdated"); await page.getByTestId("compat-toggle-transition").click(); await expect(page.getByTestId("compat-transition-card")).toHaveCount(0); await expect(page.getByTestId("compat-event-log")).toContainText( "after-leave", ); await page.getByTestId("compat-open-teleport").click(); const teleport = page.getByTestId("compat-teleport-overlay"); await expect(teleport).toBeVisible(); await expect(page.getByTestId("inject-probe-teleport")).toContainText( "Injected from the demo application root", ); await page.getByRole("button", { name: "Close teleported overlay" }).click(); await page.getByTestId("compat-reload-suspense").click(); await expect(page.getByTestId("compat-suspense-fallback")).toBeVisible(); await expect(page.getByTestId("compat-suspense-ready")).toBeVisible({ timeout: 2_000, }); await page.getByTestId("compat-open-away").click(); await expect(page.getByTestId("vue-compatibility-away")).toBeVisible(); await page.getByTestId("compat-unload-return").click(); await expect(page.getByTestId("vue-compatibility-view")).toBeVisible(); await expect(page.getByTestId("vue-compatibility-view")).not.toHaveAttribute( "data-instance-id", firstInstance!, ); await expect(page.getByTestId("compat-event-log")).toContainText( "onUnmounted", ); }); test("records a navigation frame profile across route changes", async ({ page, }) => { await page.goto("/settings"); await page.getByTestId("profile-start").click(); await expect(page.locator(".profiler-badge")).toBeVisible(); await page.getByRole("button", { name: "Back" }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await page.getByRole("link", { name: /Runtime stress lab/ }).click(); await expect(page).toHaveURL(/\/profile\/runtime-lab$/); await page.getByRole("link", { name: /You/ }).click(); await expect(page).toHaveURL(/\/profile$/); await waitForTransition(page); await page.getByRole("link", { name: /Navigation lab/ }).click(); await expect(page).toHaveURL(/\/settings$/); await waitForTransition(page); await page.getByTestId("profile-stop").click(); await expect(page.locator(".profiler-badge")).toHaveCount(0); await expect(page.getByTestId("profile-status")).toContainText("navigations"); await expect(page.getByTestId("profile-export")).toBeEnabled(); }); test("opens and dismisses the compose sheet", async ({ page }) => { await page.goto("/inbox"); await page.getByRole("button", { name: "Compose" }).click(); await expect(page).toHaveURL(/\/compose$/); await expect( page.getByRole("heading", { name: "New message" }), ).toBeVisible(); await waitForTransition(page); await expect(page.locator("[data-native-sheet]")).toHaveAttribute( "data-native-sheet-breakpoint", "0.62", ); await expect(page.locator('[data-native-role="underlay"]')).toHaveAttribute( "aria-hidden", "true", ); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page.getByRole("heading", { name: "Messages" })).toBeVisible(); }); test("contains a sheet below the safe top and supports snap and content sizes", async ({ page, }) => { await page.goto("/inbox"); await page.getByRole("button", { name: "Compose" }).click(); await expect(page).toHaveURL(/\/compose$/); await waitForTransition(page); const routerView = page.locator(".nvr-router-view"); const activeLayer = page.locator('[data-native-role="active"]'); const frame = await routerView.boundingBox(); const layer = await activeLayer.boundingBox(); if (!frame || !layer) throw new Error("Sheet route did not render"); expect(layer.y).toBeGreaterThanOrEqual(frame.y + 7); expect(layer.y + layer.height).toBeLessThanOrEqual( frame.y + frame.height + 1, ); const handle = page.getByRole("slider", { name: "Resize or dismiss sheet", }); await handle.press("ArrowUp"); await expect(page.getByTestId("sheet-size")).toHaveText("100%"); await expect(page.locator("[data-native-sheet]")).toHaveAttribute( "data-native-sheet-breakpoint", "1", ); await page.getByRole("button", { name: "Fit content" }).click(); await expect(page.getByTestId("sheet-size")).toHaveText("Auto"); await expect(page.locator("[data-native-sheet]")).toHaveAttribute( "data-native-sheet-mode", "content", ); }); test("animates the partial surface immediately and preserves underlay geometry", async ({ page, }) => { await page.goto("/inbox"); await page.getByRole("button", { name: "Compose" }).dispatchEvent("click"); const opening = await page.evaluate(async () => { for (let frame = 0; frame < 60; frame += 1) { await new Promise((resolve) => requestAnimationFrame(resolve)); const routerView = document.querySelector( ".nvr-router-view--interactive", ); const surface = document.querySelector( '[data-native-role="to"] [data-native-sheet]', ); const progress = Number( routerView?.style.getPropertyValue("--native-progress") ?? 0, ); if (routerView && surface && progress >= 0.03) { const frameRect = routerView.getBoundingClientRect(); const surfaceRect = surface.getBoundingClientRect(); return { progress, frameBottom: frameRect.bottom, surfaceTop: surfaceRect.top, surfaceHeight: surfaceRect.height, }; } } return undefined; }); expect(opening).toBeDefined(); expect(opening!.surfaceTop).toBeLessThan(opening!.frameBottom); await waitForTransition(page); const settledSurfaceHeight = await page .locator("[data-native-sheet]") .evaluate((element) => element.getBoundingClientRect().height); expect(opening!.surfaceHeight).toBeCloseTo(settledSurfaceHeight, 0); const openUnderlay = await page .locator('[data-native-role="underlay"]') .evaluate((element) => { const matrix = new DOMMatrix(getComputedStyle(element).transform); const rect = element.getBoundingClientRect(); return { scale: matrix.a, top: rect.top }; }); expect(openUnderlay.scale).toBeCloseTo(0.96, 2); expect(openUnderlay.top).toBeGreaterThan(0); await page.getByRole("button", { name: "Cancel" }).dispatchEvent("click"); const closing = await page.evaluate(async () => { for (let frame = 0; frame < 60; frame += 1) { await new Promise((resolve) => requestAnimationFrame(resolve)); const routerView = document.querySelector( '.nvr-router-view--interactive[data-native-direction="back"]', ); const destination = document.querySelector( '[data-native-role="to"]', ); const progress = Number( routerView?.style.getPropertyValue("--native-progress") ?? 0, ); if (routerView && destination && progress >= 0.03) { const matrix = new DOMMatrix(getComputedStyle(destination).transform); return { progress, scale: matrix.a }; } } return undefined; }); expect(closing).toBeDefined(); expect(closing!.scale).toBeGreaterThanOrEqual(0.96); expect(closing!.scale).toBeLessThan(1); await waitForTransition(page); await expect(page).toHaveURL(/\/inbox$/); const restoredScale = await page .locator('[data-native-role="active"]') .evaluate( (element) => new DOMMatrix(getComputedStyle(element).transform).a, ); expect(restoredScale).toBeCloseTo(1, 3); await page.getByRole("button", { name: "Compose" }).dispatchEvent("click"); await page.waitForFunction(() => { const routerView = document.querySelector( '.nvr-router-view--interactive[data-native-presentation="sheet"]', ); const surface = document.querySelector( '[data-native-role="to"] [data-native-sheet]', ); return Boolean( routerView && surface && surface.getBoundingClientRect().top < routerView.getBoundingClientRect().bottom, ); }); await waitForTransition(page); const repeatedUnderlayScale = await page .locator('[data-native-role="underlay"]') .evaluate( (element) => new DOMMatrix(getComputedStyle(element).transform).a, ); expect(repeatedUnderlayScale).toBeCloseTo(0.96, 2); }); test("hands content overscroll to adjacent sheet breakpoints", async ({ page, }) => { await page.goto("/inbox"); await page.getByRole("button", { name: "Compose" }).click(); await expect(page).toHaveURL(/\/compose$/); await waitForTransition(page); const surface = page.locator("[data-native-sheet]"); const body = page.locator(".nvr-sheet__body"); const box = await body.boundingBox(); if (!box) throw new Error("Sheet scroll body did not render"); await body.evaluate((element) => { element.scrollTop = Math.min( 40, Math.max(0, element.scrollHeight - element.clientHeight - 10), ); }); await page.mouse.move(box.x + 100, box.y + box.height * 0.5); await page.mouse.wheel(0, 1_000); await page.mouse.wheel(0, 80); await expect(surface).toHaveAttribute("data-native-sheet-breakpoint", "0.62"); await page.waitForTimeout(280); await body.evaluate((element) => { element.scrollTop = element.scrollHeight; }); await page.mouse.move(box.x + 100, box.y + box.height * 0.7); await page.mouse.down(); await page.mouse.move(box.x + 100, box.y + box.height * 0.35, { steps: 10 }); await page.mouse.up(); await expect(surface).toHaveAttribute("data-native-sheet-breakpoint", "1"); await body.evaluate((element) => { element.scrollTop = 0; }); const expandedBox = await body.boundingBox(); if (!expandedBox) throw new Error("Expanded sheet body did not render"); await page.mouse.move( expandedBox.x + 100, expandedBox.y + expandedBox.height * 0.35, ); await page.mouse.down(); await page.mouse.move( expandedBox.x + 100, expandedBox.y + expandedBox.height * 0.55, { steps: 10 }, ); await page.mouse.up(); await expect(surface).toHaveAttribute("data-native-sheet-breakpoint", "0.62"); }); test("keeps the target live during a held component drag", async ({ page }) => { await page.goto("/inbox"); const row = page.locator(".conversation-row").first(); const box = await row.boundingBox(); if (!box) throw new Error("Conversation row did not render"); await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width * 0.35, box.y + box.height / 2, { steps: 12, }); await expect(page.locator('[data-native-role="to"]')).toBeVisible(); await page.mouse.up(); await expect(page.getByRole("heading", { name: "Maya Chen" })).toBeVisible(); }); test("holds an edge-back preview without committing the URL", async ({ page, }) => { await page.goto("/inbox"); await page.getByText("Maya Chen").last().click(); await expect(page).toHaveURL(/\/chat\/maya$/); const frame = await page.locator(".app-frame").boundingBox(); if (!frame) throw new Error("App frame did not render"); await page.mouse.move(frame.x + 2, frame.y + frame.height * 0.5); await page.mouse.down(); await page.mouse.move( frame.x + frame.width * 0.55, frame.y + frame.height * 0.5, { steps: 14 }, ); await expect(page.locator('[data-native-role="to"]')).toBeVisible(); await expect(page).toHaveURL(/\/chat\/maya$/); await page.mouse.up(); await expect(page.getByRole("heading", { name: "Messages" })).toBeVisible(); }); test("never previews a stale conversation after a cancelled back gesture", async ({ page, }) => { await page.goto("/inbox"); await page.getByText("Maya Chen").last().click(); await page.getByRole("button", { name: "Back" }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); await page.getByText("Noah Williams").last().click(); await expect(page).toHaveURL(/\/chat\/noah$/); await waitForTransition(page); const frame = await page.locator(".app-frame").boundingBox(); if (!frame) throw new Error("App frame did not render"); await page.mouse.move(frame.x + 2, frame.y + frame.height * 0.5); await page.mouse.down(); await page.mouse.move( frame.x + frame.width * 0.055, frame.y + frame.height * 0.5, { steps: 16 }, ); await page.mouse.up(); await waitForTransition(page); await expect(page).toHaveURL(/\/chat\/noah$/); await page.getByRole("button", { name: "Back" }).click(); const backTarget = page.locator('[data-native-role="to"]'); await expect( backTarget.getByRole("heading", { name: "Messages" }), ).toBeVisible(); await expect( backTarget.getByRole("heading", { name: "Maya Chen" }), ).toHaveCount(0); await waitForTransition(page); await expect(page).toHaveURL(/\/inbox$/); }); test("always opens compose as a vertical sheet after prior navigation", async ({ page, }) => { await page.goto("/inbox"); await page.getByText("Maya Chen").last().click(); await page.getByRole("button", { name: "Back" }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); await page.getByRole("link", { name: /Stories/ }).click(); await expect(page).toHaveURL(/\/stories$/); await waitForTransition(page); await page.getByRole("link", { name: /Inbox/ }).click(); await expect(page).toHaveURL(/\/inbox$/); await waitForTransition(page); await page.getByRole("button", { name: "Compose" }).click(); const routerView = page.locator(".nvr-router-view"); await expect(routerView).toHaveAttribute("data-native-presentation", "sheet"); await expect(routerView).toHaveAttribute("data-native-direction", "up"); const frame = await routerView.boundingBox(); const sheet = await page.locator('[data-native-role="to"]').boundingBox(); if (!frame || !sheet) throw new Error("Sheet transition did not render"); expect(Math.abs(frame.x - sheet.x)).toBeLessThan(3); await waitForTransition(page); await page.getByRole("button", { name: "Cancel" }).click(); }); test("drags a sheet down to dismiss it", async ({ page }) => { await page.goto("/inbox"); await page.getByRole("button", { name: "Compose" }).click(); await expect(page).toHaveURL(/\/compose$/); await waitForTransition(page); const sheet = await page.locator("[data-native-sheet]").boundingBox(); const handle = await page.locator(".nvr-sheet__handle").boundingBox(); if (!sheet || !handle) throw new Error("Sheet did not render"); await page.mouse.move(handle.x + handle.width / 2, handle.y + 12); await page.mouse.down(); await page.mouse.move( handle.x + handle.width / 2, handle.y + sheet.height * 0.75, { steps: 14 }, ); await page.mouse.up(); await expect(page.getByRole("heading", { name: "Messages" })).toBeVisible(); });