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); });