import { readFileSync } from "node:fs";
import { expect, test, type Page } from "@playwright/test";
import { shimmerTextScript } from "../packages/ui/test/shimmer-text-bundle.mjs";
// No server required. Use the production markup/styles: jsdom cannot check
// counter-translated glyph alignment, text selection, or media fallbacks.
const css = readFileSync("packages/ui/styles/base.css", "utf8");
declare global {
interface Window {
shimmerFixture: { render(text: string, active: boolean): void };
}
}
test.beforeEach(async ({ page }) => {
await page.setContent(`
`);
await page.addScriptTag({ content: shimmerTextScript });
});
async function renderLabel(page: Page, text = "Working", active = true) {
await page.evaluate(({ text, active }) => window.shimmerFixture.render(text, active), { text, active });
}
test("exposes and copies the label once, including localized and updated counts", async ({ page }) => {
for (const text of ["Working", "2 agents working", "12 个智能体正在工作"]) {
await renderLabel(page, text);
expect(await page.evaluate(() => document.getAnimations().filter((a) => a.playState === "running").length)).toBe(2);
expect(await page.locator("#fixture").ariaSnapshot()).toBe(`- text: ${text}`);
const selected = await page.locator("#label").evaluate((label) => {
const range = document.createRange();
range.selectNodeContents(label);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
return selection.toString();
});
expect(selected).toBe(text);
}
});
test("keeps both copies aligned throughout the sweep and truncates long labels", async ({ page }) => {
for (const text of ["Working", "12 个智能体正在工作", "Searching through a very long project name"]) {
await renderLabel(page, text);
// Include both sides of the repeating sweep's seam.
for (const time of [0, 400, 1250, 2499, 2501]) {
const geometry = await page.evaluate((time) => {
for (const animation of document.getAnimations()) {
animation.pause();
animation.currentTime = time;
}
const label = document.querySelector("#label")!;
const copy = label.querySelector(".shimmer-text-copy")!;
const baseRect = label.getBoundingClientRect();
const copyRect = copy.getBoundingClientRect();
return {
dx: Math.abs(baseRect.x - copyRect.x),
dy: Math.abs(baseRect.y - copyRect.y),
dw: Math.abs(baseRect.width - copyRect.width),
width: baseRect.width,
overflow: getComputedStyle(label).textOverflow,
copyOverflow: getComputedStyle(copy).textOverflow,
clipped: label.scrollWidth > label.clientWidth,
};
}, time);
expect(geometry.dx).toBeLessThan(0.1);
expect(geometry.dy).toBeLessThan(0.1);
expect(geometry.dw).toBeLessThan(0.1);
expect(geometry.width).toBeLessThanOrEqual(180);
expect(geometry.overflow).toBe("ellipsis");
expect(geometry.copyOverflow).toBe("ellipsis");
if (text.startsWith("Searching")) expect(geometry.clipped).toBe(true);
}
}
});
test("remains readable without running animations when inactive or motion is reduced", async ({ page }) => {
await renderLabel(page, "Queued", false);
await expect(page.locator("#label")).toHaveText("Queued");
expect(await page.evaluate(() => document.getAnimations().length)).toBe(0);
await page.emulateMedia({ reducedMotion: "reduce" });
await renderLabel(page);
await expect(page.locator("#label")).toHaveCSS("color", "rgb(102, 102, 102)");
await expect(page.locator(".shimmer-text-window")).toBeHidden();
expect(await page.evaluate(() => document.getAnimations().length)).toBe(0);
});
test("uses readable system text and no highlight in forced colors", async ({ page }) => {
await page.emulateMedia({ forcedColors: "active" });
await renderLabel(page);
await expect(page.locator(".shimmer-text-window")).toBeHidden();
expect(await page.evaluate(() => document.getAnimations().length)).toBe(0);
expect(await page.locator("#fixture").ariaSnapshot()).toBe("- text: Working");
const colors = await page.locator("#label").evaluate((label) => {
const probe = document.createElement("span");
probe.style.color = "CanvasText";
document.body.append(probe);
const result = [getComputedStyle(label).color, getComputedStyle(probe).color];
probe.remove();
return result;
});
expect(colors[0]).toBe(colors[1]);
});