Compare commits
3 Commits
4ce28aa0d0
...
99fd92bdb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99fd92bdb8 | ||
|
|
483a083a3d | ||
|
|
56275f3aba |
@@ -153,6 +153,14 @@ The browser may display the calculation, but production payment creation must
|
||||
recalculate and validate all price, quantity, freight, and discount values on
|
||||
the server. Client-provided totals are never authoritative.
|
||||
|
||||
Normal JPG/PNG uploads are individual artwork by default, including when uploaded
|
||||
from the by-metre panel. They use the existing loose-artwork editor, printed
|
||||
width, quantity, and live packing canvas; packed height determines metres.
|
||||
Customers uploading an already assembled film-width image must explicitly select
|
||||
the ready-sheet option before uploading. PDF and manual-format ready sheets keep
|
||||
their existing workflow. These models retain their separate approved price tiers;
|
||||
image dimensions alone must never classify a normal artwork as a finished sheet.
|
||||
|
||||
## Freight
|
||||
|
||||
The original visual freight flow in `dtf-site.html` was a stub with only pickup
|
||||
|
||||
100
dtf-site.html
100
dtf-site.html
@@ -258,6 +258,10 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.trocouCam{margin-top:-4px;margin-bottom:11px;padding:10px 12px;border-radius:9px;
|
||||
background:#FFF6E8;border:1px solid #F0D9AE;font-size:11.5px;color:#7A5A16;line-height:1.5}
|
||||
.trocouCam b{color:#B07A18;font-family:"Inter",sans-serif;font-weight:700}
|
||||
.trocarModo{margin:-4px 0 11px;padding:10px 12px;border-radius:9px;background:#F1FBF5;
|
||||
border:1px solid #BFE6CE;font-size:11.5px;color:#1F5C3A;line-height:1.5}
|
||||
.trocarModo b{font-family:"Inter",sans-serif;font-weight:700}
|
||||
.trocarModo label{display:block;margin-top:7px;cursor:pointer}
|
||||
.zona{border:2px dashed var(--laranja);border-radius:13px;padding:44px 20px;text-align:center;
|
||||
cursor:pointer;margin-bottom:14px;background:#FFF6E8;transition:var(--transition)}
|
||||
.zona:hover,.zona.sobre{border-color:var(--laranja2);background:#FFEFD6;
|
||||
@@ -776,10 +780,10 @@ footer a:hover{color:var(--laranja2)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mi on">
|
||||
<a href="#envio">Impressão DTF</a>
|
||||
<a href="#envio" data-modo-cta="avulsa">Impressão DTF</a>
|
||||
<div class="sub">
|
||||
<a href="#envio">DTF Têxtil · 57 cm</a>
|
||||
<a href="#envio">DTF UV · 28,5 cm</a>
|
||||
<a href="#envio" data-modo-cta="avulsa">DTF Têxtil · 57 cm</a>
|
||||
<a href="#envio" data-modo-cta="uv">DTF UV · 28,5 cm</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mi">
|
||||
@@ -863,7 +867,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<p class="sub2">Mande a arte, veja a nota em segundos e a folha montada antes de fechar.
|
||||
<b>Revisão e remanejamento por nossa conta.</b></p>
|
||||
<div class="hcta">
|
||||
<a href="#envio" class="btn1">Enviar minha arte</a>
|
||||
<a href="#envio" class="btn1" data-modo-cta="avulsa">Enviar minha arte</a>
|
||||
<span class="mini">sem cadastro · resposta em segundos</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -996,6 +1000,13 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="foco2" id="foco2">
|
||||
<div class="fila">
|
||||
<div class="trocouCam" id="trocouCam" style="display:none"></div>
|
||||
<div class="trocarModo" id="trocarParaAvulsa" hidden>
|
||||
<b>PNG e JPG entram como artes avulsas.</b> Informe largura e quantidade;
|
||||
montamos a folha, com o preço de artes avulsas.
|
||||
<label><input type="checkbox" id="imagemComoFolha">
|
||||
Minha imagem já é uma folha montada de <span id="larguraFolhaPronta">57</span> cm
|
||||
· usar o preço de arquivo por metro</label>
|
||||
</div>
|
||||
<div class="caminhos" id="caminhos">
|
||||
<button class="cam on" data-cam="auto">
|
||||
<span class="mk2"></span>
|
||||
@@ -1447,9 +1458,23 @@ const px=f=>Math.round(Math.min(6000,Math.max(600,Math.sqrt(f.size/1024)*95)));
|
||||
|
||||
// ── escolha
|
||||
document.querySelectorAll('.ec').forEach(b=>b.addEventListener('click',()=>abrir(b.dataset.modo)));
|
||||
document.querySelectorAll('[data-modo-cta]').forEach(b=>b.addEventListener('click',e=>{
|
||||
e.preventDefault(); abrir(b.dataset.modoCta);
|
||||
}));
|
||||
function abrir(m){
|
||||
modo=m; artes=[]; folhas=[]; metros=0; nota=0; reencaixado=false;
|
||||
montagemCm=0;
|
||||
$('imagemComoFolha').checked=false;
|
||||
$('imagemComoFolha').disabled=false;
|
||||
pintaModo();
|
||||
$('lista').innerHTML=''; $('rA').style.display='none'; recusa([]);
|
||||
recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos();
|
||||
$('vMt').textContent='—'; previaAoVivo(); agendaAvaliacao();
|
||||
limpaPaineis(); // o carrinho só some se o pedido estiver vazio
|
||||
$('foco').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}
|
||||
function pintaModo(){
|
||||
const m=modo;
|
||||
const c=MODOS[m];
|
||||
$('cards').style.display='none'; $('foco').classList.add('on');
|
||||
$('fTit').textContent=c.tit; $('fLg').textContent=c.lg;
|
||||
@@ -1459,13 +1484,10 @@ function abrir(m){
|
||||
$('inp').accept = ehFolha(m) ? '.png,.jpg,.jpeg,.pdf'
|
||||
: '.png,.jpg,.jpeg,.webp';
|
||||
$('catFoco').className='cat '+(ehFolha(m)?'file':'av');
|
||||
$('trocarParaAvulsa').hidden=!ehFolha(m);
|
||||
$('larguraFolhaPronta').textContent=String(c.larg).replace('.',',');
|
||||
$('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos
|
||||
$('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo';
|
||||
$('lista').innerHTML=''; $('rA').style.display='none'; recusa([]);
|
||||
recemChegada=null; caminho='auto'; avisoCam(''); pintaCaminhos();
|
||||
$('vMt').textContent='—'; previaAoVivo(); agendaAvaliacao();
|
||||
limpaPaineis(); // o carrinho só some se o pedido estiver vazio
|
||||
$('foco').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}
|
||||
$('bVoltar').addEventListener('click',()=>{
|
||||
itemAtual=null; $('atual').style.display='none'; $('carrLin').innerHTML=''; pintaPedido();
|
||||
@@ -1503,6 +1525,18 @@ Z.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){I.click();e.pr
|
||||
I.addEventListener('change',()=>sel([...I.files]));
|
||||
function sel(fs){
|
||||
if(!fs.length) return;
|
||||
// Pixels cannot tell a loose artwork from a finished sheet. Images default
|
||||
// to the artwork model unless the customer explicitly declares a ready sheet.
|
||||
// Update this same editor in place; do not reopen it or discard uploaded files.
|
||||
if(ehFolha() && !$('imagemComoFolha').checked && fs.some(f=>ACEITA.avulsa.test(f.name))){
|
||||
if(fs.some(f=>!ACEITA.avulsa.test(f.name))){
|
||||
$('recusa').style.display='block';
|
||||
$('recusa').textContent='Envie artes avulsas e folhas prontas separadamente. Para uma imagem já montada, marque a opção de folha pronta.';
|
||||
return;
|
||||
}
|
||||
modo=modo==='uvfile'?'uv':'avulsa';
|
||||
pintaModo(); pintaCaminhos();
|
||||
}
|
||||
const regra = ehFolha() ? ACEITA.folha : ACEITA.avulsa;
|
||||
const fora = fs.filter(f=>!regra.test(f.name));
|
||||
fs = fs.filter(f=>regra.test(f.name));
|
||||
@@ -1525,6 +1559,7 @@ function sel(fs){
|
||||
pintaCaminhos();
|
||||
}
|
||||
const novas=fs.map(f=>({f, med:null, rep:1, m:0, semAnalise:null}));
|
||||
$('imagemComoFolha').checked=true;
|
||||
folhas=folhas.concat(novas); // soma, não substitui
|
||||
pintaFolha();
|
||||
novas.forEach(x=>{
|
||||
@@ -1847,6 +1882,8 @@ function medirFolha(file){
|
||||
|
||||
function pintaFolha(){
|
||||
const L=larguraFilme();
|
||||
$('imagemComoFolha').disabled=folhas.length>0;
|
||||
if(!folhas.length) $('imagemComoFolha').checked=false;
|
||||
if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; }
|
||||
|
||||
$('lista').innerHTML = folhas.map((x,i)=>{
|
||||
@@ -1999,10 +2036,9 @@ function pintaArtes(){
|
||||
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
|
||||
if(a.cm>0){
|
||||
const dpi=Math.round(a.px/(a.cm/2.54)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
|
||||
const alt=a.prop? a.cm*a.prop : null;
|
||||
barra='<div class="qb2"><i class="'+cls(n)+'" style="width:'+n+'%"></i></div>';
|
||||
calc='<span class="'+cls(n)+'"><b>'+n+'%</b> · '+dpi+' DPI</span><span>'+
|
||||
(a.q>1? a.q+' × ':'')+(a.cm*a.q/100).toFixed(2).replace('.',',')+' m no total</span>';
|
||||
a.q+' × '+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</span>';
|
||||
const sug=sugestao(a);
|
||||
dica = sug
|
||||
? '<div class="dica"><b>Reduzindo para '+n1(sug.alvo)+' cm você economiza '+
|
||||
@@ -2031,7 +2067,13 @@ function pintaArtes(){
|
||||
}).join('');
|
||||
$('lista').querySelectorAll('[data-rm]').forEach(b=>b.addEventListener('click',()=>{artes.splice(+b.dataset.rm,1);pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-cm]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.cm].cm=+i.value||0;pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=+i.value||1;pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.floor(+i.value||1));pintaArtes();}));
|
||||
$('lista').querySelectorAll('[data-cm], [data-q]').forEach(i=>i.addEventListener('input',()=>{
|
||||
const largura=i.hasAttribute('data-cm');
|
||||
const a=artes[+(largura?i.dataset.cm:i.dataset.q)];
|
||||
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.floor(+i.value||1));
|
||||
previaAoVivo();
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-usar]').forEach(b=>b.addEventListener('click',()=>{
|
||||
artes[+b.dataset.usar].cm=+b.dataset.cm; pintaArtes();
|
||||
}));
|
||||
@@ -2328,7 +2370,7 @@ function dilatar(bruto,cw,ch,cheias){
|
||||
}
|
||||
return {cw:cw2,ch:ch2,bits:out,cheias}; // cheias = área de arte, sem a folga
|
||||
}
|
||||
function mascara(img,w,h,rot){
|
||||
function mascara(img,w,h,rot,esp=false){
|
||||
const cw=Math.max(1,Math.ceil(w/CEL)), ch=Math.max(1,Math.ceil(h/CEL));
|
||||
const cv=document.createElement('canvas');
|
||||
cv.width=cw; cv.height=ch;
|
||||
@@ -2339,8 +2381,11 @@ function mascara(img,w,h,rot){
|
||||
return dilatar(bruto,cw,ch,cheias);
|
||||
}
|
||||
ctx.save();
|
||||
if(rot===90){ ctx.translate(cw,0); ctx.rotate(Math.PI/2); ctx.drawImage(img,0,0,ch,cw); }
|
||||
else ctx.drawImage(img,0,0,cw,ch);
|
||||
ctx.translate(cw/2,ch/2);
|
||||
ctx.rotate(rot*Math.PI/180);
|
||||
if(esp) ctx.scale(-1,1);
|
||||
const dw=rot%180===90?ch:cw, dh=rot%180===90?cw:ch;
|
||||
ctx.drawImage(img,-dw/2,-dh/2,dw,dh);
|
||||
ctx.restore();
|
||||
const d=ctx.getImageData(0,0,cw,ch).data;
|
||||
bruto=new Uint8Array(cw*ch); cheias=0;
|
||||
@@ -2386,7 +2431,8 @@ function encaixar(pecas,W){
|
||||
oris.forEach(o=>{
|
||||
if(o.w>W) return;
|
||||
// a máscara segue a medida da orientação escolhida · girada, largura e altura trocam
|
||||
const m = (p.mPronta && o.rot===0) ? p.mPronta : mascara(p.img, o.w, o.h, o.rot);
|
||||
const m = (p.mPronta && o.rot===0) ? p.mPronta : mascara(p.img, o.w, o.h,
|
||||
o.rot+(p.a?.giro||0), p.a?.esp);
|
||||
const xs=new Set([0]), ys=new Set([0]);
|
||||
postas.forEach(q=>{ xs.add(q.cx+q.m.cw); xs.add(q.cx); ys.add(q.cy+q.m.ch); ys.add(q.cy); });
|
||||
const passoX=Math.max(1,Math.round(cols/34));
|
||||
@@ -2416,7 +2462,7 @@ function encaixar(pecas,W){
|
||||
if(!melhor){
|
||||
const k=Math.min(1, W/p.w); // cabe na largura, nem que seja reduzindo
|
||||
const w=p.w*k, h=p.h*k;
|
||||
const m=mascara(p.img,w,h,0);
|
||||
const m=mascara(p.img,w,h,p.a?.giro||0,p.a?.esp);
|
||||
melhor={cx:0,cy:linhas,m,x:0,y:linhas*CEL,w,h,rot:0,ref:p};
|
||||
}
|
||||
// a folga só existe entre peças, não é folha cobrada
|
||||
@@ -2429,8 +2475,9 @@ function encaixar(pecas,W){
|
||||
}
|
||||
|
||||
function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
const PX_CM=W/larguraFilme();
|
||||
const itens=[]; lista.forEach(a=>{ for(let k=0;k<(a.q||1);k++) itens.push(a); });
|
||||
const versao=area.montagemVersao=(area.montagemVersao||0)+1;
|
||||
const filme=larguraFilme(), PX_CM=W/filme;
|
||||
const itens=lista.map(a=>({...a}));
|
||||
if(fora) fora.innerHTML='';
|
||||
if(!itens.length){
|
||||
area.innerHTML='<div class="semmont"><b>A montagem aparece aqui</b>'+
|
||||
@@ -2439,13 +2486,15 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(cargas=>{
|
||||
Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(imagens=>{
|
||||
if(area.montagemVersao!==versao) return;
|
||||
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},()=>({...c})));
|
||||
cargas.forEach(c=>{
|
||||
c.w = c.a.cm; // o motor trabalha em centímetros
|
||||
c.h = c.w * (c.img ? (c.a.giro? c.img.width/c.img.height : c.img.height/c.img.width) : 1);
|
||||
c.travado = !!c.a.giro; // giro do cliente manda no encaixe
|
||||
});
|
||||
const enc=encaixar(cargas,larguraFilme());
|
||||
const enc=encaixar(cargas,filme);
|
||||
const alturaCm=enc.altura;
|
||||
const pos=enc.pos.map(p=>({...p, x:p.x*PX_CM, y:p.y*PX_CM, w:p.w*PX_CM, h:p.h*PX_CM}));
|
||||
const altura=alturaCm*PX_CM;
|
||||
@@ -2463,7 +2512,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
ctx.save();
|
||||
const giro = (p.rot===90?90:0) + (c.a && c.a.giro? 90:0);
|
||||
ctx.translate(p.x+p.w/2, p.y+p.h/2);
|
||||
if(giro%180===90) ctx.rotate(Math.PI/2);
|
||||
ctx.rotate(giro*Math.PI/180);
|
||||
if(c.a && c.a.esp) ctx.scale(-1,1);
|
||||
const dw = giro%180===90 ? p.h : p.w, dh = giro%180===90 ? p.w : p.h;
|
||||
if(c.img) ctx.drawImage(c.img,-dw/2,-dh/2,dw,dh);
|
||||
@@ -2474,7 +2523,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
ctx.fillText(c.a.f.name.split('.').pop().toUpperCase(),p.x+p.w/2,p.y+p.h/2+4);
|
||||
}
|
||||
});
|
||||
molduraFolha(ctx,W,usado,larguraFilme(),alturaCm/100);
|
||||
molduraFolha(ctx,W,usado,filme,alturaCm/100);
|
||||
marcaDagua(ctx,W,cv.height);
|
||||
area.innerHTML=''; area.appendChild(cv);
|
||||
|
||||
@@ -2483,7 +2532,7 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
|
||||
const areaArteCm=enc.pos.reduce((s,p)=>
|
||||
s + (p.m && p.m.cheias!=null ? p.m.cheias*CEL*CEL : p.w*p.h), 0);
|
||||
const uso=Math.max(1,Math.min(100,
|
||||
Math.round(areaArteCm/(larguraFilme()*Math.max(alturaCm,0.1))*100)));
|
||||
Math.round(areaArteCm/(filme*Math.max(alturaCm,0.1))*100)));
|
||||
const el=document.createElement('div');
|
||||
el.className = uso>=70? 'usoBom' : 'usoRuim';
|
||||
el.innerHTML = (uso>=70
|
||||
@@ -2502,6 +2551,8 @@ function previaArtes(){ desenhaMontagem($('pArea'), artes, metros, 560); }
|
||||
// montagem ao vivo, ao lado da fila · redesenha 140 ms depois da ultima mexida
|
||||
let tMont=null;
|
||||
function previaAoVivo(){
|
||||
clearTimeout(tMont);
|
||||
$('vArea').montagemVersao=($('vArea').montagemVersao||0)+1;
|
||||
if(ehFolha()){
|
||||
$('vUso').innerHTML='';
|
||||
const tot=folhaTotalM();
|
||||
@@ -2531,7 +2582,6 @@ function previaAoVivo(){
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTimeout(tMont);
|
||||
tMont=setTimeout(()=>{
|
||||
const prontas=artes.filter(a=>a.cm>0);
|
||||
const W=Math.round(300*ZOOMS[zoomI]);
|
||||
|
||||
187
local/artwork_browser_test.mjs
Normal file
187
local/artwork_browser_test.mjs
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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('dtf-site.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')+"'");
|
||||
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;
|
||||
}
|
||||
if(['/privacy.js','/upload.js','/checkout.js','/cart.js'].includes(req.url)){
|
||||
res.setHeader('Content-Type','text/javascript');
|
||||
res.end(await readFile('local/static'+req.url));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',
|
||||
'--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 AUTO!=='undefined'`),'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}));}
|
||||
};
|
||||
})()`);
|
||||
// Reproduce the reported entry path, with no navigation workaround.
|
||||
await click('[data-modo="file"]');
|
||||
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);
|
||||
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);
|
||||
// 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);
|
||||
// Explicit finished-sheet images retain their separate dimensions and price.
|
||||
await click('#bVoltar');await click('[data-modo="file"]');
|
||||
await click('#imagemComoFolha');await evaluate('sendImage()');
|
||||
await waitFor(()=>evaluate('folhas.length===1 && !!folhas[0].previewSrc && metros===1.14'),'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);
|
||||
await evaluate(`(()=>{const el=$('lista').querySelector('[data-repf]');el.value=2;el.dispatchEvent(new Event('change',{bubbles:true}));})()`);
|
||||
await waitFor(()=>evaluate('metros===2.28'),'ready-sheet repetitions');
|
||||
assert.equal(await evaluate(`document.querySelectorAll('.folhaPrevia').length`),1);
|
||||
// Keep manual ready-sheet uploads intact and refuse ambiguous mixed batches.
|
||||
await click('#bVoltar');await click('[data-modo="file"]');
|
||||
await evaluate(`sel([new File(['sheet'],'sheet.cdr'),new File(['image'],'art.png')])`);
|
||||
assert.deepEqual(await evaluate('[folhas.length,artes.length]'),[0,0]);
|
||||
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');
|
||||
// PNG drag/drop follows exactly the same artwork path; UV stays UV.
|
||||
for(const [mode,expected] of [['file','avulsa'],['avulsa','avulsa'],['uvfile','uv']]){
|
||||
await click('#bVoltar');await click('[data-modo="'+mode+'"]');
|
||||
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.
|
||||
await evaluate(`(()=>{window.realLoad=carregarImagem;window.delayed=[];
|
||||
carregarImagem=f=>new Promise(resolve=>delayed.push(()=>realLoad(f).then(resolve)));
|
||||
})()`);
|
||||
await fill('[data-q]',5);
|
||||
await waitFor(()=>evaluate('delayed.length===1'),'delayed preview');
|
||||
await fill('[data-q]',7);
|
||||
await evaluate(`carregarImagem=realLoad;delayed[0]()`);
|
||||
await packed(7);
|
||||
// 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));}
|
||||
Reference in New Issue
Block a user