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/) } 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('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 page.getByRole('button', { name: 'Cancel' }).click() await expect(page.getByRole('heading', { name: 'Messages' })).toBeVisible() }) 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() const sheet = await page.locator('.sheet-screen').boundingBox() if (!sheet) throw new Error('Sheet did not render') await page.mouse.move(sheet.x + sheet.width / 2, sheet.y + 12) await page.mouse.down() await page.mouse.move(sheet.x + sheet.width / 2, sheet.y + sheet.height * 0.55, { steps: 14 }) await page.mouse.up() await expect(page.getByRole('heading', { name: 'Messages' })).toBeVisible() })