import { expect } from "@std/expect"; import { renderTile } from "./tile-render.ts"; import { reconcileTiles } from "./tiles-client.ts"; import type { TileView } from "./types.ts"; function rendering(...tiles: [string, TileView][]): HTMLElement[] { const template = document.createElement("template"); template.innerHTML = tiles.map(([label, view]) => renderTile(label, view)) .join(""); return [...template.content.children] as HTMLElement[]; } // Runs `test` against a page holding the tiles, and removes them afterwards. function withBoard( tiles: [string, TileView][], test: (container: HTMLElement) => void, ): void { const container = document.createElement("div"); container.append(...rendering(...tiles)); document.body.append(container); try { test(container); } finally { container.remove(); } } // Rows as the recent commits tile renders them, each titled with the same pull // request number, which its title and arrow both link to. function runRows(titles: string[]): string { return `
${ titles.map((title) => `
${title} (#1)↗
` ).join("") }
`; } Deno.test("an update keeps an unchanged tile's element and replaces a changed tile where it stands", () => { withBoard([ ["labs ci", { status: "good", value: "passing" }], ["loom ci", { status: "good", value: "passing" }], ["dau", { status: "good", value: "12" }], ], (container) => { const [labs, loom, dau] = [...container.children]; reconcileTiles( container, rendering( ["labs ci", { status: "good", value: "passing" }], ["loom ci", { status: "bad", value: "failure" }], ["dau", { status: "good", value: "12" }], ), ); const [nextLabs, nextLoom, nextDau] = [...container.children]; expect(container.children).toHaveLength(3); expect(nextLabs).toBe(labs); expect(nextLoom).not.toBe(loom); expect(nextLoom.className).toBe("tile bad"); expect(nextLoom.querySelector(".big")?.textContent).toBe("failure"); expect(nextDau).toBe(dau); }); }); Deno.test("an update adds, removes and reorders tiles by label, keeping the elements of tiles it keeps", () => { withBoard([ ["labs ci", { status: "good" }], ["loom ci", { status: "good" }], ["dau", { status: "good" }], ], (container) => { const [labs, , dau] = [...container.children]; reconcileTiles( container, rendering( ["dau", { status: "good" }], ["labs ci", { status: "good" }], ["model spend", { status: "warn" }], ), ); const [first, second, third] = [...container.children]; expect(container.children).toHaveLength(3); expect(first).toBe(dau); expect(second).toBe(labs); expect(third.getAttribute("data-tile-label")).toBe("model spend"); }); }); Deno.test("an update keeps focus and scroll position in a tile when a tile before it leaves the page", () => { withBoard([ ["scheduled workflows", { status: "bad", value: "1 failing" }], ["recent commits", { status: "good", extra: runRows(["c", "b", "a"]) }], ], (container) => { container.querySelector('[data-focus-key="pr-arrow-a"]') ?.focus({ preventScroll: true }); container.querySelector(".evscroll")!.scrollTop = 25; reconcileTiles( container, rendering([ "recent commits", { status: "warn", extra: runRows(["d", "c", "b", "a"]) }, ]), ); expect(container.children).toHaveLength(1); expect(container.children[0].className).toBe("tile warn"); expect(container.querySelector(".evscroll")?.scrollTop).toBe(25); expect(document.activeElement).toBe( container.querySelector('[data-focus-key="pr-arrow-a"]'), ); }); }); Deno.test("an update moves focus from a replaced linked tile to its replacement", () => { withBoard([[ "labs ci", { status: "good", value: "passing", href: "https://example.com/ci" }, ]], (container) => { (container.children[0] as HTMLElement).focus(); reconcileTiles( container, rendering([ "labs ci", { status: "bad", value: "failure", href: "https://example.com/ci" }, ]), ); expect(container.children[0].className).toBe("tile bad link"); expect(document.activeElement).toBe(container.children[0]); }); }); Deno.test("an update keeps focus on a tile's link when the chart under it gains or loses links of its own", () => { // A chart holding a link takes the tile's link off the whole tile and puts // it around the tile's text alone, and an empty chart puts it back. const trust = (extra: string): TileView => ({ status: "good", value: "99.0%", extra, href: "/repos?name=labs", }); const strip = `
`; withBoard([["labs ci trust", trust("")]], (container) => { (container.children[0] as HTMLElement).focus(); reconcileTiles(container, rendering(["labs ci trust", trust(strip)])); const head = container.querySelector("a.tile-head"); expect(head?.getAttribute("href")).toBe("/repos?name=labs"); expect(document.activeElement).toBe(head); reconcileTiles(container, rendering(["labs ci trust", trust("")])); expect(container.children[0].localName).toBe("a"); expect(document.activeElement).toBe(container.children[0]); }); }); Deno.test("an update moves focus inside a replaced tile to the link with the same focus key", () => { withBoard([[ "recent commits", { status: "good", extra: runRows(["b", "a"]) }, ]], (container) => { container.querySelector('[data-focus-key="pr-arrow-a"]') ?.focus(); reconcileTiles( container, rendering([ "recent commits", { status: "good", extra: runRows(["c", "b", "a"]) }, ]), ); const arrow = container.querySelector('[data-focus-key="pr-arrow-a"]'); expect(arrow).not.toBeNull(); expect(document.activeElement).toBe(arrow); }); }); Deno.test("an update keeps focus on a row's pull request number when other rows link the same pull request", () => { withBoard([[ "recent commits", { status: "good", extra: runRows(["b", "a"]) }, ]], (container) => { container.querySelector('[data-focus-key="title-a-1"]') ?.focus(); reconcileTiles( container, rendering([ "recent commits", { status: "warn", extra: runRows(["c", "b", "a"]) }, ]), ); const number = container.querySelector('[data-focus-key="title-a-1"]'); expect(number?.textContent).toBe("(#1)"); expect(document.activeElement).toBe(number); }); }); Deno.test("an update moves focus inside a replaced tile to the element that is not a link with the same focus key", () => { const targets = (host: string) => `
${host}
`; withBoard([[ "production", { status: "good", extra: targets("rapids") }, ]], (container) => { container.querySelector(".tile-detail-list")?.focus(); reconcileTiles( container, rendering(["production", { status: "bad", extra: targets("estuary") }]), ); const region = container.querySelector(".tile-detail-list"); expect(region?.textContent).toBe("estuary"); expect(document.activeElement).toBe(region); }); }); Deno.test("an update moves focus inside a replaced tile to the link with the same href when the link has no focus key", () => { const links = (hrefs: string[]) => hrefs.map((href) => `${href}`) .join(""); withBoard([[ "flaky tests", { status: "good", extra: links(["b", "a"]) }, ]], (container) => { container.querySelector('a[href$="/a"]')?.focus(); reconcileTiles( container, rendering([ "flaky tests", { status: "warn", extra: links(["c", "b", "a"]) }, ]), ); const replacement = container.querySelector('a[href$="/a"]'); expect(replacement?.textContent).toBe("a"); expect(document.activeElement).toBe(replacement); }); }); Deno.test("an update keeps the scroll position of a replaced tile's list", () => { withBoard([[ "recent commits", { status: "good", extra: runRows(["c", "b", "a"]) }, ]], (container) => { container.querySelector(".evscroll")!.scrollTop = 25; reconcileTiles( container, rendering([ "recent commits", { status: "warn", extra: runRows(["d", "c", "b", "a"]) }, ]), ); expect(container.children[0].className).toBe("tile warn"); expect(container.querySelector(".evscroll")?.scrollTop).toBe(25); }); });