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); } });