Site: scroll tour with live clips, beat-reactive desk and speaker, road playground, night wave, film on an isometric screen

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 00:33:25 -04:00
1 parent b6b882a8a1
commit 70cd57bd5d
46 files changed
+2708 -169

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
import { expect, test, type Page } from "@playwright/test";
const PAGE = "/";
/** Scrolls to the film and mutes it for autoplay. */
async function reach(page: Page): Promise<string[]> {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(String(e)));
page.on("console", (m) => {
if (m.type() === "error") errors.push(m.text());
});
await page.goto(PAGE);
await page.locator("#film").scrollIntoViewIfNeeded();
await expect(page.locator("#film")).toHaveClass(/seen/);
await page.evaluate(() => {
const video = document.querySelector<HTMLVideoElement>("#film video")!;
video.muted = true;
Object.assign(window, { filmVideo: video });
});
return errors;
}
const box = (page: Page) => page.locator("#film video").evaluate((v) => v.getBoundingClientRect().toJSON());
test("the film flattens to play and returns to its face", async ({ page }) => {
const errors = await reach(page);
await expect(page.locator("#film video")).toHaveAttribute("preload", "metadata");
const face = await box(page);
const play = page.getByRole("button", { name: "Play the film" });
await play.click();
await expect.poll(() => page.locator("#film video").evaluate((v: HTMLVideoElement) => v.paused)).toBe(false);
await expect.poll(() => page.locator("#film video").evaluate((v) => v.getAnimations().length)).toBe(0);
const flat = await box(page);
expect(flat.width / flat.height).toBeCloseTo(16 / 9, 1);
expect(flat.width).toBeGreaterThan(page.viewportSize()!.width * 0.7);
expect(flat.width).toBeGreaterThan(face.width * 1.5);
await expect(page.locator("#film video")).toHaveJSProperty("controls", true);
await page.keyboard.press("Escape");
await expect(play).toBeFocused();
await expect.poll(() => page.locator("#film video").evaluate((v) => v.getAnimations().length)).toBe(0);
const back = await box(page);
expect(back.width).toBeCloseTo(face.width, 0);
expect(back.height).toBeCloseTo(face.height, 0);
const state = await page.evaluate(() => {
const video = document.querySelector<HTMLVideoElement>("#film video")!;
return { same: video === (window as unknown as { filmVideo: HTMLVideoElement }).filmVideo, paused: video.paused, at: video.currentTime };
});
expect(state.same).toBe(true);
expect(state.paused).toBe(true);
expect(state.at).toBeGreaterThan(0);
expect(errors).toEqual([]);
});
test("with reduced motion the film plays flat at once", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
const errors = await reach(page);
await page.getByRole("button", { name: "Play the film" }).click();
const video = page.locator("#film video");
expect(await video.evaluate((v) => [v.getAnimations().length, getComputedStyle(v).transform])).toEqual([0, "none"]);
await expect.poll(() => video.evaluate((v: HTMLVideoElement) => v.paused)).toBe(false);
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "Play the film" })).toBeFocused();
expect(errors).toEqual([]);
});
+111
View File
@@ -0,0 +1,111 @@
import { expect, test, type Page } from "@playwright/test";
const PAGE = "/";
const DESK = { light: "rgb(230, 230, 226)", dark: "rgb(58, 58, 58)" };
let errors: string[] = [];
test.beforeEach(async ({ page }) => {
errors = [];
page.on("pageerror", (e) => errors.push(String(e)));
});
test.afterEach(() => expect(errors).toEqual([]));
const open = async (page: Page) => {
await page.goto(PAGE);
await expect(page.locator(".model .block").first()).toBeAttached();
};
/** The hero desk's top face colour. */
const desk = (page: Page) =>
page.evaluate(() => {
const top = document.querySelector(".model .block .face.top")!;
return getComputedStyle(top).fill;
});
const lamp = (page: Page) => page.getByRole("button", { name: "Switch to dark" });
test("the lamp turns the page dark in a wave", async ({ page }) => {
await open(page);
expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("light");
expect(await desk(page)).toBe(DESK.light);
// Read the nearest and farthest hero blocks mid-wave.
const mid = await page.evaluate(async () => {
const tops = [...document.querySelectorAll<SVGGElement>(".model .block")];
document.querySelector<HTMLButtonElement>(".night")!.click();
const wave = (g: SVGGElement) => parseFloat(g.style.getPropertyValue("--wave"));
tops.sort((a, b) => wave(a) - wave(b));
const [near, far] = [tops[0], tops[tops.length - 1]];
const fill = (g: SVGGElement) => getComputedStyle(g.querySelector(".face.top")!).fill;
// Sample between the near landing and the far start.
const gap = (wave(near) + 0.2 + wave(far)) / 2;
await new Promise((r) => setTimeout(r, gap * 1000));
return {
steps: [wave(near), wave(far)],
near: [fill(near), near.style.getPropertyValue("--tone-dark")],
far: [fill(far), far.style.getPropertyValue("--tone")],
};
});
expect(mid.steps[0] + 0.25).toBeLessThan(mid.steps[1]);
expect(mid.steps[1]).toBeLessThanOrEqual(1);
const rgb = (hex: string) => `rgb(${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`;
expect(mid.near[0]).toBe(rgb(mid.near[1]));
expect(mid.far[0]).toBe(rgb(mid.far[1]));
await expect.poll(() => desk(page), { timeout: 1200 }).toBe(DESK.dark);
expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark");
await expect(page.getByRole("button", { name: "Switch to light" })).toBeAttached();
await expect(page.locator(".night-sky")).toHaveCount(0);
await expect(page.locator("html")).toHaveCSS("background-color", "rgb(37, 37, 37)");
});
test("dark text keeps 4.5:1 against the page", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await open(page);
const worst = await page.evaluate(() => {
const parse = (c: string) => c.match(/[\d.]+/g)!.slice(0, 3).map(Number);
const lum = (c: string) => {
const [r, g, b] = parse(c).map((v) => {
const s = v / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const page = lum(getComputedStyle(document.documentElement).backgroundColor);
const ratios = [...document.querySelectorAll<HTMLElement>("h1, h2, h3, p, a, li, button")]
.filter((el) => el.textContent?.trim() && getComputedStyle(el).color !== "rgba(0, 0, 0, 0)")
.map((el) => {
const text = lum(getComputedStyle(el).color);
return { ratio: (Math.max(text, page) + 0.05) / (Math.min(text, page) + 0.05), text: el.textContent };
});
return ratios.sort((a, b) => a.ratio - b.ratio)[0];
});
expect(worst.ratio, String(worst.text)).toBeGreaterThanOrEqual(4.5);
});
test("the system scheme sets the first theme; the lamp's choice stays", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await open(page);
expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark");
expect(await desk(page)).toBe(DESK.dark);
await page.emulateMedia({ colorScheme: "light" });
await expect.poll(() => page.evaluate(() => document.documentElement.dataset.theme)).toBe("light");
await page.emulateMedia({ reducedMotion: "reduce" });
await lamp(page).click();
expect(await desk(page)).toBe(DESK.dark);
await page.emulateMedia({ colorScheme: "light" });
await page.reload();
expect(await page.evaluate(() => document.documentElement.dataset.theme)).toBe("dark");
});
test("reduced motion switches at once", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce", colorScheme: "light" });
await open(page);
await lamp(page).click();
expect(await desk(page)).toBe(DESK.dark);
await expect(page.locator(".night-sky")).toHaveCount(0);
});
+92
View File
@@ -0,0 +1,92 @@
import { expect, test, type Page } from "@playwright/test";
const TREE = "Open a Markdown file's history in ElyTree.";
/** Opens the board and waits until it has grown. */
async function open(page: Page): Promise<string[]> {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
page.on("console", (m) => m.type() === "error" && errors.push(m.text()));
await page.goto("/");
await page.locator("#roads").scrollIntoViewIfNeeded();
await expect(page.locator("#roads .stage")).not.toHaveClass(/grow/, { timeout: 10_000 });
return errors;
}
/** Screen centre of a road cell. */
async function cell(page: Page, at: string) {
const box = (await page.locator(`#roads [data-cell="${at}"]:not(.file)`).boundingBox())!;
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
}
const mark = (page: Page) => page.getByRole("button", { name: "ElyMark file" });
/** Screen point of a grid spot on the desk top. */
const desk = (page: Page, x: number, y: number) =>
page.evaluate(([x, y]) => {
const svg = document.querySelector<SVGSVGElement>("#roads .stage")!;
const p = new DOMPoint((x - y) * Math.cos(Math.PI / 6) * 28, ((x + y) / 2 - 0.6) * 28).matrixTransform(svg.getScreenCTM()!);
return { x: p.x, y: p.y };
}, [x, y]);
test("dragging Mark's file down its road reaches ElyTree", async ({ page }) => {
const errors = await open(page);
const from = (await mark(page).boundingBox())!;
const to = await cell(page, "14,12");
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
await page.mouse.down();
await page.mouse.move(to.x, to.y, { steps: 12 });
await page.mouse.up();
await expect(mark(page)).toHaveAttribute("data-cell", "14,12");
await expect(page.locator("#roads .news")).toHaveText(TREE);
await expect(page.locator('#roads [data-cell="14,12"]:not(.file)')).toHaveCSS("--tone", "#3d7fe5");
expect(errors).toEqual([]);
});
test("a file dragged onto empty desk stays on its road", async ({ page }) => {
const errors = await open(page);
const from = (await mark(page).boundingBox())!;
const away = await desk(page, 10.5, 8.5);
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
await page.mouse.down();
await page.mouse.move(away.x, away.y);
await page.mouse.up();
await page.waitForTimeout(500);
await expect(mark(page)).toHaveAttribute("data-cell", "5,10");
await expect(page.locator("#roads .news")).toHaveText("");
expect(errors).toEqual([]);
});
test("arrow keys walk Mark's file to ElyTree, then reset", async ({ page }) => {
const errors = await open(page);
await mark(page).focus();
for (const key of ["ArrowDown", "ArrowDown", ...Array(9).fill("ArrowRight")])
await page.keyboard.press(key);
await expect(mark(page)).toHaveAttribute("data-cell", "14,12");
await expect(page.locator("#roads .news")).toHaveText(TREE);
await page.getByRole("button", { name: "Put the files back" }).click();
await expect(mark(page)).toHaveAttribute("data-cell", "5,10");
await expect(page.locator("#roads .news")).toHaveText("");
expect(errors).toEqual([]);
});
test.describe("reduced motion", () => {
test.use({ reducedMotion: "reduce" });
test("files stack at ElyPDF and the line says why", async ({ page }) => {
const errors = await open(page);
const doc = page.getByRole("button", { name: "ElyDoc file" });
await doc.focus();
await page.keyboard.press("ArrowDown");
for (let i = 0; i < 13; i++) await page.keyboard.press("ArrowRight");
await page.keyboard.press("ArrowUp");
await expect(doc).toHaveAttribute("data-cell", "15,5");
await expect(page.locator("#roads .news")).toHaveText("Export to PDF, open it in ElyPDF.");
const show = page.getByRole("button", { name: "ElyShow file" });
await show.focus();
for (const key of ["ArrowDown", "ArrowDown", "ArrowRight", "ArrowRight", "ArrowRight", "ArrowRight", "ArrowRight", "ArrowUp"])
await page.keyboard.press(key);
await expect(show).toHaveAttribute("data-cell", "15,5");
expect(errors).toEqual([]);
});
});
+116
View File
@@ -0,0 +1,116 @@
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)));
await page.goto("/");
await page.locator(".tour .model").scrollIntoViewIfNeeded();
await button(page).hover();
await page.evaluate(() =>
Promise.all(document.querySelector(".tour .model")!.getAnimations({ subtree: true }).map((a) => a.finished)),
);
});
test.afterEach(() => expect(errors).toEqual([]));
const button = (page: Page) => page.locator(".speaker-button");
const paused = (page: Page) => page.evaluate(() => document.querySelector<HTMLAudioElement>(".tour audio")!.paused);
/** Screen top of each building's first top face. */
const tops = (page: Page) =>
page.evaluate(
(apps) =>
apps.map((n) => document.querySelector(`.tour .block[data-part="${n}"] .face.top`)!.getBoundingClientRect().top),
APPS,
);
/** Whether anything dances: hops, road light or a moving woofer. */
const dancing = (page: Page) =>
page.evaluate(() => {
const hops = [...document.querySelectorAll<SVGGElement>(".tour .block")].filter((g) =>
g.style.getPropertyValue("--hop"),
);
const moved = [...document.querySelectorAll<SVGGElement>(".tour .woofer")].filter((g) => g.style.translate);
return hops.length + document.querySelectorAll(".tour .pulse").length + moved.length;
});
test("the speaker plays and pauses the music", async ({ page }) => {
await expect(button(page)).toHaveAccessibleName("Play music");
await button(page).click();
await expect.poll(() => paused(page)).toBe(false);
await expect(button(page)).toHaveAccessibleName("Pause music");
await button(page).click();
await expect.poll(() => paused(page)).toBe(true);
await expect(button(page)).toHaveAccessibleName("Play music");
});
test("buildings hop on the beat; all six on the bar", async ({ page }) => {
const rest = await tops(page);
await button(page).click();
await page.waitForFunction(
([apps, rest]) =>
apps.every(
(n, i) =>
document.querySelector(`.tour .block[data-part="${n}"] .face.top`)!.getBoundingClientRect().top < rest[i] - 10,
),
[APPS, rest] as const,
{ polling: "raf", timeout: 6000 },
);
await page.waitForFunction(
([apps, rest]) =>
apps.filter(
(n, i) =>
document.querySelector(`.tour .block[data-part="${n}"] .face.top`)!.getBoundingClientRect().top < rest[i] - 10,
).length === 1,
[APPS, rest] as const,
{ polling: "raf", timeout: 6000 },
);
});
test("paused, nothing moves", async ({ page }) => {
const rest = await tops(page);
await button(page).click();
await expect.poll(() => dancing(page)).toBeGreaterThan(0);
await button(page).click();
await expect.poll(() => paused(page)).toBe(true);
await expect.poll(() => dancing(page)).toBe(0);
for (let i = 0; i < 4; i++) {
expect(await dancing(page)).toBe(0);
expect(await tops(page)).toEqual(rest);
await page.waitForTimeout(150);
}
});
test.describe("reduced motion", () => {
test.use({ reducedMotion: "reduce" });
test("music plays, buildings stay, the woofer stays out", async ({ page }) => {
const rest = await tops(page);
await button(page).click();
await expect.poll(() => paused(page)).toBe(false);
await page.waitForTimeout(1200);
expect(await dancing(page)).toBe(0);
expect(await tops(page)).toEqual(rest);
const push = await page.locator(".tour .woofer").first().evaluate((g) => getComputedStyle(g).translate);
expect(push).not.toBe("none");
});
});
test("the desk cell under the pointer rises", async ({ page }) => {
const at = await page.evaluate(() => {
const svg = document.querySelector<SVGSVGElement>(".tour .stage")!;
const [cos, sin, unit, top] = [Math.cos(Math.PI / 6), 0.5, 28, 0.6];
const p = new DOMPoint((11.5 - 9.5) * cos * unit, ((11.5 + 9.5) * sin - top) * unit).matrixTransform(
svg.getScreenCTM()!,
);
return { x: p.x, y: p.y };
});
await page.mouse.move(at.x, at.y);
await expect(page.locator(".tour .tile")).toHaveCount(1);
await page.mouse.move(at.x, at.y - 600);
await expect(page.locator(".tour .tile")).toHaveCount(0);
});
+15 -12
View File
@@ -61,6 +61,7 @@ test.describe("English locale", () => {
await page.mouse.move(5, 500);
await expect(lang).toHaveCSS("opacity", "0");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await expect(lang).toBeFocused();
await expect(lang).toHaveCSS("opacity", "1");
});
@@ -79,9 +80,9 @@ test.describe("English locale", () => {
test("links point at the release and the repository", async ({ page }) => {
await page.goto("/");
await expect(page.locator(".region .get")).toHaveCount(APPS.length);
for (const app of APPS) {
await expect(page.locator(`#${app.toLowerCase()} .get`)).toHaveAttribute("href", dmg(app));
await expect(page.locator(".tour .get")).toHaveCount(APPS.length);
for (const [i, app] of APPS.entries()) {
await expect(page.locator(".tour .get").nth(i)).toHaveAttribute("href", dmg(app));
}
await expect(page.locator("#download ul a")).toHaveText([...APPS, TEXT.en.source]);
for (const [i, app] of APPS.entries()) {
@@ -103,17 +104,19 @@ test.describe("English locale", () => {
}
});
test("region downloads show on hover or focus", async ({ page }) => {
await page.goto("/");
for (const app of APPS) {
const region = page.locator(`#${app.toLowerCase()}`);
const get = region.locator(".get");
await region.scrollIntoViewIfNeeded();
await page.mouse.move(0, 0);
test("each app's copy arrives at its anchor; its download shows on intent", async ({ page }) => {
for (const [i, app] of APPS.entries()) {
await page.goto(`/#${app.toLowerCase()}`);
const article = page.locator(".copy article").nth(i);
await expect(article).toHaveClass(/current/);
await expect(page.locator(".copy article.current")).toHaveCount(1);
await expect(page.locator(".rail a").nth(i)).toHaveAttribute("aria-current", "step");
const get = article.locator(".get");
await page.mouse.move(1000, 5);
await expect(get).toHaveCSS("opacity", "0");
await region.locator("h2").hover();
await article.locator("h2").hover();
await expect(get).toHaveCSS("opacity", "1");
await page.locator(".lang").hover();
await page.mouse.move(1000, 5);
await expect(get).toHaveCSS("opacity", "0");
await get.focus();
await expect(get).toHaveCSS("opacity", "1");
+11 -8
View File
@@ -5,12 +5,13 @@ const APPS = ["ElyDoc", "ElyTable", "ElyShow", "ElyPDF", "ElyTree", "ElyMark"];
test.use({ reducedMotion: "reduce" });
/** Screen top of the building's first block, in SVG pixels. */
/** Building top over the desk top, in SVG pixels; camera-free. */
const top = (page: Page, name: string) =>
page.evaluate((n) => {
const svg = document.querySelector<SVGSVGElement>(".hero .stage")!;
const svg = document.querySelector<SVGSVGElement>(".model .stage")!;
const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width;
return (document.querySelector(`.block[data-part="${n}"]`)!.getBoundingClientRect().top + scrollY) / scale;
const at = (sel: string) => document.querySelector(sel)!.getBoundingClientRect().top;
return (at(`.block[data-part="${n}"]`) - at('.block[data-part=""]')) / scale;
}, name);
/** A point that hits the building; `low` picks the lowest. */
@@ -38,7 +39,7 @@ const spot = (page: Page, name: string, low = false) =>
[name, low] as const,
);
const caption = (page: Page, name: string) => page.locator(".caption", { hasText: name });
const caption = (page: Page, name: string) => page.locator(".tour .caption", { hasText: name });
let errors: string[] = [];
@@ -46,7 +47,7 @@ test.beforeEach(async ({ page }) => {
errors = [];
page.on("pageerror", (e) => errors.push(String(e)));
await page.goto("/");
await expect(page.locator(".hero .hit-face:not(.ground)").first()).toBeAttached();
await expect(page.locator(".model .hit-face:not(.ground)").first()).toBeAttached();
});
test.afterEach(() => expect(errors).toEqual([]));
@@ -59,7 +60,7 @@ test("six links in reading order; reduced motion skips growth", async ({ page })
test("focus lifts one cell, shows the name, survives the mouse", async ({ page }) => {
const rest = await top(page, "ElyDoc");
for (let i = 0; i < 3; i++) await page.keyboard.press("Tab");
await page.locator(".doors a").first().focus();
await expect(page.locator(".doors a").first()).toBeFocused();
expect(rest - (await top(page, "ElyDoc"))).toBeCloseTo(CELL, 0);
await expect(caption(page, "ElyDoc")).toHaveCSS("opacity", "1");
@@ -89,7 +90,7 @@ test("hover at the bottom edge holds still", async ({ page }) => {
test("the hit owner is the block you see", async ({ page }) => {
const wrong = await page.evaluate(() => {
const r = document.querySelector(".hero .stage")!.getBoundingClientRect();
const r = document.querySelector(".model .stage")!.getBoundingClientRect();
const out: string[] = [];
const owner = (x: number, y: number) => {
const face = document.elementsFromPoint(x, y).find((e) => e.classList.contains("face"));
@@ -120,6 +121,8 @@ test("a primary press sinks one cell; only a click starting on it opens", async
await page.evaluate(() => history.replaceState(null, "", "/"));
await page.evaluate(() => scrollTo(0, 0));
// The camera settles on the next frame.
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))));
const again = await spot(page, "ElyShow");
await page.mouse.click(again.x, again.y, { button: "right" });
await page.mouse.move(5, 5);
@@ -131,7 +134,7 @@ test("a primary press sinks one cell; only a click starting on it opens", async
test("holding Enter sinks the focused building until release", async ({ page }) => {
const rest = await top(page, "ElyDoc");
for (let i = 0; i < 3; i++) await page.keyboard.press("Tab");
await page.locator(".doors a").first().focus();
await page.keyboard.down("Enter");
expect(await top(page, "ElyDoc")).toBeCloseTo(rest, 0);
await expect(page).toHaveURL(/#elydoc$/);
+111
View File
@@ -0,0 +1,111 @@
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);
});
});