Files
dtf-system/tests/artwork_browser_test.mjs
Cauê Faleiros b7e2ea12d0 feat: accept large JPG and WebP artworks instead of refusing them
Above 150 MB the browser cannot read JPG or WebP in parts, so a large
artwork was refused. It is now measured from its header, graded and priced
with the discount like any other, and placed on the sheet as a full box,
which is exact for JPG. The card says there is no preview and that the team
checks the art before printing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:14:04 -03:00

351 lines
23 KiB
JavaScript

// Isolated upload/editor regression: no API, database, storage, or Docker services.
// Run: node local/artwork_browser_test.mjs
// Optional: ARTWORK_FILE=/absolute/path/to/image.jpg to also inspect a real image.
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
import {createServer} from 'node:http';
import {createHash} from 'node:crypto';
import {mkdtemp, readFile, writeFile, mkdir} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {resolve} from 'node:path';
const html=await readFile('web/index.html','utf8');
const hashes=[...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)]
.filter(m=>! /\bsrc\s*=/i.test(m[1]))
.map(m=>"'sha256-"+createHash('sha256').update(m[2]).digest('base64')+"'");
function pdfFixture({pages=1,media='[0 0 720 360]',crop='',rotate=0,unit=1}={}){
const objects=[
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids ['+Array.from({length:pages},(_,i)=>i+3+' 0 R').join(' ')+
'] /Count '+pages+' /MediaBox '+media+' >>',
...Array.from({length:pages},()=> '<< /Type /Page /Parent 2 0 R'+
(crop?' /CropBox '+crop:'')+(rotate?' /Rotate '+rotate:'')+
(unit!==1?' /UserUnit '+unit:'')+' >>')
];
const chunks=['%PDF-1.6\n%\xE2\xE3\xCF\xD3\n'], offsets=[0];
let length=Buffer.byteLength(chunks[0],'latin1');
for(let i=0;i<objects.length;i++){
offsets.push(length);
const part=(i+1)+' 0 obj\n'+objects[i]+'\nendobj\n';
chunks.push(part);length+=Buffer.byteLength(part,'latin1');
}
const xref=length;
chunks.push('xref\n0 '+(objects.length+1)+'\n0000000000 65535 f \n');
for(const offset of offsets.slice(1))chunks.push(String(offset).padStart(10,'0')+' 00000 n \n');
chunks.push('trailer\n<< /Size '+(objects.length+1)+
' /Root 1 0 R >>\nstartxref\n'+xref+'\n%%EOF\n');
return Buffer.from(chunks.join(''),'latin1');
}
const pdfData=options=>JSON.stringify(pdfFixture(options).toString('base64'));
const server=createServer(async(req,res)=>{
if(req.url.startsWith('/api/')){
res.setHeader('Content-Type','application/json');
res.end(JSON.stringify(req.url==='/api/session'?{cart_scope:'isolated-artwork-test'}:{customer:null}));
return;
}
if(req.url==='/'){
res.setHeader('Content-Type','text/html');
res.setHeader('Content-Security-Policy',`default-src 'self'; script-src 'self' ${hashes.join(' ')}; script-src-attr 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'`);
res.end(html);return;
}
// Serve any script the page asks for, resolved inside web/, rather than
// a hardcoded list: the Site's behaviour is split across several files and a
// list would silently 404 the next one added.
if(/^\/[\w.-]+\.js$/.test(req.url) || /^\/vendor\/pdf\.(worker\.)?min\.js$/.test(req.url)){
try{
const body=await readFile(resolve('web'+req.url));
res.setHeader('Content-Type','text/javascript');res.end(body);return;
}catch{ res.writeHead(404);res.end();return; }
}
res.writeHead(404);res.end();
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));
const profile=await mkdtemp(tmpdir()+'/dtf-artwork-');
const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[
'--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check',
...(process.env.CHROME_NO_SANDBOX==='1'?['--no-sandbox']:[]),
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
],{stdio:['ignore','ignore','pipe']});
let stderr='',ws,next=0;
chrome.stderr.on('data',b=>stderr+=b);
const pending=new Map(),errors=[];
const pause=ms=>new Promise(r=>setTimeout(r,ms));
async function waitFor(fn,label){
const end=Date.now()+15000;
while(Date.now()<end){if(await fn())return;await pause(50);}
throw new Error('Timeout: '+label);
}
function call(method,params={}){
return new Promise((resolve,reject)=>{const id=++next;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
}
async function evaluate(expression){
const r=await call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});
if(r.exceptionDetails)throw new Error(JSON.stringify(r.exceptionDetails));
return r.result.value;
}
const click=selector=>evaluate(`document.querySelector(${JSON.stringify(selector)}).click()`);
async function fill(selector,value){
await evaluate(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.value=${JSON.stringify(String(value))};el.dispatchEvent(new Event('input',{bubbles:true}));})()`);
}
async function packed(count){
await waitFor(()=>evaluate(`window.lastLayout?.pos.length===${count} && !!document.querySelector('#vArea canvas') && Math.abs(metros-window.lastLayout.altura/100)<0.0001`),'packed canvas and matching metres');
return evaluate(`({height:lastLayout.altura, pos:lastLayout.pos, metres:metros, billed:cobrar(metros), mode:modo, heading:$('montcabTit').textContent})`);
}
async function screenshot(name){
// Finish editing as a customer does when leaving the field, refreshing the
// item's quality/details as well as the already-live canvas.
await evaluate(`$('lista').querySelector('[data-cm]')?.dispatchEvent(new Event('change',{bubbles:true}))`);
await waitFor(()=>evaluate(`!!$('vArea').querySelector('canvas') && Math.abs(metros-montagemCm/100)<0.0001`),'screenshot layout');
await evaluate(`$('foco').scrollIntoView({behavior:'instant',block:'start'})`);
const r=await call('Page.captureScreenshot',{format:'png'});
await mkdir('output/local',{recursive:true});
await writeFile('output/local/'+name+'.png',Buffer.from(r.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');
const tab=await(await fetch(`http://127.0.0.1:${port}/json/new?about:blank`,{method:'PUT'})).json();
ws=new WebSocket(tab.webSocketDebuggerUrl);
await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;});
ws.onmessage=event=>{
const m=JSON.parse(event.data);
if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}
else if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails);
};
await call('Runtime.enable');await call('Page.enable');
await call('Emulation.setDeviceMetricsOverride',{width:1440,height:1100,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:`http://127.0.0.1:${server.address().port}/`});
await waitFor(()=>evaluate(`typeof sel==='function' && typeof pintaEntrega==='function'`),'Site scripts');
// Observe the actual engine, without replacing its placement or rendering.
await evaluate(`(()=>{
const original=encaixar;
encaixar=(pieces,width)=>{const result=original(pieces,width);
if(pieces.some(p=>p.a))window.lastLayout={altura:result.altura,pos:result.pos.map(p=>({x:p.x,y:p.y,w:p.w,h:p.h,rot:p.rot}))};
return result;};
window.sendImage=async(type='image/jpeg',drop=false)=>{
const c=document.createElement('canvas');c.width=200;c.height=400;
const ctx=c.getContext('2d');ctx.fillStyle='white';ctx.fillRect(0,0,200,400);
ctx.fillStyle='#222';ctx.fillRect(85,20,20,360);ctx.fillRect(25,100,140,20);
ctx.fillStyle='#e65c00';ctx.fillRect(130,280,45,80);
const blob=await new Promise(r=>c.toBlob(r,type));
const file=new File([blob],type==='image/jpeg'?'artwork.jpg':'artwork.png',{type});
const dt=new DataTransfer();dt.items.add(file);
if(drop)$('zona').dispatchEvent(new DragEvent('drop',{dataTransfer:dt,bubbles:true,cancelable:true}));
else{$('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true}));}
};
// A genuine mounted sheet spans the film width with printable resolution.
window.sendSheet=async()=>{
const cm=larguraFilme(), px=Math.ceil(cm/2.54*150);
const c=document.createElement('canvas');c.width=px;c.height=Math.round(px*0.6);
const ctx=c.getContext('2d');ctx.fillStyle='white';ctx.fillRect(0,0,c.width,c.height);
ctx.fillStyle='#222';ctx.fillRect(40,40,px/4,px/4);
const blob=await new Promise(r=>c.toBlob(r,'image/png'));
const dt=new DataTransfer();dt.items.add(new File([blob],'folha-montada.png',{type:'image/png'}));
$('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true}));
};
window.pickTipo=t=>document.querySelector('#tipoEnvio .cam[data-tipo="'+t+'"]').click();
})()`);
// Reproduce the reported entry path, with no navigation workaround.
// Navigation links must reach the chooser, never preselect a product.
assert.equal(await evaluate(`document.querySelectorAll('[data-modo-cta]').length`),0);
await click('[data-modo="file"]');
// Opening a product paints on the next frame; on a busy runner the check
// must wait for it rather than race it.
await waitFor(()=>evaluate(`!$('tipoEnvio').hidden`),'by-metre product open');
// By-metre opens declaring a mounted sheet; switching is explicit and priced.
assert.deepEqual(await evaluate(`({shown:!$('tipoEnvio').hidden,
on:[...document.querySelectorAll('#tipoEnvio .cam.on')].map(b=>b.dataset.tipo)})`),
{shown:true,on:['folha']});
await evaluate(`(()=>{const f=new File(['x'],'oversize.cdr');Object.defineProperty(f,'size',{value:5*1073741824+1});sel([f]);})()`);
assert.equal(await evaluate(`folhas.length===0 && $('recusa').textContent.includes('5 GB')`),true,
'files beyond the 5 GB upload limit are rejected before browser analysis');
// A multi-GB sheet is the normal order: graded from the pixel size in its
// header, never decoded. 6732 x 35433 px across 57 cm is 3 m at 300 DPI.
await evaluate(`(()=>{const h=new Uint8Array(33);h.set([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A,0,0,0,13,0x49,0x48,0x44,0x52]);
const v=new DataView(h.buffer);v.setUint32(16,6732);v.setUint32(20,35433);h.set([8,6,0,0,0],24);
const f=new File([h],'folha-grande.png');Object.defineProperty(f,'size',{value:3*1073741824});sel([f]);})()`);
await waitFor(()=>evaluate(`folhas.length===1 && !!folhas[0].an`),'large sheet graded from its header');
assert.deepEqual(await evaluate(`({grande:folhas[0].an.grande,dpi:folhas[0].an.dpi,alt:folhas[0].med.alt,preview:!!folhas[0].previewSrc})`),
{grande:true,dpi:300,alt:300,preview:false});
await evaluate(`folhas=[];pintaFolha()`);
await evaluate(`pickTipo('avulsa')`);
assert.equal(await evaluate('modo'),'avulsa');
// A large JPG artwork cannot be read in parts: it is measured from its
// header and placed as a full box, with the discount, instead of refused.
await evaluate(`(()=>{const h=new Uint8Array(20);h.set([0xFF,0xD8,0xFF,0xC0,0,17,8,0x09,0x3A,0x09,0x3A,3]);
const f=new File([h],'arte-grande.jpg');Object.defineProperty(f,'size',{value:400*1048576});sel([f]);})()`);
await waitFor(()=>evaluate(`artes.length===1 && artes[0].semPrevia===true`),'large JPG artwork measured from its header');
await fill('[data-cm]',20);
await waitFor(()=>evaluate(`!!itemAtual?.production?.sources?.length`),'large JPG artwork packed');
assert.deepEqual(await evaluate(`({dpi:Math.round(dpiDe(artes[0])),src:!!artes[0].src,
measurement:itemAtual.production.sources[0].measurement,note:$('lista').textContent.includes('Sem prévia')})`),
{dpi:300,src:false,measurement:'file',note:true});
await evaluate(`artes=[];pintaArtes()`);
await evaluate('sendImage()');
await waitFor(()=>evaluate(`artes.length===1 && !!artes[0].src`),'JPG artwork model');
assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,width:artes[0].cm,quantity:artes[0].q})`),{mode:'avulsa',sheets:0,width:0,quantity:1});
await fill('[data-cm]',20);await fill('[data-q]',6);
let layout=await packed(6);
await waitFor(()=>evaluate(`itemAtual?.production?.sources[0]?.copies===6`),'per-file production record');
assert.deepEqual(await evaluate(`({version:itemAtual.production.version,source:itemAtual.production.sources[0]})`),
{version:2,source:{kind:'artwork',width_cm:20,length_cm:40,copies:6,
rotation_degrees:0,mirrored:false,measurement:'file'}});
assert.equal(await evaluate('itemAtual.production.placements.length'),6);
assert.equal(await evaluate('itemAtual.production.height_cm'),121);
assert.deepEqual(await evaluate('itemAtual.production.placements.map(p=>[p.source_index,p.copy_index,p.x_cm,p.y_cm])'),
[[0,0,0,0],[0,1,20.5,0],[0,2,0,40.5],[0,3,20.5,40.5],[0,4,0,81],[0,5,20.5,81]]);
assert.equal(layout.heading,'Montagem ao vivo');assert.equal(layout.height,121);
assert.equal(layout.billed,1.3);
assert.deepEqual(layout.pos.map(p=>[p.x,p.y]),[[0,0],[20.5,0],[0,40.5],[20.5,40.5],[0,81],[20.5,81]]);
assert.ok(layout.pos.every(p=>p.h/p.w===2 && p.x+p.w<=57));
await screenshot('artwork-packed');
await fill('[data-cm]',15);
await waitFor(()=>evaluate('lastLayout?.altura===60.5 && metros===0.605'),'width updates without blur');
await fill('[data-q]',9);layout=await packed(9);assert.equal(layout.height,91);
await click('[data-giro]');
await waitFor(()=>evaluate('lastLayout?.altura===23.5 && metros===0.235'),'rotation updates packing');
const beforeMirror=await evaluate(`$('vArea').querySelector('canvas').toDataURL()`);
await click('[data-esp]');
await waitFor(()=>evaluate(`$('vArea').querySelector('canvas').toDataURL()!==${JSON.stringify(beforeMirror)}`),'mirror updates pixels');
assert.equal(await evaluate('metros'),0.235);
await waitFor(()=>evaluate(`itemAtual?.production?.sources[0]?.mirrored===true`),'transforms in production record');
assert.deepEqual(await evaluate(`({rotation:itemAtual.production.sources[0].rotation_degrees,copies:itemAtual.production.sources[0].copies})`),
{rotation:90,copies:9});
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='rejected'`),'low resolution refusal');
assert.equal(await evaluate('cartPodeEnviar()'),false);
await fill('[data-cm]',3);
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='ok'`),'rotated DPI uses image height');
assert.equal(await evaluate('Math.round(dpiDe(artes[0]))'),339);
await click('[data-giro]');
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='warning'`),'unrotated resolution warning');
assert.equal(await evaluate('Math.round(dpiDe(artes[0]))'),169);
assert.equal(await evaluate('cartPodeEnviar()'),false);
await click('#cienteOk');
assert.equal(await evaluate('cartPodeEnviar()'),true);
await fill('[data-q]',8);
assert.equal(await evaluate('itemAtual===null'),true,'editing invalidates the old cart immediately');
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='warning'`),'edited warning');
assert.equal(await evaluate('cartPodeEnviar()'),false,'acknowledgement does not survive an edit');
await fill('[data-cm]',58);
await waitFor(()=>evaluate(`itemAtual===null && artes[0].cm===58`),'oversized width rejected');
assert.equal(await evaluate('cartPodeEnviar()'),false);
assert.equal(await evaluate(`(()=>{try{encaixar([{w:58,h:10,img:null}],57);return false}catch(error){return error instanceof RangeError}})()`),true,
'packing engine must not shrink an oversized source');
await click('[data-rm]');
assert.equal(await evaluate(`artes.length===0 && itemAtual===null && !cartPodeEnviar()`),true,
'removed artwork cannot remain in the cart');
// A transparent asymmetric image exposes masks that ignore user transforms.
// Its top-left quarter becomes bottom-right after a mirror and 90° turn.
assert.equal(await evaluate(`(()=>{
const image=document.createElement('canvas');image.width=100;image.height=200;
image.getContext('2d').fillRect(0,0,50,100);
const result=encaixar([{w:20,h:10,img:image,a:{giro:90,esp:true},travado:true}],57);
const m=result.pos[0].m;
return !m.bits[2*m.cw+2] && !m.bits[2*m.cw+30] && !!m.bits[15*m.cw+30];
})()`),true);
// A real finished sheet keeps its own dimensions and by-metre price.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate('sendSheet()');
await waitFor(()=>evaluate('folhas.length===1 && !!folhas[0].previewSrc && metros===0.342'),'ready-sheet image');
assert.deepEqual(await evaluate(`({mode:modo,art:artes.length,title:$('montcabTit').textContent,rate:precoBase(100)})`),{mode:'file',art:0,title:'Sua folha',rate:14.9});
assert.equal(await evaluate(`document.querySelectorAll('[data-cm]').length`),0);
// With a sheet loaded the declaration is locked; it cannot flip under the customer.
assert.equal(await evaluate(`document.querySelector('#tipoEnvio .cam[data-tipo="avulsa"]').disabled`),true);
await evaluate(`(()=>{const el=$('lista').querySelector('[data-repf]');el.value=2;el.dispatchEvent(new Event('change',{bubbles:true}));})()`);
await waitFor(()=>evaluate('metros===0.684'),'ready-sheet repetitions');
assert.equal(await evaluate(`document.querySelectorAll('.folhaPrevia').length`),1);
await evaluate(`(()=>{folhas.push({f:new File(['manual'],'manual.cdr'),med:null,rep:1,m:1,an:null});pintaFolha();})()`);
await waitFor(()=>evaluate(`itemAtual?.nota===0 && itemAtual?.unit===TABELA[modo]`),'mixed analyzed and manual sheets use table pricing');
// An image too small to span the film is refused, never silently repriced.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate('sendImage()');
await waitFor(()=>evaluate(`!!document.getElementById('usarAvulsa')`),'ready-sheet refusal');
assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,art:artes.length})`),{mode:'file',sheets:0,art:0});
await click('#usarAvulsa');
await waitFor(()=>evaluate(`modo==='avulsa' && artes.length===1`),'one-click recovery into loose artwork');
// Keep manual ready-sheet uploads intact.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate(`sel([new File(['sheet'],'sheet.cdr')])`);
await waitFor(()=>evaluate(`!!$('lista').querySelector('[data-comp]')`),'manual ready sheet');
await evaluate(`(()=>{const el=$('lista').querySelector('[data-comp]');el.value=1.01;el.dispatchEvent(new Event('change',{bubbles:true}));})()`);
await waitFor(()=>evaluate('itemAtual?.total===21.89'),'unchanged manual pricing');
await evaluate(`folhas[0].semAnalise='<img src=x onerror=alert(1)>';pintaFolha()`);
assert.equal(await evaluate(`!$('lista').querySelector('img') && $('lista').textContent.includes('<img src=x')`),true,
'PDF parser errors are text, never executable markup');
// Parsed geometry honors inherited MediaBox, CropBox, rotation and UserUnit.
// Extra pages and unreadable PDFs must never fall through to manual pricing.
const pdfFile=(data,name='sheet.pdf')=>`new File([Uint8Array.from(atob(${data}),c=>c.charCodeAt(0))],${JSON.stringify(name)},{type:'application/pdf'})`;
const rotated=await evaluate(`medirFolha(${pdfFile(pdfData({media:'[0 0 720 1440]',crop:'[0 0 360 720]',rotate:90,unit:2}))})`);
assert.deepEqual(rotated,{larg:50.8,alt:25.4,fonte:'página do PDF · UserUnit 2'});
const rendered=await evaluate(`rasterizarPdf(${pdfFile(pdfData({media:'[0 0 720 1440]',crop:'[0 0 360 720]',rotate:90,unit:2}))},50.8,25.4).then(r=>({ok:!!r.tela,error:r.erro||null}))`);
assert.deepEqual(rendered,{ok:true,error:null});
const multi=await evaluate(`medirFolha(${pdfFile(pdfData({pages:2}))})`);
assert.equal(multi.rejected,true);assert.match(multi.reason,/2 páginas/);
const beyondSpec=await evaluate(`medirFolha(${pdfFile(pdfData({media:'[0 0 20000 720]'}))})`);
assert.equal(beyondSpec.rejected,true);assert.match(beyondSpec.reason,/508 cm/);
const broken=await evaluate(`medirFolha(new File(['broken'],'broken.pdf',{type:'application/pdf'}))`);
assert.equal(broken.rejected,true);
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate(`sel([${pdfFile(pdfData({pages:2}),'two-pages.pdf')}])`);
await waitFor(()=>evaluate(`folhas.length===1 && !!folhas[0].measurementError`),'multipage PDF refusal');
assert.equal(await evaluate(`avaliar().pronto`),false);
assert.equal(await evaluate(`cartPodeEnviar()`),false);
assert.match(await evaluate(`$('lista').textContent`),/não pode ser orçado/);
await click('#bVoltar');await click('[data-modo="avulsa"]');
await evaluate(`sel([new File(['not an image'],'broken.png',{type:'image/png'})])`);
await waitFor(()=>evaluate('artes.length===1 && artes[0].decodeError===true'),'failed image decode');
await fill('[data-cm]',20);
await waitFor(()=>evaluate('itemAtual===null && !cartPodeEnviar()'),'undecodable image cannot be quoted');
// PNG drag/drop follows exactly the same artwork path; UV stays UV. Reaching it
// from a by-metre product is one declared click, and it is reversible.
for(const [mode,expected] of [['file','avulsa'],['avulsa','avulsa'],['uvfile','uv'],['uv','uv']]){
await click('#bVoltar');await click('[data-modo="'+mode+'"]');
if(await evaluate(`ehFolha()`))await evaluate(`pickTipo('avulsa')`);
await evaluate(`sendImage('image/png',true)`);
await waitFor(()=>evaluate('artes.length===1 && !!artes[0].src'),'PNG drop');
assert.equal(await evaluate('modo'),expected);
await fill('[data-cm]',10);await fill('[data-q]',4);await packed(4);
}
// An older image load must not overwrite a newer edit, even before debounce.
// The packing loads each artwork's image through imagemDaArte.
await evaluate(`(()=>{window.realLoad=imagemDaArte;window.delayed=[];
imagemDaArte=a=>new Promise(resolve=>delayed.push(()=>realLoad(a).then(resolve)));
})()`);
await fill('[data-q]',5);
await waitFor(()=>evaluate('delayed.length===1'),'delayed preview');
await fill('[data-q]',7);
await evaluate(`imagemDaArte=realLoad;delayed[0]()`);
await packed(7);
const timeoutCleanup=await evaluate(`(async()=>{
const realLoader=carregarPdfJs, realTimer=setTimeout, realWorker=temWorker;
let destroyed=false, fail;
carregarPdfJs=async()=>({getDocument:()=>({
promise:new Promise((_,reject)=>{fail=reject}),
destroy:()=>{destroyed=true;fail(new Error('cancelled'));return Promise.resolve()}
})});
temWorker=true;
window.setTimeout=(fn,ms)=>realTimer(fn,ms===30000?1:ms);
try{
const result=await rasterizarPdf({arrayBuffer:async()=>new ArrayBuffer(1)},10,10);
return {timedOut:result.erro==='demorou demais neste navegador',destroyed};
}finally{carregarPdfJs=realLoader;window.setTimeout=realTimer;temWorker=realWorker;}
})()`);
assert.deepEqual(timeoutCleanup,{timedOut:true,destroyed:true});
// Inspect the supplied local artwork, when requested, using the real file input.
if(process.env.ARTWORK_FILE){
await click('#bVoltar');await click('[data-modo="file"]');
const doc=await call('DOM.getDocument');
const input=await call('DOM.querySelector',{nodeId:doc.root.nodeId,selector:'#inp'});
await call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve(process.env.ARTWORK_FILE)]});
await waitFor(()=>evaluate('artes.length===1 && !!artes[0].src'),'supplied artwork');
await fill('[data-cm]',20);await fill('[data-q]',6);layout=await packed(6);
assert.ok(new Set(layout.pos.map(p=>p.x)).size>1);
assert.ok(new Set(layout.pos.map(p=>p.y)).size>1);
await screenshot('artwork-supplied');
console.log('Supplied image:',JSON.stringify(layout));
}
assert.deepEqual(errors,[]);
console.log('PASS: JPG/PNG upload and drop, packed copies, 5 mm gaps, aspect ratio, live width/quantity/rotation/mirror, metres, ready-sheet pricing/repeats, UV, stale renders, strict inline CSP.');
}catch(error){console.error(error);if(stderr)console.error(stderr.slice(-1200));process.exitCode=1;}
finally{ws?.close();chrome.kill();server.closeAllConnections();await new Promise(r=>server.close(r));}