// Source/DOM simulation only. This is not a browser, visual, keyboard, or XSS execution test. const fs = require('fs'); const vm = require('vm'); const assert = require('assert/strict'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const ROOT = path.resolve(__dirname, '..'); const OUT = path.join(ROOT, 'evidence'); (async () => { const { normalizeIssues, applyDecisions } = await import(pathToFileURL(path.join(ROOT, 'src/core.mjs')).href); const checks=[], ids=new Map(), requests=[], downloads=[], blobMap=new Map(); class Element { constructor(tag='div'){this.tagName=tag.toUpperCase();this.children=[];this.attributes={};this.events={};this.className='';this._text='';this.hidden=false;this.disabled=false;this.value='';this.dataset={};this.checked=false;this.files=[];this.classList={toggle:(name,on)=>{let a=this.className.split(' ').filter(Boolean);const enabled=on??!a.includes(name);a=a.filter(n=>n!==name);if(enabled)a.push(name);this.className=a.join(' ');},remove:(name)=>{this.className=this.className.split(' ').filter(n=>n!==name).join(' ');}};} set textContent(v){this._text=String(v);this.children=[];} get textContent(){return this._text+this.children.map(c=>c.textContent).join('');} set innerHTML(_){throw Error('Unexpected HTML sink in shipped code');} append(...children){for(const c of children){c.parentNode=this;this.children.push(c);}} replaceChildren(...children){this._text='';this.children=[];this.append(...children);} get childElementCount(){return this.children.length;} addEventListener(type,fn){(this.events[type]??=[]).push(fn);} setAttribute(k,v){this.attributes[k]=String(v);} focus(){} remove(){if(this.parentNode)this.parentNode.children=this.parentNode.children.filter(x=>x!==this);} async dispatch(type){for(const fn of this.events[type]??[])await fn({target:this});} click(){if(this.tagName==='A')downloads.push({url:this.href,name:this.download});else if(!this.disabled)return this.dispatch('click');} } const body=new Element('body'); const html=fs.readFileSync(ROOT+'/public/index.html','utf8'); for(const match of html.matchAll(/id="([^"]+)"/g)){const e=new Element();e.id=match[1];ids.set(e.id,e);body.append(e);} const filters=['all','review','duplicates'].map(f=>{const e=new Element('button');e.className='filter';e.dataset.filter=f;body.append(e);return e;}); function elements(){const out=[];const visit=n=>{out.push(n);n.children.forEach(visit)};visit(body);return out;} const document={body,getElementById:id=>ids.get(id),createElement:tag=>new Element(tag),querySelectorAll:selector=>elements().filter(e=>selector.split(',').some(sel=>e.className.split(' ').includes(sel.trim().slice(1))))}; let current=[], before=[], runNo=0; const sample=[{id:1,title:'Synthetic crash',body:'Test body',labels:['needs-triage']},{id:2,title:'A feature request',body:'Synthetic feature',labels:[]}]; const fetch=async (url,options)=>{ const input=options.body?JSON.parse(options.body):undefined; requests.push({url,input}); let data; if(url==='/api/evaluation')data={available:false}; else if(url==='/api/sample')data={issues:sample}; else if(url==='/api/triage'){ current=normalizeIssues(input);runNo++; const items=current.map((item,i)=>({...item,suggestedLabel:i?'feature':'bug',similarity:.6,margin:.1,needsReview:i===1,evidence:{title:'Synthetic reference',label:i?'feature':'bug',similarity:.7},duplicateOf:i?1:null,duplicateSimilarity:i?.9:0})); data={runId:'mock-'+runNo,items,summary:{total:items.length,ready:1,review:items.length-1,duplicateCandidates:items.length-1},model:{name:'mock-model-for-tests-only',offline:true},timings:{totalMs:1},workflowSteps:['Test stub']}; }else if(url==='/api/apply'){before.push(JSON.parse(JSON.stringify(current)));current=applyDecisions(current,input.decisions);data={revisionId:'rev-'+before.length,issues:current,appliedCount:input.decisions.length};} else if(url==='/api/undo'){current=before.pop();data={issues:current,restoredCount:1};} else throw Error('Unexpected URL: '+url); return {ok:true,status:200,json:async()=>JSON.parse(JSON.stringify(data))}; }; const context={document,fetch,Blob,URL:{createObjectURL:blob=>{const id='blob:mock/'+blobMap.size;blobMap.set(id,blob);return id;},revokeObjectURL:()=>{}},setTimeout:()=>{},console}; vm.createContext(context); const source=fs.readFileSync(ROOT+'/public/app.js','utf8'); vm.runInContext(source.replace(/\}\)\(\);\s*$/, 'globalThis.__test={state,validateIssues,render,runTriage,loadSample,applySelected,undoLast,exportCurrent,exportCSV};})();'),context); const app=context.__test; await app.loadSample(); assert.equal(app.state.currentIssues.length,2); await app.runTriage(); assert.equal(app.state.selected.size,0); assert(ids.get('apply-button').disabled); assert.equal(document.querySelectorAll('.issue-card').length,2); checks.push({check:'Load sample and triage produce API-backed cards with no automatic selection',pass:true}); let checkbox=document.querySelectorAll('.issue-checkbox')[0];checkbox.checked=true;await checkbox.dispatch('change'); let select=document.querySelectorAll('.label-select')[0];select.value='documentation';await select.dispatch('change'); assert.equal(app.state.selected.size,1);assert(!ids.get('apply-button').disabled); await app.applySelected(); assert.equal(requests.at(-1).input.decisions.length,1);assert.equal(requests.at(-1).input.decisions[0].label,'documentation'); assert(current[0].labels.includes('documentation'));assert.deepEqual(current[1].labels,[]); checks.push({check:'Explicit checkbox and label override submit one decision; other issue stays unchanged',pass:true}); await app.undoLast();assert.equal(JSON.stringify(current),JSON.stringify(sample)); checks.push({check:'Undo response restores the local version and clears selection',pass:true}); await filters[2].dispatch('click');assert.equal(document.querySelectorAll('.issue-card').length,1); await filters[1].dispatch('click');assert.equal(document.querySelectorAll('.issue-card').length,1); checks.push({check:'Review and duplicate filters render matching returned flags',pass:true}); app.exportCurrent();let exported=JSON.parse(await blobMap.get(downloads.at(-1).url).text());assert.equal(JSON.stringify(exported.issues),JSON.stringify(sample)); checks.push({check:'JSON download serializes the restored current local copy',pass:true}); ids.get('json-input').value='{broken';await ids.get('json-input').dispatch('input');await app.runTriage(); assert(ids.get('notice').textContent.includes('JSON isn’t valid'));assert(ids.get('apply-button').disabled); checks.push({check:'Invalid JSON is rejected and input edits disable stale selections',pass:true}); const unsafe=[{number:10,title:'',body:'',labels:[{name:''}],privateExtra:'must-not-export'},...['=2+2','+SUM(1,2)','-1+2','@SUM(1,2)','\t=2+2','\r=2+2'].map((title,i)=>({id:11+i,title,body:null,labels:[]}))]; ids.get('file-input').files=[{name:'synthetic.json',size:100,text:async()=>JSON.stringify(unsafe)}]; await ids.get('file-input').dispatch('change');await app.runTriage(); assert(!JSON.stringify(app.state.currentIssues).includes('privateExtra'));assert(!JSON.stringify(app.state.currentIssues).includes('must-not-export')); assert(document.querySelectorAll('.issue-card')[0].textContent.includes('['IMG','SCRIPT','SVG'].includes(e.tagName)).length,0); checks.push({check:'DOM simulation uses text nodes for markup payloads; schema strips extra fields (not a browser XSS execution test)',pass:true}); app.exportCSV();const csv=await blobMap.get(downloads.at(-1).url).text(); for(const prefix of ['=2+2','+SUM(1,2)','-1+2','@SUM(1,2)'])assert(csv.includes('"\''+prefix+'"')); assert(csv.includes('onerror=""evil()""'));assert(!csv.includes('privateExtra')); checks.push({check:'CSV formula prefixes and embedded quotes are escaped; extra fields absent',pass:true}); const idsInHTML=[...html.matchAll(/id="([^"]+)"/g)].map(x=>x[1]);assert.equal(idsInHTML.length,new Set(idsInHTML).size); const referencedIDs=[...source.matchAll(/\$\('([^']+)'\)/g)].map(x=>x[1]);for(const id of referencedIDs)assert(ids.has(id),'missing '+id); assert(!/\.innerHTML\s*=|insertAdjacentHTML|\beval\s*\(|new Function\b/.test(source)); checks.push({check:'Unique HTML IDs, all JS element references present, and no dynamic HTML/eval sink',pass:true}); const report={kind:'Source and simulated DOM checks, not browser verification',checks,passed:checks.length,browserStatus:'Blocked: Chromium Unix socket creation denied by sandbox. Visual layout, real keyboard behavior, browser XSS inertness and actual downloads remain unverified.'}; fs.writeFileSync(OUT+'/ui-dom-report.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2)); })().catch(e=>{console.error(e);process.exitCode=1});