Add prettier. Format.
This commit is contained in:
@@ -1,400 +1,542 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
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')
|
||||
__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')) {
|
||||
if (view.classList.contains("nvr-router-view--interactive")) {
|
||||
state.__nvrEvents?.push({
|
||||
direction: view.getAttribute('data-native-direction'),
|
||||
presentation: view.getAttribute('data-native-presentation'),
|
||||
})
|
||||
direction: view.getAttribute("data-native-direction"),
|
||||
presentation: view.getAttribute("data-native-presentation"),
|
||||
});
|
||||
}
|
||||
})
|
||||
state.__nvrObserver.observe(view, { attributes: true })
|
||||
})
|
||||
});
|
||||
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 ?? [])
|
||||
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/)
|
||||
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()
|
||||
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.3, y, { steps: 10 });
|
||||
await page.waitForTimeout(90);
|
||||
}
|
||||
await page.mouse.move(surface.x + surface.width * 0.27, y)
|
||||
await page.mouse.up()
|
||||
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("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("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' })
|
||||
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 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$/)
|
||||
})
|
||||
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()
|
||||
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$/)
|
||||
})
|
||||
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')
|
||||
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 })
|
||||
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()
|
||||
})
|
||||
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')
|
||||
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')
|
||||
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()
|
||||
})
|
||||
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')
|
||||
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, 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)
|
||||
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')
|
||||
})
|
||||
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()
|
||||
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 })
|
||||
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)
|
||||
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)
|
||||
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!)
|
||||
})
|
||||
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$/)
|
||||
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 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)
|
||||
})
|
||||
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)
|
||||
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)
|
||||
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))
|
||||
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)
|
||||
})
|
||||
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')
|
||||
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)
|
||||
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()
|
||||
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 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!)
|
||||
})
|
||||
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)
|
||||
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 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)
|
||||
})
|
||||
await expect(page.getByTestId("unload-stories")).toHaveText(
|
||||
"Unloaded 1 Stories view",
|
||||
);
|
||||
await expect(page.getByTestId("stories-view")).toHaveCount(0);
|
||||
});
|
||||
|
||||
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()
|
||||
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.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()
|
||||
})
|
||||
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 page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Messages' })).toBeVisible()
|
||||
})
|
||||
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("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("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)
|
||||
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$/)
|
||||
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$/)
|
||||
})
|
||||
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)
|
||||
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()
|
||||
})
|
||||
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()
|
||||
})
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user