The plan asked for jsdom AND Playwright from the start and only jsdom was ever built. jsdom has no layout engine: getBoundingClientRect answers zeros, container queries never fire, a theme is an attribute rather than a computed cascade. So every claim about POSITION or SIZE in this kit was unchecked, and the unit suite could not have noticed either of these. A popover bounded its height and not its width. `max-height: 60vh` was there from the start; there was no max-inline-size at all, so a long unbroken string -- a file path, a model filename -- made the popover wider than the screen and anchor.js could not save it: it can move a box, not shrink one. Measured at 1436px inside a 1280px viewport, off the right edge in all four corners. A horizontal split could not fit a narrow screen. The first pane carries an inline size and `flex: 0 0 auto`, so it would not shrink, and the page scrolled sideways at 360px -- 407px of content in a 360px viewport. It wraps now, which stacks the panes, rather than a media query: the kit sizes itself from its container and not from the window. Nine layout tests: every overlay corner, an overlay taller than the viewport, both themes resolving every token they use, light actually being lighter than dark, and no horizontal scrollbar at five widths. `npm run test:all` runs both suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.5 KiB
JavaScript
65 lines
2.5 KiB
JavaScript
// Both themes, painted.
|
|
//
|
|
// jsdom treats a theme as an attribute: it never resolves a custom property,
|
|
// so "the light theme is light" is unverifiable there. A real browser computes
|
|
// the cascade, which is the only place a token that resolves to the wrong value
|
|
// -- or to nothing -- shows up.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
const rgb = (value) => value.match(/\d+/g).map(Number);
|
|
const luminance = ([r, g, b]) => (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
|
|
test.describe("themes", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/funpack/");
|
|
await page.waitForFunction(() => window.FunPack !== undefined);
|
|
});
|
|
|
|
for (const theme of ["dark", "light"]) {
|
|
test(`${theme} resolves every token it uses`, async ({ page }) => {
|
|
await page.evaluate((t) => window.ComposerTheme.apply(t), theme);
|
|
|
|
const unresolved = await page.evaluate(() => {
|
|
const style = getComputedStyle(document.documentElement);
|
|
const names = [];
|
|
for (const sheet of document.styleSheets) {
|
|
let rules;
|
|
try { rules = sheet.cssRules; } catch { continue; }
|
|
for (const rule of rules) {
|
|
if (!rule.style) continue;
|
|
for (const prop of rule.style) {
|
|
if (prop.startsWith("--")) names.push(prop);
|
|
}
|
|
}
|
|
}
|
|
// A token that resolves to nothing is the failure this catches: the
|
|
// rule still applies and the colour silently comes from elsewhere.
|
|
return [...new Set(names)].filter((n) => style.getPropertyValue(n).trim() === "");
|
|
});
|
|
|
|
expect(unresolved, `tokens with no value in ${theme}`).toEqual([]);
|
|
});
|
|
}
|
|
|
|
test("light is actually lighter than dark", async ({ page }) => {
|
|
const read = async (theme) => {
|
|
await page.evaluate((t) => window.ComposerTheme.apply(t), theme);
|
|
return page.evaluate(() => getComputedStyle(document.body).backgroundColor);
|
|
};
|
|
const dark = luminance(rgb(await read("dark")));
|
|
const light = luminance(rgb(await read("light")));
|
|
expect(light, "the light theme is not lighter than the dark one")
|
|
.toBeGreaterThan(dark);
|
|
});
|
|
|
|
test("the page never scrolls sideways", async ({ page }) => {
|
|
for (const width of [1440, 1024, 768, 480, 360]) {
|
|
await page.setViewportSize({ width, height: 800 });
|
|
const overflows = await page.evaluate(() =>
|
|
document.documentElement.scrollWidth > document.documentElement.clientWidth + 1);
|
|
expect(overflows, `horizontal scrollbar at ${width}px`).toBe(false);
|
|
}
|
|
});
|
|
});
|