← back to CelebritySignatures
test/signature-ink.test.mjs
60 lines
import test from 'node:test';
import assert from 'node:assert/strict';
import {normalizeInk,imageSource,inkColorFor,inkColorForHue} from '../public/assets/signature-ink.js';
// WCAG relative luminance + contrast-vs-white, to test the requirement (legible
// color on a clean white ground) rather than a naive brightness proxy.
const wcagLum=([r,g,b])=>{const f=c=>{c/=255;return c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4;};return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);};
const contrastWhite=rgb=>1.05/(wcagLum(rgb)+0.05);
function fixture(paper,ink){const data=new Uint8ClampedArray(32*16*4);for(let p=0;p<512;p++)data.set(p>=240&&p<256?ink:paper,p*4);return {data,width:32,height:16};}
test('aged paper, colored ink and faded ink become only dark ink and opaque white',()=>{
for(const f of [fixture([226,205,151,255],[60,75,100,255]),fixture([245,241,230,255],[195,185,181,255]),fixture([0,0,0,0],[0,0,0,255])]){
const out=normalizeInk(f);let dark=0;
for(let i=0;i<out.data.length;i+=4){const c=out.data[i];assert.ok(c===26||c===255);assert.equal(out.data[i+1],c);assert.equal(out.data[i+2],c);assert.equal(out.data[i+3],255);if(c===26)dark++;}
assert.equal(dark,16);assert.equal(out.width,f.width);assert.equal(out.height,f.height);
}
});
test('light ink on a uniform dark ground is inverted without cutting the image frame',()=>{
const f=fixture([12,12,12,255],[230,230,230,255]);const out=normalizeInk(f);
assert.equal(out.inverted,true);assert.equal(out.data[240*4],26);assert.equal(out.data[0],255);
});
test('blank images fail explicitly and source URLs cannot inject scripts or credentials',()=>{
assert.throws(()=>normalizeInk(fixture([255,255,255,255],[255,255,255,255])));
assert.equal(imageSource('javascript:alert(1)'),null);assert.equal(imageSource('https://user:pass@example.com/a'),null);
assert.equal(imageSource('http://commons.wikimedia.org/a'),'https://commons.wikimedia.org/a');
});
test('an ink color recolors the strokes to that color on a clean white ground',()=>{
const f=fixture([245,241,230,255],[60,75,100,255]);
const out=normalizeInk(f,[180,40,60]);let ink=0;
for(let i=0;i<out.data.length;i+=4){
const r=out.data[i],g=out.data[i+1],b=out.data[i+2];
const isInk=(r===180&&g===40&&b===60), isPaper=(r===255&&g===255&&b===255);
assert.ok(isInk||isPaper); // only the tint or clean white
assert.equal(out.data[i+3],255); // fully opaque
if(isInk)ink++;
}
assert.equal(ink,16); // same strokes, now colored
});
test('inkColorFor is stable per key and varies across keys',()=>{
const a=inkColorFor('Charles Darwin'), a2=inkColorFor('Charles Darwin');
assert.deepEqual(a,a2); // deterministic
assert.notDeepEqual(a,inkColorFor('Abraham Lincoln'));
assert.equal(a.length,3);
});
test('EVERY hue on the wheel clears WCAG 3:1 non-text contrast on white',()=>{
// The real requirement — legible color on a clean white ground — across the
// whole 360°, not three lucky names. Catches the olive/yellow dead zone.
let worst=Infinity, worstHue=-1;
for(let hue=0;hue<360;hue++){
const c=inkColorForHue(hue), cr=contrastWhite(c);
if(cr<worst){worst=cr;worstHue=hue;}
assert.ok(cr>=3.0, `hue ${hue} → rgb(${c}) only ${cr.toFixed(2)}:1 on white`);
}
assert.ok(worst>=3.0, `worst hue ${worstHue} at ${worst.toFixed(2)}:1`);
});
test('sampled real names all clear the contrast floor',()=>{
for(const n of ['Charles Darwin','Abraham Lincoln','Ada Lovelace','Nikola Tesla','Marie Curie','Vincent van Gogh','Wolfgang Amadeus Mozart','Q42']){
assert.ok(contrastWhite(inkColorFor(n))>=3.0, `${n} below 3:1`);
}
});