caterium-app/tests/proposal-quality.spec.mjs
2026-09-18 05:31:15 +03:00

170 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 21 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.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(21);expect(result.choices).toHaveLength(21);
expect(result.fonts).toEqual(['loaded','loaded','loaded']);
expect(new Set(result.out.map(r=>r.pixel)).size).toBe(21);
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 ['light','editorial-grid','neon-menu','premium-emerald']){
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('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 retains the classic, modern and additional collections together',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.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(21);
await expect(page.locator('.sun-v1767-group-label')).toHaveCount(3);
});
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');
});