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 `
`;
}
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);
});
});