Add experiments folder. Add usage.md and SKILL.md. Fix Sheet interactions and add dynamic sheet mode.
This commit is contained in:
@@ -389,6 +389,69 @@ test("manually unloads an inactive route through the public API demo", async ({
|
||||
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,
|
||||
}) => {
|
||||
@@ -421,10 +484,213 @@ test("opens and dismisses the compose sheet", async ({ page }) => {
|
||||
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<HTMLElement>(
|
||||
".nvr-router-view--interactive",
|
||||
);
|
||||
const surface = document.querySelector<HTMLElement>(
|
||||
'[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<HTMLElement>(
|
||||
'.nvr-router-view--interactive[data-native-direction="back"]',
|
||||
);
|
||||
const destination = document.querySelector<HTMLElement>(
|
||||
'[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<HTMLElement>(
|
||||
'.nvr-router-view--interactive[data-native-presentation="sheet"]',
|
||||
);
|
||||
const surface = document.querySelector<HTMLElement>(
|
||||
'[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();
|
||||
@@ -528,13 +794,16 @@ test("always opens compose as a vertical sheet after prior navigation", async ({
|
||||
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 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(
|
||||
sheet.x + sheet.width / 2,
|
||||
sheet.y + sheet.height * 0.55,
|
||||
handle.x + handle.width / 2,
|
||||
handle.y + sheet.height * 0.75,
|
||||
{ steps: 14 },
|
||||
);
|
||||
await page.mouse.up();
|
||||
|
||||
Reference in New Issue
Block a user