Files
stack/packages/webui/tests/w15-requests-browser.test.mjs

89 lines
9.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { setup, state, candidate, draft, evidence, wait, question, REQUEST, DECISION } from './helpers/w15-requests.mjs';
const choices = [{key:'guided',text:'New project guided'}, {key:'autonomous',text:'New project autonomous'}, {key:'existing',text:'Add to existing project'}];
const activeDraft = {...draft, snapshot:{id:'snapshot-1',revision:2}};
const inbox = [{id:DECISION,question:'Choose a mode',console:{kind:'decision',options:choices}}];
const scenarios = [
['mode-choice', state({project:null,phase:'selection',selection:{decision:DECISION},wait:wait('decision','request-mode',DECISION)}), {inbox}],
['project-picker-empty', state({project:null,phase:'selection',route:'existing',wait:wait('setup','request-existing-project',REQUEST)}), {}],
['pm-question', state({question,wait:wait('question','question','q-1')}), {}],
['prd-confirmation', state({phase:'prd-review',candidate,wait:wait('decision','prd-approval',DECISION)}), {draft,allowConfirmation:true,inbox:[{id:DECISION,question:'Approve exact PRD?',console:{kind:'decision',options:[{key:'yes',text:'Approve exact PRD'},{key:'no',text:'Request changes'}]}}]}],
['request-progress', state({phase:'implementation',snapshot:'snapshot-1',candidate,question:{...question,answer:'answer-1'},tasks:['vikunja:2/41']}), {draft:activeDraft}],
['request-refresh-failed', state({question,wait:wait('question','question','q-1')}), {}],
['reviewed-result', state({phase:'complete',snapshot:'snapshot-1',candidate,delivery:evidence.id,delivery_evidence:evidence}), {draft:activeDraft}],
['result-evidence', state({phase:'complete',snapshot:'snapshot-1',candidate,delivery:evidence.id,delivery_evidence:evidence}), {draft:activeDraft}],
['legacy-complete', state({phase:'complete'}), {}],
['pr-link-present-and-absent', state({phase:'complete',snapshot:'snapshot-1',candidate,delivery:evidence.id,delivery_evidence:{...evidence,pull_request:{request:REQUEST,project:'factory',url:'https://example.org/repo/pull/1'}}}), {draft:activeDraft}],
];
test('W15 requests fixture scenarios and screenshots', async t => {
const manifest = [];
for (const [name, current, extra] of scenarios) {
await t.test(name, async t => {
const {b, route, navigate, shot, network, consoleErrors} = await setup(t, {states:{[REQUEST]:current}, ...extra});
for (const width of [1440,390]) {
await navigate(width);
const waitFor = async expression => b.evaluate(`(async()=>{const end=Date.now()+10000;while(!(${expression})){if(Date.now()>end)throw Error('state missing');await new Promise(r=>setTimeout(r,30))}return true})()`);
if (name === 'mode-choice') { await waitFor("document.querySelector('[data-request-choice]')"); assert.match(await b.evaluate("document.querySelector('[data-request-choice=guided]').textContent"), /^Guided/); }
if (name === 'project-picker-empty') { await waitFor("document.querySelector('.request-picker')"); assert.match(await b.evaluate("document.querySelector('.request-picker').textContent"), /No project/); }
if (name === 'prd-confirmation') { await waitFor("document.querySelector('[data-request-choice=yes]')"); await b.evaluate("document.querySelector('[data-request-choice=yes]').click();true"); await waitFor("document.querySelector('[data-request-confirm]')"); }
if (name === 'request-progress') assert.match(await b.evaluate("document.querySelector('.request-panel').textContent"), /Answer received.*not established/s);
if (name === 'legacy-complete') assert.match(await b.evaluate("document.querySelector('.request-panel').textContent"), /Completed before delivery evidence/);
if (name === 'reviewed-result') assert.match(await b.evaluate("document.querySelector('.request-delivery').textContent"), /not landed or deployed.*PR link unavailable/s);
if (name === 'result-evidence') await b.evaluate("document.querySelector('.request-delivery details').open=true;true");
if (name === 'pr-link-present-and-absent') assert.equal(await b.evaluate("document.querySelector('.request-delivery a[target=_blank]').href"), 'https://example.org/repo/pull/1');
if (name === 'request-refresh-failed') {
const observed = await b.evaluate("document.querySelector('.request-observed time').dateTime");
await b.evaluate("(()=>{window.__failure=true;const a=document.querySelector('.request-answer textarea');a.value='Draft kept';a.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('[data-request-reload]').click();return true})()");
await waitFor("document.querySelector('.request-retained-draft')");
assert.match(await b.evaluate("document.querySelector('.request-panel').textContent"), /Could not refresh.*last state/s);
assert.equal(await b.evaluate("document.querySelector('.request-retained-draft').textContent"), 'Draft kept');
assert.equal(await b.evaluate("document.querySelector('.request-observed time').dateTime"), observed);
assert.equal(await b.evaluate('window.__events.length'), 1);
assert.equal(await b.evaluate("document.querySelectorAll('.request-panel form').length"), 0);
}
assert.equal(await b.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth"), true, 'no document overflow');
assert.equal(await b.evaluate("document.querySelector('.request-return').getAttribute('href')"), route);
assert.deepEqual(await b.evaluate('window.__fileCalls.at(-1)'), [REQUEST]);
assert.deepEqual(await b.evaluate('window.__unexpected'), [], 'fixture must not refuse any request');
const files = await shot(name, width);
manifest.push({scenario:name, route, width, height:1000, observation:'Fixture read at 2026-11-05T12:00:00.000Z', files, networkErrors:[...network], consoleErrors:[...consoleErrors], refusedFixtureRequests:await b.evaluate('window.__unexpected')});
}
});
}
if (process.env.MOSAIC_UX_SHOTS) await writeFile(join(process.env.MOSAIC_UX_SHOTS,'manifest.json'), JSON.stringify({candidate:'set after commit', command:'node --test --test-concurrency=1 packages/webui/tests/w15-requests-browser.test.mjs', result:'see w15-tests.txt', scenarios:manifest}, null, 2));
});
test('project summary rereads known IDs, refuses foreign project, and sign-out clears it', async t => {
const {b,navigate} = await setup(t,{states:{[REQUEST]:state({})}}); await navigate(1440);
assert.deepEqual(await b.evaluate(`(async()=>{const el=document.createElement('div');document.body.append(el);window.__summary=el;await MosaicRequests.projectSummary({container:el,requestIds:['${REQUEST}'],project:'other'});return [el.textContent,window.__events.length]})()`), ['Request information unavailable.',1]);
assert.match(await b.evaluate(`(async()=>{await MosaicRequests.projectSummary({container:window.__summary,requestIds:['${REQUEST}'],project:'factory'});return window.__summary.textContent})()`), /factory|discovery/);
await b.evaluate("document.dispatchEvent(new Event('mosaic-signed-out'));true");
assert.equal(await b.evaluate('window.__summary.textContent'), '');
});
test('PR URLs require exact request and project binding and safe HTTPS', async t => {
const {b,navigate} = await setup(t,{states:{[REQUEST]:state({phase:'complete',snapshot:'snapshot-1',delivery:evidence.id,delivery_evidence:{...evidence,pull_request:{request:REQUEST,project:'factory',url:'javascript:alert(1)'}}})}}); await navigate(1440);
assert.equal(await b.evaluate("document.querySelectorAll('.request-delivery a[target=_blank]').length"),0);
assert.match(await b.evaluate("document.querySelector('.request-delivery').textContent"), /PR link unavailable/);
});
test('optional Files module can be absent and keyboard refresh preserves focus', async t => {
const {b,navigate} = await setup(t,{states:{[REQUEST]:state({question,wait:wait('question','question','q-1')})}}); await navigate(390);
await b.evaluate("delete window.MosaicFiles;document.querySelector('[data-request-reload]').focus();true");
await b.key('Enter');
await b.evaluate(`(async()=>{const end=Date.now()+10000;while(!document.querySelector('[data-request-reload]') || document.querySelector('[data-request-reload]').disabled){if(Date.now()>end)throw Error('refresh did not finish');await new Promise(r=>setTimeout(r,30))}return true})()`);
assert.match(await b.evaluate("document.querySelector('[data-request-files]').textContent"), /unavailable/i);
assert.notEqual(await b.evaluate('document.activeElement.tagName'), 'BODY');
await b.key('Tab'); await b.key('Tab','Tab',8); await b.key('Escape');
assert.equal(await b.evaluate("document.querySelector('.request-panel button[type=submit]').getBoundingClientRect().height >= 44"), true);
await b.evaluate("document.documentElement.dataset.mode='dark';true");
await b.call('Emulation.setPageScaleFactor',{pageScaleFactor:2});
assert.equal(await b.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth"),true);
});