90 lines
3.8 KiB
TypeScript
90 lines
3.8 KiB
TypeScript
/**
|
|
* Mobile-Chrome geometry coverage for opaque content rendered by
|
|
* `PluginModalHost`. The fixture is installed through the real upload flow so
|
|
* the test exercises the packaged `host.openModal` contract on a phone.
|
|
*/
|
|
import { expect, test } from "../../fixtures/test-base";
|
|
import { installFixturePlugin, PLUGIN_ID } from "../../helpers/plugin-fixture";
|
|
|
|
test.describe("Mobile plugin modal content", () => {
|
|
test.afterEach(async ({ apiClient }) => {
|
|
await apiClient.rawRequest("DELETE", `/api/plugins/${PLUGIN_ID}`).catch(() => undefined);
|
|
});
|
|
|
|
test("keeps long plugin content scrollable and dismissible", async ({ testPage, prCapture }) => {
|
|
test.setTimeout(120_000);
|
|
|
|
await installFixturePlugin(testPage);
|
|
await testPage.goto("/");
|
|
await testPage.reload();
|
|
await testPage.goto("/plugins/e2e-hello");
|
|
await expect(testPage.locator("#hello-plugin-page")).toBeVisible({ timeout: 15_000 });
|
|
await testPage.keyboard.press("ControlOrMeta+Shift+J");
|
|
|
|
const dialog = testPage.getByRole("dialog", { name: "Demo Modal" });
|
|
const body = dialog.locator('[data-testid^="plugin-modal-body-"]');
|
|
const title = dialog.locator('[data-slot="dialog-title"]');
|
|
const close = dialog.locator('[data-slot="dialog-close"]');
|
|
const finalAction = dialog.getByTestId("hello-long-modal-final-action");
|
|
await expect(dialog).toBeVisible();
|
|
await expect(body).toHaveCount(1);
|
|
await expect(finalAction).toBeVisible();
|
|
await dialog.evaluate(async (element) => {
|
|
const animations = element.getAnimations({ subtree: true }).filter((animation) => {
|
|
if (animation.playState !== "running") return false;
|
|
const iterations = animation.effect?.getComputedTiming().iterations;
|
|
return typeof iterations === "number" && Number.isFinite(iterations);
|
|
});
|
|
await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined)));
|
|
});
|
|
|
|
const metrics = await body.evaluate((element) => {
|
|
const node = element as HTMLElement;
|
|
return { clientHeight: node.clientHeight, scrollHeight: node.scrollHeight };
|
|
});
|
|
expect(metrics.scrollHeight).toBeGreaterThan(metrics.clientHeight);
|
|
|
|
const viewport = await testPage.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
|
for (const [label, locator] of [
|
|
["dialog", dialog],
|
|
["title", title],
|
|
["close", close],
|
|
] as const) {
|
|
const box = await locator.boundingBox();
|
|
if (!box) throw new Error(`${label} has no layout box`);
|
|
expect(box.x).toBeGreaterThanOrEqual(0);
|
|
expect(box.y).toBeGreaterThanOrEqual(0);
|
|
expect(box.x + box.width).toBeLessThanOrEqual(viewport.width);
|
|
expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
|
|
}
|
|
|
|
await body.evaluate((element) => {
|
|
const node = element as HTMLElement;
|
|
node.scrollTop = node.scrollHeight;
|
|
});
|
|
await expect
|
|
.poll(() => body.evaluate((element) => (element as HTMLElement).scrollTop))
|
|
.toBeGreaterThan(0);
|
|
await expect(finalAction).toBeInViewport();
|
|
const finalBox = await finalAction.boundingBox();
|
|
if (!finalBox) throw new Error("final plugin action has no layout box");
|
|
expect(finalBox.y).toBeGreaterThanOrEqual(0);
|
|
expect(finalBox.y + finalBox.height).toBeLessThanOrEqual(viewport.height);
|
|
await finalAction.tap();
|
|
await expect(finalAction).toHaveText("Plugin modal action complete");
|
|
if (prCapture.capturing) {
|
|
await prCapture.screenshot("long-plugin-modal", {
|
|
caption:
|
|
"Mobile plugin modal content stays inside one scroll body with a reachable final action.",
|
|
});
|
|
}
|
|
|
|
await close.tap();
|
|
await expect(dialog).not.toBeVisible();
|
|
const overflow = await testPage.evaluate(
|
|
() => document.documentElement.scrollWidth > innerWidth,
|
|
);
|
|
expect(overflow).toBe(false);
|
|
});
|
|
});
|