Files
ElyOffice/site/e2e/tour.spec.ts
T

112 lines
4.8 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const APPS = ["ElyDoc", "ElyTable", "ElyShow", "ElyPDF", "ElyTree", "ElyMark"];
let errors: string[] = [];
test.beforeEach(async ({ page }) => {
errors = [];
page.on("pageerror", (e) => errors.push(String(e)));
});
test.afterEach(() => expect(errors).toEqual([]));
const frame = (page: Page) => page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
/** Scrolls to `spans` stage heights into the tour. */
async function at(page: Page, spans: number) {
await page.evaluate((s) => {
document.documentElement.style.scrollBehavior = "auto";
scrollTo(0, s * document.querySelector<HTMLElement>(".pin")!.clientHeight);
}, spans);
await frame(page);
}
/** Largest gap in px between clip and face corners. */
const drift = (page: Page, i: number) =>
page.evaluate((i) => {
const video = document.querySelectorAll<HTMLVideoElement>(".clip")[i];
const world = video.parentElement!;
const host = world.parentElement!.getBoundingClientRect();
const m = new DOMMatrix(getComputedStyle(world).transform).multiply(new DOMMatrix(getComputedStyle(video).transform));
const [w, h] = [video.offsetWidth, video.offsetHeight];
const clip = [[0, 0], [w, 0], [w, h], [0, h]].map(([x, y]) => m.transformPoint(new DOMPoint(x, y)));
const row = (k: number) => document.querySelector<SVGGElement>(`.block.slab[data-app="${i}"][data-row="${k}"]`)!;
const ctm = row(0).ownerSVGElement!.getScreenCTM()!;
const corners = (g: SVGGElement) =>
g.querySelector(".face.left")!.getAttribute("points")!.split(" ").map((p) => {
const [x, y] = p.split(",").map(Number);
return new DOMPoint(x, y).matrixTransform(ctm);
});
// Left face points: bottom-left, bottom-right, top-right, top-left.
const [bl, br] = corners(row(0));
const [, , tr, tl] = corners(row(3));
const face = [tl, tr, br, bl];
return Math.max(...clip.map((p, k) => Math.hypot(p.x + host.left - face[k].x, p.y + host.top - face[k].y)));
}, i);
test("each clip stays on its screen face, held or gliding", async ({ page }) => {
await page.goto("/");
for (const [i] of APPS.entries()) {
for (const s of [i + 1, i + 1.5]) {
await at(page, s);
await page.waitForTimeout(800);
expect(await drift(page, i), `${APPS[i]} at ${s}`).toBeLessThan(1);
}
}
});
test("only the current app's clip loads and plays", async ({ page }) => {
await page.goto("/");
await at(page, 3);
const clips = page.locator(".clip");
await expect.poll(() => clips.nth(2).evaluate((v: HTMLVideoElement) => !v.paused && v.currentTime > 0)).toBe(true);
for (const i of [0, 1, 3, 4, 5]) await expect(clips.nth(i).locator("source")).toHaveCount(0);
await at(page, 4);
await expect(clips.nth(2).locator("source")).toHaveCount(0);
await expect.poll(() => clips.nth(3).evaluate((v: HTMLVideoElement) => !v.paused)).toBe(true);
await at(page, 0);
await expect(page.locator(".clip source")).toHaveCount(0);
});
test("the rail scrolls to an app and marks it", async ({ page }) => {
await page.goto("/");
await at(page, 1);
await page.locator(".rail a").nth(4).click();
await expect(page.locator(".rail a").nth(4)).toHaveAttribute("aria-current", "step");
await expect(page.locator(".copy article.current h2")).toHaveText("ElyTree");
});
test.describe("reduced motion", () => {
test.use({ reducedMotion: "reduce" });
test("clips wait for a press; the camera jumps", async ({ page }) => {
await page.goto("/#elyshow");
const clip = page.locator(".clip").nth(2);
await expect(page.locator(".copy article.current h2")).toHaveText("ElyShow");
await expect(clip).toHaveAttribute("poster", "/clips/show.jpg");
expect(await clip.evaluate((v: HTMLVideoElement) => v.paused)).toBe(true);
const held = await page.evaluate(() => getComputedStyle(document.querySelector(".world")!).transform);
await at(page, 3.4);
expect(await page.evaluate(() => getComputedStyle(document.querySelector(".world")!).transform)).toBe(held);
await page.locator(".copy article.current .toggle-clip").click();
await expect.poll(() => clip.evaluate((v: HTMLVideoElement) => !v.paused)).toBe(true);
await expect(page.locator(".copy article.current .toggle-clip")).toHaveText("Pause");
});
});
test.describe("phone", () => {
test.use({ viewport: { width: 390, height: 844 } });
test("no sideways scroll; copy sits below the stage", async ({ page }) => {
await page.goto("/");
for (const s of [0, 1, 2.5, 4, 6, 7]) {
await at(page, s);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
}
await at(page, 2);
const copy = await page.locator(".copy article.current").boundingBox();
expect(copy!.y).toBeGreaterThan(844 * 0.55);
});
});