fix: harden contrast test tooling

This commit is contained in:
Peter Li 2026-07-16 15:27:31 +08:00
parent 7f3c846955
commit 95c5289f0e
4 changed files with 61 additions and 5 deletions

View file

@ -1,4 +1,5 @@
import { pathToFileURL } from "node:url";
import { realpathSync } from "node:fs";
import { fileURLToPath } from "node:url";
const HEX_COLOR = /^#[0-9a-f]{6}$/i;
@ -58,6 +59,10 @@ export function runContrastCli(
if (failed) process.exitCode = 1;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const isDirectRun =
process.argv[1] !== undefined &&
realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
if (isDirectRun) {
runContrastCli();
}

View file

@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
@ -116,6 +119,14 @@ test("six core foreground/background pairs meet WCAG AA contrast", () => {
}
});
test("core contrast ratios match the WCAG calculations", () => {
const expectedRatios = ["12.72", "4.53", "5.23", "13.32", "6.57", "7.38"];
for (const [index, [, foreground, background]] of CORE_CONTRAST_PAIRS.entries()) {
assert.equal(contrastRatio(foreground, background).toFixed(2), expectedRatios[index]);
}
});
test("contrast calculation matches WCAG reference endpoints", () => {
assert.equal(contrastRatio("#000000", "#ffffff"), 21);
assert.equal(contrastRatio("#68756d", "#68756d"), 1);
@ -136,6 +147,28 @@ test("contrast CLI prints all six pairs", () => {
}
});
test("contrast CLI runs when invoked through a symbolic link", (t) => {
const directory = mkdtempSync(join(tmpdir(), "aera-contrast-cli-"));
const symlinkPath = join(directory, "contrast.mjs");
const cliPath = fileURLToPath(new URL("../scripts/contrast.mjs", import.meta.url));
t.after(() => rmSync(directory, { recursive: true, force: true }));
try {
symlinkSync(cliPath, symlinkPath);
} catch (error) {
if (["EACCES", "EPERM"].includes(error.code)) {
t.skip("creating symbolic links is not permitted in this environment");
return;
}
throw error;
}
const result = spawnSync(process.execPath, [symlinkPath], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /PASS light text:/);
});
test("contrast CLI marks a failing pair and requests exit code 1", () => {
const originalExitCode = process.exitCode;
const output = [];

View file

@ -7,9 +7,9 @@ export function declarationsFor(css, selector) {
root.walkRules((rule) => {
if (rule.selector !== selector) return;
rule.walkDecls((declaration) => {
declarations.set(declaration.prop, declaration.value);
});
for (const node of rule.nodes ?? []) {
if (node.type === "decl") declarations.set(node.prop, node.value);
}
});
return declarations;

View file

@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import test from "node:test";
import { declarationsFor } from "./css.mjs";
test("declarationsFor ignores declarations in nested child rules", () => {
const css = `
body {
--font-text-theme: parent;
.child {
--font-text-theme: child;
}
}
`;
assert.equal(declarationsFor(css, "body").get("--font-text-theme"), "parent");
});