Files
AgentCompany/tests/e2e/pixel-style.spec.ts
T

93 lines
6.1 KiB
TypeScript

import { _electron as electron, expect, test } from "@playwright/test";
import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture";
import { closeApp, onboard } from "./onboard";
test("text uses the bundled pixel font, and the palette follows the day part, season and clock theme", async () => {
const profileDir = join(tmpdir(), `agentcompany-new-e2e-pixel-${randomUUID()}`);
mkdirSync(profileDir, { recursive: true });
const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-pixel-builtin-${randomUUID()}`));
const app = await electron.launch({
args: [resolve("out/main/index.js")],
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir },
});
try {
const page = await app.firstWindow();
await onboard(page, "Pixel Co");
// The font file itself loaded, not a fallback.
expect(await page.evaluate(async () => {
await document.fonts.load('12px "Fusion Pixel"', "像素 Pixel");
return [...document.fonts].some((face) => face.family.replace(/"/g, "") === "Fusion Pixel" && face.status === "loaded");
})).toBe(true);
expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toMatch(/^"?Fusion Pixel/);
const expected = await page.evaluate(() => {
const now = new Date();
const hour = now.getHours();
const month = now.getMonth();
const part = hour >= 5 && hour < 8 ? "dawn" : hour >= 8 && hour < 17 ? "day" : hour >= 17 && hour < 20 ? "dusk" : "night";
const season = month >= 2 && month <= 4 ? "spring" : month >= 5 && month <= 7 ? "summer" : month >= 8 && month <= 10 ? "autumn" : "winter";
return { part, season };
});
const root = page.locator("html");
await expect(root).toHaveAttribute("data-daypart", expected.part);
await expect(root).toHaveAttribute("data-season", expected.season);
// The ground carries the dither; the chosen nav row blinks the pixel menu arrow, other rows show none; dialogs sit in the 9-slice frame.
expect(await page.evaluate(() => getComputedStyle(document.querySelector(".main")!).backgroundImage)).toContain("data:image/svg+xml");
expect(await page.evaluate(() => {
const rows = [...document.querySelectorAll(".nav > .nav-item")];
const active = rows.find((row) => row.matches(".active, [aria-current]"))!;
const idle = rows.find((row) => !row.matches(".active, [aria-current]"))!;
return [getComputedStyle(active, "::before").animationName, getComputedStyle(idle, "::before").animationName, getComputedStyle(idle, "::before").visibility];
})).toEqual(["px-blink", "none", "hidden"]);
// Every icon on screen is a pixel glyph on the font's grid; no stroked vector icons remain.
expect(await page.evaluate(() => ({
stroked: [...document.querySelectorAll("svg")].filter((svg) => !svg.closest("canvas") && svg.getAttribute("stroke") && svg.getAttribute("stroke") !== "none").length,
size: getComputedStyle(document.querySelector("svg.pixel-icon")!).width === getComputedStyle(document.body).fontSize,
}))).toEqual({ stroked: 0, size: true });
expect(await page.evaluate(() => document.querySelectorAll(".nav > .nav-item").length)).toBe(await page.evaluate(() => document.querySelectorAll(".nav > .nav-item svg.pixel-icon").length));
await page.getByRole("button", { name: "New task" }).click();
const dialog = page.getByRole("dialog", { name: "New task" });
await expect(dialog).toBeVisible();
expect(await dialog.evaluate((node) => {
const style = getComputedStyle(node.closest(".modal") ?? node.querySelector(".modal")!);
return [style.borderImageSource.startsWith('url("data:image/svg+xml'), style.borderImageSlice, style.borderTopWidth];
})).toEqual([true, "3 fill", "6px"]);
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
// Progress is an XP bar announced as a value; hints are spoken by the guide in a bubble.
await page.getByRole("button", { name: "Home" }).click();
await expect(page.getByRole("progressbar", { name: "Goal progress" })).toHaveAttribute("aria-valuenow", "0");
// On the roster the only agent holds the largest budget, and the bar says it compares budgets, not spend.
await page.getByRole("button", { name: "Team", exact: true }).click();
await expect(page.getByRole("progressbar", { name: "Budget compared with the largest agent budget" })).toHaveAttribute("aria-valuenow", "100");
await page.getByRole("button", { name: "Work", exact: true }).click();
const hint = page.getByRole("note").filter({ hasText: "Only a task runs" });
await expect(hint.locator(".hint-speaker svg.pixel-icon")).toHaveCount(1);
// Tabs are raised pixel tabs; fields are sunken pixel boxes.
const views = page.getByRole("radiogroup", { name: "View" });
expect(await views.locator("[aria-checked=true]").evaluate((tab) => getComputedStyle(tab).borderBottomColor === getComputedStyle(tab).backgroundColor)).toBe(true);
await page.getByRole("button", { name: "Settings" }).click();
expect(await page.getByRole("combobox", { name: "Theme" }).evaluate((field) => [getComputedStyle(field).borderTopWidth, getComputedStyle(field).borderRadius])).toEqual(["2px", "0px"]);
await page.getByRole("combobox", { name: "Theme" }).selectOption("dark");
await expect(root).toHaveAttribute("data-theme", "dark");
await page.getByRole("combobox", { name: "Theme" }).selectOption({ label: "Follow the clock" });
await expect(root).toHaveAttribute("data-theme", expected.part === "night" ? "dark" : "light");
// Each font pixel covers a whole number of screen pixels at this display's scale.
expect(await page.evaluate(() => {
const fontPx = parseFloat(getComputedStyle(document.body).fontSize) * window.devicePixelRatio;
return Math.abs(fontPx - Math.round(fontPx)) < 1e-6 && Math.round(fontPx) % 12 === 0;
})).toBe(true);
} finally {
await closeApp(app);
}
});