Files
HMDesign/packages/org-chart/src/core/contrast.test.ts
T

98 lines
6.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { contrastRatio, evaluateGradientContrast, parseOpaqueColor, parseVerticalGradient, relativeLuminance, textContrastThreshold } from './contrast.js';
const near = (actual: number, expected: number, tolerance = 0.001) => assert.ok(Math.abs(actual - expected) < tolerance, `${actual} ≈ ${expected}`);
const integrated = 'linear-gradient(180deg, #8c67b6 0%, #5d3494 100%)';
const design = 'linear-gradient(180deg, rgb(9, 167, 162) 0%, rgb(0, 132, 127) 100%)';
test('standard sRGB luminance and contrast use unrounded channels with symmetric ratios', () => {
near(relativeLuminance([0, 0, 0]), 0);
near(relativeLuminance([255, 255, 255]), 1);
near(contrastRatio([0, 0, 0], [255, 255, 255]), 21);
near(contrastRatio([16, 36, 59], [255, 255, 255]), 15.7005);
near(contrastRatio([89, 101, 117], [244, 247, 251]), 5.5160);
assert.equal(contrastRatio([16, 36, 59], [255, 255, 255]), contrastRatio([255, 255, 255], [16, 36, 59]));
});
test('color parser accepts opaque HEX and computed RGB, rejects transparent/unknown compositing', () => {
assert.deepEqual(parseOpaqueColor('#09a7a2'), [9, 167, 162]);
assert.deepEqual(parseOpaqueColor('#fff'), [255, 255, 255]);
assert.deepEqual(parseOpaqueColor('rgb(16 36 59)'), [16, 36, 59]);
assert.deepEqual(parseOpaqueColor('rgba(16, 36, 59, 1)'), [16, 36, 59]);
for (const value of ['rgba(0, 0, 0, 0.5)', '#ffffff80', 'transparent', 'red', 'rgb(100% 0% 0%)', 'color(display-p3 1 0 0)', 'rgb(256, 0, 0)']) assert.equal(parseOpaqueColor(value), null);
});
test('large text threshold uses measured size/weight and does not treat 15px bold as large', () => {
assert.equal(textContrastThreshold(15, 700), 4.5);
assert.equal(textContrastThreshold(19.5, 700), 3);
assert.equal(textContrastThreshold(18.6, 700), 4.5);
assert.equal(textContrastThreshold(14 * 96 / 72, 700), 3);
assert.equal(textContrastThreshold(19.5, 500), 4.5);
assert.equal(textContrastThreshold(24, 400), 3);
});
test('restricted gradient parser preserves RGB commas and explicit ordered percent stops', () => {
assert.equal(parseVerticalGradient(design)!.stops.length, 2);
assert.deepEqual(parseVerticalGradient(design)!.stops[0]!.color, [9, 167, 162]);
assert.equal(parseVerticalGradient('linear-gradient(180deg, #000 0%, #fff 25%, #000 100%)')!.stops[1]!.offset, 0.25);
for (const value of ['linear-gradient(90deg, #000 0%, #fff 100%)', 'linear-gradient(180deg in oklab, #000 0%, #fff 100%)', 'linear-gradient(180deg, #000, #fff)', 'linear-gradient(180deg, #000 0px, #fff 100px)', 'linear-gradient(180deg, #000 0%, #fff 50%)', 'linear-gradient(180deg, #000 0%, #fff 0%, #000 100%)', 'linear-gradient(180deg, #000 0%, #fff 100%), url(x)', 'url(image.png)', 'repeating-linear-gradient(180deg, #000 0%, #fff 100%)']) assert.equal(parseVerticalGradient(value), null, value);
});
test('browser-computed omitted default angle is equivalent to 180deg, without inferring omitted stops', () => {
const computed = 'linear-gradient(rgb(9, 167, 162) 0%, rgb(0, 132, 127) 100%)';
assert.deepEqual(parseVerticalGradient(computed), parseVerticalGradient(design));
const input = { foreground: '#fff', ranges: [{ top: 6, bottom: 24 }], backgroundHeight: 30 };
assert.deepEqual(evaluateGradientContrast({ ...input, backgroundImage: computed }), evaluateGradientContrast({ ...input, backgroundImage: design }));
assert.equal(parseVerticalGradient('linear-gradient(rgb(9, 167, 162), rgb(0, 132, 127))'), null);
});
test('inset actual text model can pass while a conservative stop value is below 4.5', () => {
const result = evaluateGradientContrast({ foreground: '#fff', backgroundImage: integrated, ranges: [{ top: 6, bottom: 24 }], backgroundHeight: 30 });
assert.equal(result.status, 'MODEL');
near(result.stopMinimum!, 4.4391);
assert.ok(result.stopMinimum! < 4.5);
assert.ok(result.textRegionMinimum! > 5);
assert.ok(result.textRegionMinimum! > result.fullGradientMinimum!);
});
test('current large title and normal Full title thresholds remain independent for the same actual area', () => {
const result = evaluateGradientContrast({ foreground: '#fff', backgroundImage: design, ranges: [{ top: 6, bottom: 24 }], backgroundHeight: 30 });
assert.ok(result.textRegionMinimum! >= textContrastThreshold(19.5, 700));
assert.ok(result.textRegionMinimum! < textContrastThreshold(15, 700));
const black = evaluateGradientContrast({ foreground: '#000', backgroundImage: design, ranges: [{ top: 0, bottom: 30 }], backgroundHeight: 30 });
near(black.fullGradientMinimum!, 4.6070);
assert.ok(black.fullGradientMinimum! >= 4.5);
});
test('all actual line ranges are included, preserving a weak wrapped line', () => {
const first = evaluateGradientContrast({ foreground: '#fff', backgroundImage: integrated, ranges: [{ top: 60, bottom: 80 }], backgroundHeight: 100 });
const wrapped = evaluateGradientContrast({ foreground: '#fff', backgroundImage: integrated, ranges: [{ top: 60, bottom: 80 }, { top: 0, bottom: 10 }], backgroundHeight: 100 });
assert.ok(first.textRegionMinimum! > 4.5);
near(wrapped.textRegionMinimum!, wrapped.stopMinimum!);
assert.ok(wrapped.textRegionMinimum! < first.textRegionMinimum!);
});
test('stop minima never substitute for whole-gradient sampling when foreground meets an interior color', () => {
const result = evaluateGradientContrast({ foreground: '#777', backgroundImage: 'linear-gradient(180deg, #000 0%, #fff 100%)', ranges: [{ top: 40, bottom: 55 }], backgroundHeight: 100 });
assert.ok(result.stopMinimum! > 4);
assert.ok(result.fullGradientMinimum! < 1.02);
assert.ok(result.textRegionMinimum! < 1.03);
});
test('unsupported backgrounds, alpha, missing/clipped ranges and unbounded sample requests remain NOT VERIFIED', () => {
const valid = { foreground: '#fff', backgroundImage: integrated, ranges: [{ top: 2, bottom: 20 }], backgroundHeight: 30 };
for (const input of [
{ ...valid, foreground: 'rgba(255, 255, 255, 0.5)' }, { ...valid, backgroundImage: 'url(x)' },
{ ...valid, ranges: [] }, { ...valid, ranges: [{ top: -1, bottom: 20 }] },
{ ...valid, ranges: [{ top: 2, bottom: 31 }] }, { ...valid, ranges: [{ top: 20, bottom: 2 }] },
{ ...valid, backgroundHeight: NaN }, { ...valid, backgroundHeight: 0 }, { ...valid, sampleStep: 0 },
{ ...valid, sampleStep: 0.000001 },
]) {
const result = evaluateGradientContrast(input);
assert.equal(result.status, 'NOT VERIFIED');
assert.equal(result.textRegionMinimum, undefined);
assert.ok(result.reason);
}
});