Combined node --test runs hung in the org browser files. Two causes: - org-quick: a page's GET /api/config reached the server on a kept-alive connection after close(server) had stopped the listener. The handler threw on server.address().port, the request was never answered, and server.close() waited for it forever. close() in fixture.mjs now cuts every connection and fails the hook after 5 s with a message. - org-delete: t.after hooks run in registration order, so the data directory was removed while the server still wrote there. The rm failed with ENOTEMPTY, node:test skipped the later hooks, and the open server and chromium kept the file running. The three org files and project-archive-style now register the removal last. browser.mjs: close() kills a browser that does not answer Browser.close within 5 s, a call to an exited browser fails at once, and an unclosed chromium no longer holds the event loop and is killed when the test process exits. teardown.test.mjs reproduces each case: 0/4 pass on the old helpers, 4/4 on the new ones.
64 lines
5.2 KiB
JavaScript
64 lines
5.2 KiB
JavaScript
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 = `<!doctype html><html data-palette="harbor" data-mode="light"><head><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
${['shared/app.css', 'tokens.css', 'chat.css', 'shell.css', 'project-archive.css'].map(x => `<link rel="stylesheet" href="${url(x)}">`).join('')}
|
|
<script src="${url('project-archive.js')}"></script></head><body><main class="content"><div id="project"></div><div id="list"></div><button id="default">Default</button></main></body></html>`;
|
|
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');
|
|
});
|
|
}
|