// Dependency-free Chrome DevTools smoke test. Node 22+ and Chrome required. import {spawn} from 'node:child_process'; import {mkdtemp,readFile,writeFile,mkdir} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import {resolve} from 'node:path'; import assert from 'node:assert/strict'; try { for(const line of (await readFile('.env','utf8')).split('\n')) { if(!line.startsWith('#') && line.includes('=')) { const i=line.indexOf('=');process.env[line.slice(0,i)]??=line.slice(i+1); } } }catch(e){if(e.code!=='ENOENT')throw e;} const profile=await mkdtemp(tmpdir()+'/dtf-browser-'); const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[ '--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check', '--remote-debugging-port=0','--user-data-dir='+profile,'about:blank' ],{stdio:['ignore','ignore','pipe']}); const pause=ms=>new Promise(r=>setTimeout(r,ms)); let stderr='';chrome.stderr.on('data',data=>stderr+=data.toString()); const clients=[]; async function waitFor(fn,description,timeout=30000){const end=Date.now()+timeout;while(Date.now(){const d=JSON.parse(event.data);if(d.id){const p=this.pending.get(d.id);if(p){this.pending.delete(d.id);d.error?p.reject(d.error):p.resolve(d.result);}}else if(d.method==='Runtime.exceptionThrown')this.errors.push(d.params.exceptionDetails);}; } call(method,params={}){return new Promise((resolve,reject)=>{const id=++this.next;this.pending.set(id,{resolve,reject});this.ws.send(JSON.stringify({id,method,params}));});} async eval(expression){const result=await this.call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(result.exceptionDetails)throw new Error(JSON.stringify(result.exceptionDetails));return result.result.value;} async click(selector){await this.eval(`document.querySelector(${JSON.stringify(selector)}).click()`);} async fill(selector,value,event='input'){await this.eval(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.value=${JSON.stringify(value)};el.dispatchEvent(new Event(${JSON.stringify(event)},{bubbles:true}));})()`);} async text(){return this.eval('document.body.innerText');} async screenshot(path){const result=await this.call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});await writeFile(path,Buffer.from(result.data,'base64'));} } try{ let port; await waitFor(async()=>{try{port=Number((await readFile(profile+'/DevToolsActivePort','utf8')).split('\n')[0]);return true;}catch{return false;}},'Chrome startup'); async function page(url){ const tab=await (await fetch('http://localhost:'+port+'/json/new?'+encodeURIComponent(url),{method:'PUT'})).json(); const ws=new WebSocket(tab.webSocketDebuggerUrl);await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;}); const p=new Page(ws);clients.push(p);await p.call('Runtime.enable');await p.call('Page.enable'); await p.call('Emulation.setDeviceMetricsOverride',{width:1440,height:1000,deviceScaleFactor:1,mobile:false}); await waitFor(()=>p.eval('document.readyState === "complete"'),'page load');return p; } const site=await page('http://localhost:'+(process.env.SITE_PORT||8080)); await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'checkout bridge'); // Prove escaping itself, independently of the CSP's second line of defense. await site.call('Page.setBypassCSP',{enabled:true}); await site.eval(`(()=>{window.xssProbe=0;abrir('file');sel([new File(['test'],'.cdr')]);})()`); await pause(500); assert.equal(await site.eval('window.xssProbe'),0); assert.equal(await site.eval('document.querySelectorAll("#lista img[onerror]").length'),0); assert.ok((await site.text()).includes('.cdr')); await site.eval(`(()=>{recusa([new File(['test'],'bad.')]);})()`); await pause(200);assert.equal(await site.eval('window.xssProbe'),0); await site.call('Page.setBypassCSP',{enabled:false}); await site.call('Page.reload'); await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe'); await site.click('[data-modo="file"]'); await site.click('[data-cam="tabela"]'); const root=await site.call('DOM.getDocument'); const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'}); await site.call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve('local/fixtures/local-test.cdr')]}); await waitFor(()=>site.eval('!!document.querySelector("[data-comp]")'),'manual length field'); await site.fill('[data-comp]','1.01','change'); await waitFor(()=>site.eval('!!itemAtual'),'cart calculation'); assert.equal(await site.eval('itemAtual.total'),21.89); await site.fill('#fCnpj','11222333000181'); await site.fill('#fZap','11999999999'); await site.fill('#fMail','local-browser@example.test'); await pause(800); await site.call('Page.reload'); await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery'); assert.equal(await site.eval('pedido[0].localFiles[0].name'),'local-test.cdr'); assert.equal(await site.eval('pedido[0].total'),21.89); assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false); await site.click('#bPagar'); await waitFor(async()=> (await site.text()).includes('Arquivos enviados.'),'browser upload and quote',45000); const qid=await site.eval('localStorage.getItem("dtf-quote")'); const kanban=await page('http://localhost:'+(process.env.KANBAN_PORT||8081)); await kanban.fill('#user',process.env.OPERATOR_USER||'operator'); await kanban.fill('#password',process.env.OPERATOR_PASSWORD||'local-operator-only'); await kanban.eval('document.getElementById("login").requestSubmit()'); await waitFor(async()=> (await kanban.text()).includes(qid.slice(0,8)),'quote on Kanban'); assert.equal(await kanban.eval('sessionStorage.getItem("dtf-operator")'),null); assert.equal(await kanban.eval('document.getElementById("password").value'),''); await kanban.eval(`(()=>{const card=[...document.querySelectorAll('.review')].find(x=>x.textContent.includes(${JSON.stringify(qid.slice(0,8))}));card.querySelector('[type=checkbox]').click();card.querySelector('form').requestSubmit();})()`); await waitFor(async()=> (await kanban.text()).includes('Aprovada:'),'quote approval'); await site.eval('[...document.querySelectorAll("button")].find(x=>x.textContent==="Atualizar pedido local").click()'); await waitFor(async()=> (await site.text()).includes('Total validado no servidor:'),'approved quote displayed'); await site.eval('[...document.querySelectorAll("button")].find(x=>x.textContent==="Criar pedido pago local").click()'); await waitFor(async()=> (await site.text()).includes('Nenhuma cobrança real.'),'local payment'); await kanban.click('#refresh'); await waitFor(()=>kanban.eval(`board.orders.some(o=>o.quote_id===${JSON.stringify(qid)})`),'paid card'); const oid=await kanban.eval(`board.orders.find(o=>o.quote_id===${JSON.stringify(qid)}).id`); // Click real transition buttons, including rerender after each move. for(const [title,state] of [['Arte tratada','tra'],['Fila de impressão','fil'],['Imprimindo','imp'],['Finalizado','fin']]){ if(state==='fil'){ await kanban.eval(`(()=>{const card=document.querySelector('[data-order="${oid}"]');[...card.querySelectorAll('button')].find(x=>x.textContent==='Arquivos de produção').click();})()`); await waitFor(()=>kanban.eval(`!!document.querySelector('[data-order="${oid}"] [data-final-item]')`),'final upload controls'); const doc=await kanban.call('DOM.getDocument'); const input=await kanban.call('DOM.querySelector',{nodeId:doc.root.nodeId,selector:`[data-order="${oid}"] [data-final-item]`}); await kanban.call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve('local/fixtures/local-test.cdr')]}); await kanban.fill(`[data-order="${oid}"] input[placeholder="Nota da revisão"]`,'Browser test final file'); await kanban.eval(`(()=>{const form=document.querySelector('[data-order="${oid}"] form');form.querySelector('[type=checkbox]').click();form.requestSubmit();})()`); await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').version===2`),'final file approval'); } await kanban.eval(`(()=>{const card=document.querySelector('[data-order="${oid}"]');[...card.querySelectorAll('button')].find(x=>x.textContent===${JSON.stringify('→ '+title)}).click();})()`); await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').state==='${state}'`),'transition '+state); } // Reload proves the board is persisted on the backend. await kanban.call('Page.reload'); await waitFor(()=>kanban.eval(`typeof board!=='undefined' && !!board && board.orders.some(o=>o.id==='${oid}'&&o.state==='fin')`),'persisted board'); await mkdir('output/local',{recursive:true}); await site.eval('window.scrollTo({top:0,behavior:"instant"})'); await waitFor(()=>site.eval('window.scrollY===0'),'screenshot scroll position'); await site.screenshot('output/local/site.png'); await kanban.screenshot('output/local/kanban.png'); const portal=await page('http://localhost:'+(process.env.SITE_PORT||8080)+'/portal.html?order='+oid); await waitFor(async()=> (await portal.text()).includes('Finalizado'),'customer order tracking'); await portal.fill('#cnpj','11222333000181');await portal.fill('#phone','11999999999'); await portal.fill('#register-email','browser-'+Date.now()+'@example.test'); await portal.fill('#register-password','local-browser-password-123'); await portal.eval('document.getElementById("register").requestSubmit()'); await waitFor(()=>portal.eval('document.getElementById("auth").hidden'),'customer registration'); assert.ok((await portal.text()).includes('Finalizado')); await portal.screenshot('output/local/portal.png'); // A logout must clear draft file blobs and metadata, including other open Site tabs. await portal.eval(`(async()=>{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onsuccess=()=>resolve(r.result);r.onerror=reject;});await new Promise((resolve,reject)=>{const tx=db.transaction('cart','readwrite');tx.objectStore('cart').put({items:[new File(['private'],'private.cdr')],expires:Date.now()+86400000},'security-fixture');tx.oncomplete=resolve;tx.onerror=reject;});db.close();})()`); await portal.click('#logout'); await waitFor(()=>portal.eval('document.getElementById("logout").hidden'),'customer logout'); let stored; await waitFor(async()=>{stored=await portal.eval(`(async()=>{try{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});const n=await new Promise((resolve,reject)=>{const r=db.transaction('cart').objectStore('cart').count();r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});db.close();return n;}catch{return -1;}})()`);return stored>=0;},'IndexedDB available after Clear-Site-Data'); assert.equal(stored,0); assert.deepEqual(portal.errors,[]); assert.deepEqual(site.errors,[]);assert.deepEqual(kanban.errors,[]); console.log('PASS: browser Site upload → operator quote → local paid order → all main Kanban states → reload persistence. Order '+oid); console.log('Screenshots: output/local/site.png and output/local/kanban.png'); console.log('PASS: filename XSS escaping with CSP bypassed, no stored operator password, logout clears browser file blobs.'); }catch(error){console.error(error);if(stderr)console.error(stderr.slice(-1500));process.exitCode=1;} finally{for(const p of clients)p.ws.close();chrome.kill();}