import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { browser } from './browser.mjs'; const publicDir = resolve(import.meta.dirname, '../src/public'); const url = name => pathToFileURL(join(publicDir, name)).href; const project = { id: '20000000-0000-4000-8000-000000000002', name: 'Example project', repo: null }; for (const width of [1440, 390]) { test(`archive controls have shared button styling at ${width}px`, async t => { const dir = await mkdtemp(join(tmpdir(), 'archive-style-')); const html = ` ${['shared/app.css', 'tokens.css', 'chat.css', 'shell.css', 'project-archive.css'].map(x => ``).join('')}
`; const fixture = join(dir, 'fixture.html'); await writeFile(fixture, html); const b = await browser(); t.after(() => b.close()); t.after(() => rm(dir, { recursive: true, force: true })); await b.viewport(width, 1000); await b.navigate(pathToFileURL(fixture).href); await b.evaluate(`window.MosaicChat={projectPath:id=>'#/'+id}; window.fetch=async()=>({ok:true,json:async()=>({rows:[{...${JSON.stringify(project)},archived:true}]})}); window.renderList=()=>{const e=document.querySelector('#list');e.replaceChildren();MosaicProjectArchive.mount(e,{kind:'list',canSend:true})};window.addEventListener('hashchange',renderList);window.renderProject=archived=>{const e=document.querySelector('#project');e.replaceChildren();MosaicProjectArchive.mount(e,{kind:'project',canSend:true,id:${JSON.stringify(project.id)},projects:[{...${JSON.stringify(project)},archived}]})};renderProject(false);renderList();true`); await b.evaluate(`document.querySelector('#project-archive-toggle').click();true`); await b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(document.querySelector('#project-archive-list button'))return;await new Promise(r=>setTimeout(r,20))}throw Error('archived row missing')})()`); if (process.env.ARCHIVE_STYLE_SHOTS) await b.screenshot(join(process.env.ARCHIVE_STYLE_SHOTS, `${process.env.ARCHIVE_STYLE_PHASE}-${width}.png`)); const inspect = async selector => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)}),s=getComputedStyle(e);return {height:e.getBoundingClientRect().height,radius:s.borderTopLeftRadius,background:s.backgroundColor,classes:e.className}})()`); const defaults = await inspect('#default'); const check = (s, label) => { assert.ok(s.height >= 44, `${label} height: ${s.height}`); assert.equal(s.radius, '6px', `${label} radius`); assert.notEqual(s.background, defaults.background, `${label} background`); assert.ok(s.classes.split(/\s+/).includes('btn'), `${label} shared class`); }; const archive = await inspect('#project-archive-button'); const row = await inspect('#project-archive-list button'); assert.equal(await b.evaluate(`document.querySelector('#project-archive-list a button') === null && document.querySelector('#project-archive-list li > a + button') !== null`), true, 'Unarchive is a sibling of the card link'); check(archive, 'Archive'); check(row, 'list Unarchive'); check(await inspect('#project-archive-toggle'), 'toggle'); assert.notEqual(archive.background, row.background, 'Archive has a distinct background'); // The danger tint must preserve readable text in every palette and appearance. const contrastFailures = await b.evaluate(`(()=>{ const button=document.querySelector('#project-archive-button'), root=document.documentElement; const canvas=document.createElement('canvas'), ctx=canvas.getContext('2d'); const rgb=color=>{ctx.clearRect(0,0,1,1);ctx.fillStyle=color;ctx.fillRect(0,0,1,1);return [...ctx.getImageData(0,0,1,1).data].slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4})}; const luminance=c=>{const [r,g,b]=rgb(c);return .2126*r+.7152*g+.0722*b}; const failures=[]; for(const palette of ['harbor','atlantic','aubergine','carmine','cobalt','graphite','grove','mineral','rosewood','terracotta']) for(const mode of ['light','dim','dark']) { root.dataset.palette=palette;root.dataset.mode=mode; const s=getComputedStyle(button), a=luminance(s.color), b=luminance(s.backgroundColor), ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05); if(ratio<4.5)failures.push({palette,mode,ratio}); } root.dataset.palette='harbor';root.dataset.mode='light';button.focus(); const s=getComputedStyle(button);if(s.outlineStyle==='none'||parseFloat(s.outlineWidth)<2)failures.push({focus:s.outline}); return failures; })()`); assert.deepEqual(contrastFailures, [], 'danger button text contrast and focus ring'); await b.evaluate('renderProject(true);true'); check(await inspect('#project-archive-button'), 'project Unarchive'); }); }