98 lines
6.4 KiB
TypeScript
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);
|
|
}
|
|
});
|