test(theme): replace lightness-band test with pairwise distinguishability

The lightness-band guard was looser than the palette it was meant to catch
(old band 0.0175 vs new 0.0246) and the muted-AA test never failed against
the old palette (5.08:1), so both docs' stated expectations and one spec
claim ("chroma roughly doubles") were measured falsehoods. Replace the band
test with an RGB-distance pairwise check, which does discriminate (old
closest pair fog/storm = 7.5, new closest pair coral/peach = 12), fix the
misleading test comment, and correct the spec/plan docs to match reality.
This commit is contained in:
Frederic Cabassut
2026-07-31 12:03:39 +02:00
committed by Fred Cabassut
parent 9600c62288
commit ffc49f2676
3 changed files with 38 additions and 12 deletions
+27 -7
View File
@@ -24,10 +24,14 @@ describe('getNoteColor', () => {
});
});
function toRgb(hex: string): [number, number, number] {
const n = parseInt(hex.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
/** WCAG relative luminance. */
function relativeLuminance(hex: string): number {
const n = parseInt(hex.slice(1), 16);
const channels = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {
const channels = toRgb(hex).map((v) => {
const s = v / 255;
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
});
@@ -39,6 +43,12 @@ function contrastRatio(a: string, b: string): number {
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
function rgbDistance(a: string, b: string): number {
const [ra, ga, ba] = toRgb(a);
const [rb, gb, bb] = toRgb(b);
return Math.hypot(ra - rb, ga - gb, ba - bb);
}
describe('dark note colours', () => {
// Must track [data-theme="dark"] in src/app.css.
const DARK_TEXT = '#ece3d3';
@@ -51,15 +61,25 @@ describe('dark note colours', () => {
});
it('keeps muted text above the AA threshold on every dark card', () => {
// This is the invariant the old palette broke: muted/struck-through text
// became unreadable on the lighter surfaces.
// The shipped dark theme failed this at 3.77:1 with its previous muted
// colour (#9a8e7e). This guard exists so a future palette edit cannot
// reintroduce it — it does not discriminate the old surfaces, which were
// darker and so scored higher against the lifted muted colour.
for (const [name, { bg }] of Object.entries(NOTE_COLORS_DARK)) {
expect(contrastRatio(bg, DARK_TEXT_MUTED), `${name} (${bg})`).toBeGreaterThanOrEqual(4.5);
}
});
it('holds the twelve surfaces within a narrow lightness band so they read as a family', () => {
const luminances = Object.values(NOTE_COLORS_DARK).map((c) => relativeLuminance(c.bg));
expect(Math.max(...luminances) - Math.min(...luminances)).toBeLessThan(0.03);
it('keeps every pair of dark surfaces distinguishable', () => {
// The old palette failed this: fog and storm sat 7.5 apart, so the twelve
// colours read as one dark smudge. Current closest pair is coral/peach at 12.
const entries = Object.entries(NOTE_COLORS_DARK);
for (let i = 0; i < entries.length; i++) {
for (let j = i + 1; j < entries.length; j++) {
const [[nameA, a], [nameB, b]] = [entries[i], entries[j]];
const distance = rgbDistance(a.bg, b.bg);
expect(distance, `${nameA} vs ${nameB}`).toBeGreaterThan(10);
}
}
});
});