115 lines
8.2 KiB
JavaScript
115 lines
8.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { fixture, close } from './fixture.mjs';
|
|
import { browser } from './browser.mjs';
|
|
const GEN = '12345678-1234-4234-8234-123456789abc';
|
|
async function setup(t, readonly = false) {
|
|
const f = await fixture(); t.after(() => f.close());
|
|
const server = await startServer({ port: 0, board: f.boardURL }); t.after(() => close(server));
|
|
const b = await browser(); t.after(() => b.close());
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: `(() => {
|
|
const real=fetch.bind(window);window.writes=[];window.failWrite=false;window.confirmResume=true;window.confirmed=0;
|
|
window.processingRequests=[];window.processingSignin=null;
|
|
const interval=window.setInterval.bind(window),clear=window.clearInterval.bind(window),active=new Set();
|
|
window.processingTimers=()=>[...active];
|
|
window.setInterval=(fn,ms,...args)=>{const id=interval(fn,ms,...args);if(ms===5000&&new Error().stack.includes('/processing.js:'))active.add(id);return id};
|
|
window.clearInterval=id=>{active.delete(id);return clear(id)};
|
|
window.confirm=()=>{window.confirmed++;return window.confirmResume};
|
|
window.processing={version:1,generation:'${GEN}',halted:${readonly},quiescent:true,remaining:0,stopped:0};
|
|
window.fetch=async(url,init={})=>{
|
|
if(url==='/api/config'||String(url).startsWith('/api/processing')){
|
|
window.processingRequests.push({path:url,method:init.method||'GET',redirect:init.redirect??null});
|
|
if(window.processingSignin?.path===url)return window.processingSignin.kind==='redirect'
|
|
? {type:'opaqueredirect',ok:false,status:0,json:async()=>{throw Error('redirect has no body')}}
|
|
: new Response(JSON.stringify({error:'sign-in required'}),{status:401});
|
|
}
|
|
if(url==='/api/config')return new Response(JSON.stringify({send:${!readonly}}));
|
|
if(!String(url).startsWith('/api/processing'))return real(url,init);
|
|
if(init.method==='POST'){
|
|
window.writes.push({path:url,body:JSON.parse(init.body),redirect:init.redirect});
|
|
if(window.failWrite)return new Response(JSON.stringify({error:'outcome-unknown',message:'do not echo'}),{status:503});
|
|
window.processing.halted=String(url).endsWith('/halt');
|
|
}
|
|
return new Response(JSON.stringify(window.processing));
|
|
};
|
|
})();` });
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/`);
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30));}throw Error(${JSON.stringify(expression)})})()`);
|
|
await wait(`document.querySelector('#processing-state')?.textContent.includes(${JSON.stringify(readonly ? 'halted' : 'enabled')})`);
|
|
return { b, wait };
|
|
}
|
|
test('unchanged processing reads retain the status text node', async t => {
|
|
const { b, wait } = await setup(t);
|
|
await b.evaluate(`window.processingText=document.querySelector('#processing-state').firstChild;window.readsBefore=window.processingRequests.length;window.dispatchEvent(new Event('focus'))`);
|
|
await wait('window.processingRequests.length >= window.readsBefore + 2');
|
|
await b.evaluate(`new Promise(resolve=>setTimeout(resolve,50))`);
|
|
assert.equal(await b.evaluate(`processingText.isConnected && document.querySelector('#processing-state').firstChild===processingText`), true);
|
|
});
|
|
test('processing browser: keyboard Stop, confirmed Resume, unknown outcome and persistent banner', async t => {
|
|
const { b, wait } = await setup(t);
|
|
await b.evaluate(`document.querySelector('#processing-halt').focus(),true`); await b.key('Enter');
|
|
await wait('window.writes.length===1 && !document.querySelector("#processing-banner").hidden');
|
|
assert.deepEqual(await b.evaluate('window.writes[0]'), { path: '/api/processing/halt', body: {}, redirect: 'manual' });
|
|
for (const route of ['#/tasks', '#/settings', '#/board']) {
|
|
await b.evaluate(`location.hash=${JSON.stringify(route)},true`);
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-banner").hidden'), false);
|
|
}
|
|
await b.evaluate('window.confirmResume=false;document.querySelector("#processing-resume").click();true');
|
|
assert.equal(await b.evaluate('window.writes.length'), 1);
|
|
await b.evaluate('window.confirmResume=true;document.querySelector("#processing-resume").click();true');
|
|
await wait('window.writes.length===2 && document.querySelector("#processing-banner").hidden');
|
|
assert.deepEqual(await b.evaluate('window.writes[1].body'), { generation: GEN });
|
|
assert.equal(await b.evaluate('window.confirmed'), 2);
|
|
await b.evaluate('window.failWrite=true;document.querySelector("#processing-halt").click();true');
|
|
await wait('document.querySelector("#processing-receipt").textContent.includes("Outcome unknown")');
|
|
assert.equal(await b.evaluate('window.writes.length'), 3);
|
|
assert.doesNotMatch(await b.evaluate('document.querySelector("#processing-receipt").textContent'), /do not echo/);
|
|
});
|
|
test('processing browser: Q175 read-only interim keeps both writes disabled and names CLI recovery at 320px', async t => {
|
|
const { b, wait } = await setup(t, true); await b.viewport(320);
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-halt").disabled'), true);
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-resume").disabled'), true);
|
|
assert.match(await b.evaluate('document.querySelector("#processing-unavailable").textContent'), /mosaic halt.*mosaic resume/);
|
|
await b.evaluate('document.querySelector("#processing-halt").click();document.querySelector("#processing-resume").click();true');
|
|
assert.equal(await b.evaluate('window.writes.length'), 0);
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-banner").hidden'), false);
|
|
});
|
|
|
|
for (const [name,path,kind] of [
|
|
['config redirect','/api/config','redirect'],
|
|
['state redirect','/api/processing','redirect'],
|
|
['write redirect','/api/processing/halt','redirect'],
|
|
['config sign-in required','/api/config','signin'],
|
|
['state sign-in required','/api/processing','signin'],
|
|
['write sign-in required','/api/processing/halt','signin'],
|
|
]) test(`processing browser: sign-out from ${name} blocks later refresh and writes`, async t => {
|
|
const { b, wait } = await setup(t);
|
|
assert.equal(await b.evaluate('window.processingRequests.every(r=>r.redirect==="manual")'), true);
|
|
assert.equal(await b.evaluate('window.processingTimers().length'), 1);
|
|
await b.evaluate(`window.processingSignin=${JSON.stringify({path,kind})};true`);
|
|
if(path.endsWith('/halt')) await b.evaluate('document.querySelector("#processing-halt").click();true');
|
|
else await b.evaluate('window.dispatchEvent(new Event("focus"));true');
|
|
await wait('document.body.classList.contains("signed-out")');
|
|
assert.equal(await b.evaluate('window.processingTimers().length'), 0, 'sign-out cancels the processing interval');
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-halt").disabled'), true);
|
|
const count = await b.evaluate('window.processingRequests.length');
|
|
await b.evaluate(`window.dispatchEvent(new Event('hashchange'));window.dispatchEvent(new Event('focus'));
|
|
document.querySelector('#processing-halt').disabled=false;document.querySelector('#processing-halt').click();true`);
|
|
await new Promise(resolve=>setTimeout(resolve,100));
|
|
assert.equal(await b.evaluate('window.processingRequests.length'), count, 'no processing GET or POST after shared sign-out');
|
|
if(kind==='signin') assert.equal(await b.evaluate(`!!document.querySelector('#signed-out a[href^="/auth/login"]')`), true);
|
|
});
|
|
|
|
|
|
test('processing browser: Dashboard card is reachable at narrow and wide sizes', async t => {
|
|
const { b } = await setup(t);
|
|
for (const [width, height] of [[400, 1000], [1440, 1000], [1440, 420], [800, 420]]) {
|
|
await b.viewport(width, height);
|
|
await b.evaluate('document.querySelector("#processing-link").click(); document.querySelector("#processing-halt").focus(); true');
|
|
assert.equal(await b.evaluate('document.querySelector("#processing-controls").parentElement.id'), 'dashboard-processing');
|
|
assert.equal(await b.evaluate('document.activeElement.id'), 'processing-halt');
|
|
assert.equal(await b.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth'), true);
|
|
}
|
|
});
|