35 lines
3.6 KiB
JavaScript
35 lines
3.6 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
const read=p=>fs.readFileSync(new URL('../src/'+p,import.meta.url),'utf8');
|
|
const tokens=Object.fromEntries([...read('styles/variables.css').matchAll(/(--[\w-]+):\s*(#[\da-f]{6})/gi)].map(m=>[m[1],m[2]]));
|
|
const luminance=hex=>hex.slice(1).match(/../g).map(h=>parseInt(h,16)/255).map(c=>c<=.04045?c/12.92:((c+.055)/1.055)**2.4).reduce((s,c,i)=>s+c*[.2126,.7152,.0722][i],0);
|
|
const ratio=(a,b)=>(Math.max(luminance(a),luminance(b))+.05)/(Math.min(luminance(a),luminance(b))+.05);
|
|
function colorRule(source,selector){const start=source.indexOf(selector);assert.ok(start>=0,selector);const body=source.slice(source.indexOf('{',start)+1,source.indexOf('}',start));const token=body.match(/color:\s*var\((--[\w-]+)\)/)?.[1];assert.ok(tokens[token],selector);return {body,color:tokens[token]};}
|
|
function textContrast(color){for(const bg of ['#ffffff',tokens['--color-tint']])assert.ok(ratio(color,bg)>=4.5,`${color} on ${bg}`);}
|
|
test('inline prose links remain distinguishable and meet normal-text contrast in resting and interactive states',()=>{const css=read('styles/blog.css');const normal=colorRule(css,'.prose :is(p, li, td) a:not(.content-button) {');textContrast(normal.color);assert.match(normal.body,/text-decoration:\s*underline/);textContrast(colorRule(css,'.prose :is(p, li, td) a:not(.content-button):is(:hover, :focus-visible)').color);});
|
|
test('source FAQ answer links meet normal-text contrast and retain non-color identification',()=>{const css=read('components/FAQAccordion.astro');const normal=colorRule(css,'.faq-accordion--source .faq-answer__inner :global(a) {');textContrast(normal.color);assert.match(normal.body,/text-decoration:\s*underline/);textContrast(colorRule(css,'.faq-accordion--source .faq-answer__inner :global(a:focus-visible)').color);});
|
|
test('skip and contact/form instructions retain readable link contrast',()=>{
|
|
for(const [file,selector] of [
|
|
['styles/layout.css','.skip-link, .skip-link:visited'],
|
|
['styles/forms.css','.form-note a, .service-inquiry-form__footer a'],
|
|
['components/pages/ContactPage.astro','.contact-page__copy :global(a:not(.content-button))'],
|
|
['components/ContactForm.astro','.contact-form-card__note :global(a)'],
|
|
]){const rule=colorRule(read(file),selector);textContrast(rule.color);assert.match(rule.body,/text-decoration:\s*underline/);}
|
|
});
|
|
test('authored navigation, byline and diagram focus outlines contrast with their light surfaces',()=>{for(const file of ['components/MainNav.astro','components/MobileNav.astro','components/LanguageSwitcher.astro','layouts/BlogPostLayout.astro','styles/blog.css']){const css=read(file);const outlines=[...css.matchAll(/outline:\s*3px solid var\((--[\w-]+)\)/g)];assert.ok(outlines.length,file);for(const [,token]of outlines)for(const bg of ['#ffffff',tokens['--color-tint']])assert.ok(ratio(tokens[token],bg)>=3,file);}});
|
|
|
|
test('article cards, related links and light-surface navigation retain readable contrast across states', () => {
|
|
for (const [file, selector] of [
|
|
['styles/components.css', '.text-link, .text-link:visited'],
|
|
['styles/components.css', '.text-link:hover, .text-link:active'],
|
|
['styles/typography.css', '.eyebrow {'],
|
|
['components/MainNav.astro', '.main-nav a:hover'],
|
|
['components/LanguageSwitcher.astro', '.lang-switcher__menu a[aria-current="page"]'],
|
|
['components/BlogPostFooter.astro', '.post-pagination a:hover'],
|
|
]) textContrast(colorRule(read(file), selector).color);
|
|
const card = read('components/BlogCard.astro');
|
|
const token = card.match(/outline: 3px solid var\((--[\w-]+)\)/)?.[1];
|
|
assert.ok(ratio(tokens[token], '#ffffff') >= 3);
|
|
});
|