226 lines
17 KiB
JavaScript
226 lines
17 KiB
JavaScript
import {test,expect} from '@playwright/test';
|
||
import {demo} from '../ops/demo/trial-demo-data.mjs';
|
||
|
||
test('a stalled photo cannot block the actual offer, and refresh recovers the missing image',async({page})=>{
|
||
test.setTimeout(35000);await boot(page);
|
||
await page.evaluate(d=>{
|
||
boxes=structuredClone(d.boxes.slice(0,6));orders=[{id:'slow-photo',event:'Проверка загрузки',lines:boxes.map(b=>({id:b.id,qty:1})),total:14900,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
||
const fetchOriginal=window.fetch;window.hangPhoto=true;window.photoAborted=false;
|
||
window.fetch=(input,init)=>{
|
||
if(window.hangPhoto&&String(input.url||input).includes('bruschetta-tomato.webp'))return new Promise((_,reject)=>init?.signal?.addEventListener('abort',()=>{window.photoAborted=true;reject(init.signal.reason)},{once:true}));
|
||
return fetchOriginal(input,init);
|
||
};
|
||
void window.sunOpenClientOffer('slow-photo');
|
||
},demo);
|
||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toBeVisible({timeout:14000});
|
||
await expect(page.locator('#sunOfferPdf')).toBeEnabled();await expect(page.locator('#sunOfferAssetWarning')).toContainText('Не загрузились');
|
||
expect(await page.evaluate(()=>window.photoAborted)).toBe(true);
|
||
const words=await page.locator('#sunClientOfferPreview canvas').evaluateAll(cs=>cs.flatMap(c=>c.__proposalLayout).map(t=>t.text||'').join(' '));
|
||
for(const box of demo.boxes.slice(0,6))expect(words.replaceAll('\n',' ')).toContain(box.name);
|
||
await page.evaluate(()=>{window.hangPhoto=false;document.getElementById('sunOfferRefresh').click()});
|
||
await expect(page.locator('#sunOfferAssetWarning')).toBeHidden();await expect(page.locator('#sunOfferPdf')).toHaveText('Скачать PDF');
|
||
});
|
||
|
||
test('unrelated page fonts cannot keep the proposal font loader waiting',async({page})=>{
|
||
await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
Object.defineProperty(document.fonts,'ready',{configurable:true,get:()=>new Promise(()=>{})});
|
||
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:'light'});
|
||
return {pages:pages.length,fonts:[...document.fonts].filter(f=>f.family.includes('Caterium')).map(f=>f.status)};
|
||
},base);
|
||
expect(result.pages).toBeGreaterThan(0);expect(result.fonts).toEqual(['loaded','loaded','loaded']);
|
||
});
|
||
|
||
test('closing a loading offer cancels its response body and does not overwrite a later offer',async({page})=>{
|
||
await boot(page);
|
||
await page.evaluate(d=>{
|
||
boxes=structuredClone(d.boxes.slice(0,2));orders=boxes.map((b,i)=>({id:'cancel-'+i,event:'Мероприятие '+i,lines:[{id:b.id,qty:1}],total:b.price,status:'Новый',clientOfferTemplateId:i?'event-ticket':'light'}));
|
||
const fetchOriginal=window.fetch;window.bodyStarted=false;window.bodyAborted=false;
|
||
window.fetch=async(input,init)=>{
|
||
if(String(input.url||input).includes('bruschetta-tomato.webp')){
|
||
init.signal.addEventListener('abort',()=>{window.bodyAborted=true},{once:true});
|
||
return {ok:true,blob:()=>{window.bodyStarted=true;return new Promise(()=>{})}};
|
||
}
|
||
return fetchOriginal(input,init);
|
||
};
|
||
window.firstOfferDone=false;void sunOpenClientOffer('cancel-0').then(()=>{window.firstOfferDone=true});
|
||
},demo);
|
||
await expect.poll(()=>page.evaluate(()=>window.bodyStarted)).toBe(true);
|
||
await page.evaluate(()=>document.getElementById('sunOfferClose').click());
|
||
await expect.poll(()=>page.evaluate(()=>window.bodyAborted&&window.firstOfferDone)).toBe(true);
|
||
await page.evaluate(()=>sunOpenClientOffer('cancel-1'));
|
||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
||
const words=await page.locator('#sunClientOfferPreview canvas').evaluateAll(cs=>cs.flatMap(c=>c.__proposalLayout).map(t=>t.text||'').join(' '));
|
||
expect(words).toContain('Мероприятие 1');expect(words).not.toContain('Мероприятие 0');
|
||
await expect(page.locator('#sunOfferPdf')).toBeEnabled();await expect(page.locator('#sunOfferAssetWarning')).toBeHidden();
|
||
});
|
||
|
||
test('a render error shows a retry action instead of leaving a disabled loading button',async({page})=>{
|
||
await boot(page);
|
||
await page.evaluate(s=>{
|
||
boxes=[{id:'fixture',name:s.items[0].name,price:12000,category:0,weight:'1200 г',photo:''}];orders=[{id:'retry-offer',event:s.event,lines:[{id:'fixture',qty:2}],total:24000,status:'Новый'}];
|
||
window.realProposalPDF=CateriumProposalPDF;window.CateriumProposalPDF={...CateriumProposalPDF,renderPages:async()=>{throw new Error('Simulated render failure')}};
|
||
void sunOpenClientOffer('retry-offer');
|
||
},base);
|
||
await expect(page.locator('#sunClientOfferPreview')).toContainText('Не удалось подготовить предложение');
|
||
await expect(page.locator('#sunOfferPdf')).not.toHaveText('Готовлю просмотр…');await expect(page.locator('#sunOfferRefresh')).toBeEnabled();
|
||
await page.evaluate(()=>{window.CateriumProposalPDF=window.realProposalPDF;document.getElementById('sunOfferRefresh').click()});
|
||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toBeVisible();await expect(page.locator('#sunOfferPdf')).toBeEnabled();
|
||
});
|
||
|
||
async function boot(page){
|
||
await page.route('https://**',r=>r.abort());
|
||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||
await page.waitForFunction(()=>Boolean(window.sunClientOfferDebugPdfPages&&window.SunOfferTemplate));
|
||
}
|
||
const base={brandName:'Компания Лист',brandCity:'Казань',client:'Анна и Михаил',event:'Ужин для друзей',date:'2026-10-24',guests:20,foodGrams:8000,pricing:{base:24000,manual:2000,promo:0,discount:2000,itemsTotal:22000,delivery:1000,total:23000},items:[{id:'fixture',name:'Брускетта с печёными овощами, творожным сыром и зеленью',categoryId:0,categoryName:'Боксы',qty:2,unitPrice:12000,sum:24000,weight:'1200 г',composition:['Томаты, баклажаны, перец, сливочный сыр и зелень'],photoData:''}]};
|
||
function validLayout(pages){
|
||
return pages.flatMap((p,index)=>p.__proposalLayout.filter(b=>b.x<35||b.x+b.w>965||b.y<0||b.y+b.h>1400).map(b=>({page:index+1,...b})));
|
||
}
|
||
|
||
test('all six selections render their actual distinct covers and local Cyrillic fonts',async({page})=>{
|
||
test.setTimeout(90000);await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
const out=[];
|
||
for(const id of CateriumProposalPDF.CURATED_IDS){
|
||
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:id});
|
||
const text=pages.flatMap(p=>p.__proposalLayout).map(x=>x.text).join('\n');
|
||
out.push({id,cover:pages[0].dataset.sunProposalTemplate,width:pages[0].width,pages:pages.length,text,pixel:pages[0].toDataURL('image/jpeg',.1)});
|
||
pages.forEach(c=>{c.width=0;c.height=0});
|
||
}
|
||
return {out,fonts:[...document.fonts].filter(f=>f.family.includes('Caterium')).map(f=>f.status),choices:SunOfferTemplate.list().map(t=>t.id)};
|
||
},base);
|
||
expect(result.out).toHaveLength(6);expect(result.choices).toHaveLength(6);
|
||
expect(result.fonts).toEqual(['loaded','loaded','loaded']);
|
||
expect(new Set(result.out.map(r=>r.pixel)).size).toBe(6);
|
||
for(const row of result.out){expect(row.cover).toBe(row.id);expect(row.width).toBe(2400);expect(row.text.replaceAll('\n',' ')).toContain(base.items[0].name);expect(row.text).toContain('23');expect(row.text).not.toMatch(/Солнце|SUN \/ CATERING/)}
|
||
});
|
||
|
||
test('flow preserves long Cyrillic names and compositions across pages without text collisions',async({page})=>{
|
||
test.setTimeout(90000);await boot(page);
|
||
const results=await page.evaluate(async({s,validator})=>{
|
||
const valid=eval('('+validator+')'),out=[];
|
||
const long='Очень длинное название блюда с запечённым баклажаном и сливочным сыром ';
|
||
const composition='НачалоСостава '+('Томаты и печёный перец, сливочный сыр; '.repeat(130))+' КонецСостава';
|
||
const sample={...s,event:long.repeat(3),client:long.repeat(2),items:[{...s.items[0],name:long.repeat(4),composition:[composition]}]};
|
||
for(const id of CateriumProposalPDF.CURATED_IDS){
|
||
const pages=await sunClientOfferDebugPdfPages({...sample,offerTemplateId:id});
|
||
const layout=pages.flatMap(p=>p.__proposalLayout),text=layout.map(b=>b.text).join(' ').replace(/\s+/g,' ');
|
||
const collisions=[];
|
||
pages.slice(1).forEach((p,index)=>{const list=p.__proposalLayout;for(let i=0;i<list.length;i++)for(let j=i+1;j<list.length;j++){const a=list[i],b=list[j];if(Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x)>2&&Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y)>2)collisions.push({page:index+2,a:a.role,b:b.role})}});
|
||
out.push({id,bounds:valid(pages),collisions,text});pages.forEach(c=>{c.width=0;c.height=0});
|
||
}return out;
|
||
},{s:base,validator:validLayout.toString()});
|
||
for(const r of results){expect(r.bounds).toEqual([]);expect(r.collisions).toEqual([]);expect(r.text).toContain('НачалоСостава');expect(r.text).toContain('КонецСостава');expect(r.text).not.toContain('…')}
|
||
});
|
||
|
||
test('legacy choices resolve to a selectable design without changing saved orders',async({page})=>{
|
||
await page.addInitScript(()=>localStorage.setItem('sunOfferTemplateV1',JSON.stringify({id:'cream-elegance'})));
|
||
await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
const order={id:'old-design',clientOfferTemplateId:'emerald-gold',clientOfferSnapshot:{...s,offerTemplateId:'emerald-gold'}};
|
||
orders=[order];const before=JSON.stringify(order);
|
||
const pages=await sunClientOfferDebugPdfPages(order.clientOfferSnapshot);
|
||
return {default:SunOfferTemplate.get().id,cover:pages[0].dataset.sunProposalTemplate,unchanged:before===JSON.stringify(order),valid:CateriumProposalPDF.IDS.every(id=>CateriumProposalPDF.CURATED_IDS.includes(CateriumProposalPDF.resolveTemplate(id)))};
|
||
},base);
|
||
expect(result).toEqual({default:'light',cover:'midnight-glass',unchanged:true,valid:true});
|
||
});
|
||
|
||
test('transparent wide, tall and white PNG logos keep their proportions and remain visible on every design',async({page})=>{
|
||
test.setTimeout(90000);await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
const rows=[];
|
||
for(const [shape,color] of [['wide','#214735'],['tall','#214735'],['wide','#ffffff']]){
|
||
const c=document.createElement('canvas');c.width=1400;c.height=800;const ctx=c.getContext('2d');ctx.fillStyle=color;
|
||
const w=shape==='wide'?900:160,h=shape==='wide'?150:560;ctx.fillRect(200,100,w,h);
|
||
const blob=await new Promise(r=>c.toBlob(r,'image/png'));
|
||
const data=await CateriumBranding.saveLogo(new File([blob],'company.png',{type:'image/png'}));
|
||
const saved=new Image();saved.src=data;await saved.decode();
|
||
for(const id of CateriumProposalPDF.CURATED_IDS){
|
||
const pages=await CateriumProposalPDF.renderPages({...s,offerTemplateId:id,logo:data});
|
||
const image=pages[0].__proposalImages[0],canvas=pages[0],scale=canvas.width/1000;
|
||
const bg=Array.from(canvas.getContext('2d').getImageData(20*scale,80*scale,1,1).data);
|
||
const colorAtLogo=Array.from(canvas.getContext('2d').getImageData((image.x+image.w/2)*scale,(image.y+image.h/2)*scale,1,1).data);
|
||
const collisions=pages.flatMap(p=>(p.__proposalImages||[]).flatMap(im=>p.__proposalLayout.filter(b=>Math.min(im.x+im.w,b.x+b.w)-Math.max(im.x,b.x)>2&&Math.min(im.y+im.h,b.y+b.h)-Math.max(im.y,b.y)>2)));
|
||
rows.push({id,shape,color,w:image.w,h:image.h,aspect:image.w/image.h,sourceAspect:saved.width/saved.height,bg,colorAtLogo,collisions});
|
||
pages.forEach(p=>{p.width=0;p.height=0});
|
||
}
|
||
}
|
||
return rows;
|
||
},base);
|
||
expect(result).toHaveLength(18);
|
||
for(const r of result){
|
||
expect(r.aspect).toBeCloseTo(r.sourceAspect,5);expect(r.collisions).toEqual([]);
|
||
if(r.shape==='wide')expect(r.w).toBeGreaterThan(250);else expect(r.h).toBe(76);
|
||
expect(r.colorAtLogo).not.toEqual(r.bg);
|
||
if(r.color==='#ffffff'){expect(r.bg[0]).toBeLessThan(50);expect(r.colorAtLogo[0]).toBeGreaterThan(245)}
|
||
}
|
||
});
|
||
|
||
test('multi-page menus keep a normal price breakdown and grand total together',async({page})=>{
|
||
await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
const rows=[];
|
||
for(const id of CateriumProposalPDF.CURATED_IDS){
|
||
const pages=await CateriumProposalPDF.renderPages({...s,offerTemplateId:id,items:Array.from({length:9},(_,i)=>({...s.items[0],name:s.items[0].name+' '+i}))});
|
||
rows.push({id,prices:pages.map((p,i)=>p.__proposalLayout.some(b=>b.role==='pricing-value')?i:-1).filter(i=>i>=0),total:pages.findIndex(p=>p.__proposalLayout.some(b=>b.role==='total-value'))});
|
||
pages.forEach(p=>{p.width=0;p.height=0});
|
||
}return rows;
|
||
},base);
|
||
for(const r of result){expect(r.prices).toHaveLength(1);expect(r.prices[0]).toBe(r.total)}
|
||
});
|
||
|
||
test('offer settings hide personal data and optional blocks; custom gallery reaches the PDF',async({page})=>{
|
||
await boot(page);
|
||
const result=await page.evaluate(async s=>{
|
||
const c=document.createElement('canvas');c.width=40;c.height=40;const ctx=c.getContext('2d');ctx.fillStyle='#891552';ctx.fillRect(0,0,40,40);const gallery=c.toDataURL();
|
||
window.SunOfferWorkspaceV1769={galleryFor:()=>[gallery]};
|
||
window.SunClientOfferSettings={get:()=>({showClientName:false,showDate:false,showGuests:false,showGallery:true,showBoxCount:false,showPriceBreakdown:false,showAmountPerGuest:false,showControl:false,showExtras:false,showFooterNote:false,texts:{menuTitle:'Специальное меню'}})};
|
||
const draws=[],draw=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(image,...a){draws.push(image.src);return draw.call(this,image,...a)};
|
||
try{
|
||
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:'light',orderId:'local-fixture'}),text=pages.flatMap(p=>p.__proposalLayout).map(b=>b.text).join(' ');
|
||
return {text,gallery:draws.includes(gallery),bounds:pages.flatMap(p=>p.__proposalLayout.filter(b=>b.y+b.h>1400))};
|
||
}finally{CanvasRenderingContext2D.prototype.drawImage=draw}
|
||
},base);
|
||
expect(result.text).toContain('Специальное меню');expect(result.text).not.toContain(base.client);expect(result.text).not.toContain('октября');expect(result.text).not.toContain('Скидка');expect(result.text).not.toContain('гостей');expect(result.gallery).toBe(true);expect(result.bounds).toEqual([]);
|
||
});
|
||
|
||
test('per-order picker presents six curated designs without redundant collection headings',async({page})=>{
|
||
await boot(page);
|
||
await page.evaluate(()=>{
|
||
const grid=document.createElement('div');grid.className='sun-v1764-offer-template-grid';
|
||
for(const id of CateriumProposalPDF.CURATED_IDS){const b=document.createElement('button');b.dataset.v1764OfferTemplate=id;grid.appendChild(b)}document.body.appendChild(grid);
|
||
});
|
||
await expect(page.locator('.sun-v1764-offer-template-grid button')).toHaveCount(6);
|
||
await expect(page.locator('.sun-v1767-group-label')).toHaveCount(0);
|
||
});
|
||
|
||
test('an actual offer downloads the same A4 pages shown in its preview',async({page})=>{
|
||
test.setTimeout(60000);await boot(page);
|
||
await page.evaluate(async s=>{
|
||
boxes=[{id:'pdf-local',name:s.items[0].name,price:12000,category:0,weight:'1200 г',composition:s.items[0].composition,photo:''}];
|
||
orders=[{id:'pdf-local-order',event:s.event,contact:s.client,date:s.date,time:'18:00',guestsCount:20,lines:[{id:'pdf-local',qty:2}],total:24000,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
||
orders[0].clientOfferSnapshot=await sunClientOfferDebugCreateSnapshot(orders[0]);delete orders[0].clientOfferSnapshot.offerTemplateId;
|
||
await window.sunOpenClientOffer('pdf-local-order');
|
||
},base);
|
||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
||
const pageCount=await page.locator('#sunClientOfferPreview canvas').count();
|
||
const downloadPromise=page.waitForEvent('download');
|
||
await page.evaluate(()=>window.sunClientOfferDownload());
|
||
const download=await downloadPromise,stream=await download.createReadStream(),chunks=[];
|
||
for await(const chunk of stream)chunks.push(chunk);
|
||
const body=Buffer.concat(chunks).toString('latin1');
|
||
expect(body.startsWith('%PDF-1.4')).toBe(true);
|
||
expect((body.match(/\/Type \/Page\b/g)||[]).length).toBe(pageCount);
|
||
expect((body.match(/\/MediaBox \[0 0 595\.28 841\.89\]/g)||[]).length).toBe(pageCount);
|
||
expect(body).toContain('/Width 2400');
|
||
// Cloud sync re-emits the default template even when this order's design is unchanged.
|
||
const stable=await page.evaluate(()=>{
|
||
const canvas=document.querySelector('#sunClientOfferPreview canvas');
|
||
window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{id:'cream-elegance'}}));
|
||
return canvas===document.querySelector('#sunClientOfferPreview canvas')&&!document.getElementById('sunOfferPdf').disabled;
|
||
});expect(stable).toBe(true);
|
||
await page.evaluate(()=>SunUXFixV1764.persistOfferTemplate('pdf-local-order','light'));
|
||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','light');
|
||
});
|