caterium-app/tests/app.spec.mjs
pavlov346346-source 4aa66d0a56
Caterium v17.7.3 — clients server read
Server-preferred normalized client profiles with legacy fallback, canonical snapshot RPC, preserved order metrics, release metadata and tests.
2026-09-09 12:07:37 +03:00

339 lines
26 KiB
JavaScript
Raw Permalink 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 fs from 'node:fs';
import path from 'node:path';
import { test, expect } from '@playwright/test';
async function loadModule(page,file,globalName){
await page.goto('/index.html', { waitUntil:'domcontentloaded' });
const content=fs.readFileSync(path.join(process.cwd(),'public','core',file),'utf8');
await page.addScriptTag({content});
expect(await page.evaluate(name=>Boolean(window[name]),globalName)).toBeTruthy();
}
async function injectCore(page,file,globalName){
const content=fs.readFileSync(path.join(process.cwd(),'public','core',file),'utf8');
await page.addScriptTag({content});
if(globalName)expect(await page.evaluate(name=>Boolean(window[name]),globalName)).toBeTruthy();
}
test('legacy localStorage payload cannot execute XSS', async ({ page }) => {
const pageErrors=[]; page.on('pageerror',e=>pageErrors.push(String(e)));
await page.addInitScript(() => {
localStorage.setItem('sunBoxes', JSON.stringify([{id:'1',name:'<img src=x onerror="window.__cateriumXss=1">',price:100,photo:'javascript:alert(1)',ingredients:[['<svg onload="window.__cateriumXss=2">',1,'шт.']]}]));
localStorage.setItem('sunOrders', JSON.stringify([{id:1,event:'<img src=x onerror="window.__cateriumXss=3">',date:'2026-09-07',time:'12:00',address:'<svg onload="window.__cateriumXss=4">',status:'Новый',lines:[{id:'1',qty:1}]}]));
});
await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(300);
expect(await page.evaluate(()=>window.__cateriumXss)).toBeUndefined();
expect(pageErrors.filter(x=>!x.includes('supabase'))).toEqual([]);
});
test('safe insert helper handles foreign reference node', async ({ page }) => {
await loadModule(page,'sun-safe.js','SunSafe');
const result=await page.evaluate(()=>{const a=document.createElement('div'),b=document.createElement('div'),n=document.createElement('span'),foreign=document.createElement('i');a.appendChild(foreign);document.body.append(a,b);try{window.SunSafe.insertBefore(b,n,foreign);return {ok:true,parent:n.parentNode===b}}catch(e){return {ok:false,error:String(e)}}});
expect(result).toEqual({ok:true,parent:true});
});
test('shared PDF engine produces A4 PDF blob', async ({ page }) => {
await loadModule(page,'pdf-engine.js','SunPdfEngine');
const result=await page.evaluate(async()=>{const fake=new Uint8Array([255,216,255,217]);const blob=window.SunPdfEngine.fromJpegs([{width:1000,height:1414,bytes:fake}]);const head=new TextDecoder().decode(new Uint8Array(await blob.arrayBuffer()).slice(0,8));return {type:blob.type,size:blob.size,head,w:window.SunPdfEngine.PAGE_W,h:window.SunPdfEngine.PAGE_H}});
expect(result.type).toBe('application/pdf'); expect(result.size).toBeGreaterThan(150); expect(result.head.startsWith('%PDF-1.4')).toBeTruthy(); expect(result.w/result.h).toBeCloseTo(1/Math.sqrt(2),3);
});
test('chat photo guard compresses a large camera image', async ({ page }) => {
await loadModule(page,'performance.js','SunAttachmentGuard');
const result=await page.evaluate(async()=>{
const c=document.createElement('canvas');c.width=3000;c.height=2200;const x=c.getContext('2d');
const g=x.createLinearGradient(0,0,c.width,c.height);g.addColorStop(0,'#172a3a');g.addColorStop(.5,'#d79a4a');g.addColorStop(1,'#f1e3ca');x.fillStyle=g;x.fillRect(0,0,c.width,c.height);
for(let i=0;i<1200;i++){x.fillStyle=`rgba(${i%255},${(i*7)%255},${(i*13)%255},.55)`;x.fillRect((i*31)%3000,(i*47)%2200,90,55)}
const blob=await new Promise(r=>c.toBlob(r,'image/jpeg',1));const input=new File([blob],'camera-photo.jpg',{type:'image/jpeg'});const out=await window.SunAttachmentGuard.prepareAttachment(input);const bm=await createImageBitmap(out.file);const data={original:input.size,size:out.file.size,width:bm.width,height:bm.height,type:out.file.type,name:out.file.name};bm.close();return data;
});
expect(Math.max(result.width,result.height)).toBeLessThanOrEqual(2048);expect(result.size).toBeLessThan(result.original);expect(result.size).toBeLessThanOrEqual(15*1024*1024);expect(result.type).toBe('image/jpeg');expect(result.name.endsWith('.jpg')).toBeTruthy();
});
test('v17.6.2 operations UX boots with menu and route features', async ({ page }) => {
await page.goto('/index.html', { waitUntil:'domcontentloaded' });
await page.evaluate(()=>{
if(!document.querySelector('header nav')){
const header=document.createElement('header');
const nav=document.createElement('nav');
header.appendChild(nav);
document.body.appendChild(header);
}
window.SunCloudV2={
getSupportMode:()=>null,
isSupportMode:()=>false,
hasPermission:()=>true,
getSession:()=>({user:{id:'e2e'}})
};
window.editBox=()=>{};
});
const content=fs.readFileSync(path.join(process.cwd(),'public','core','ops-ux-v1762.js'),'utf8');
await page.addScriptTag({content});
await page.waitForFunction(()=>Boolean(window.SunOpsUXV1762),null,{timeout:5000});
const checks=await page.evaluate(()=>window.SunOpsUXV1762.checks());
expect(checks.version).toBe('17.6.2');
expect(checks.menuPage).toBeTruthy();
expect(checks.routeStartKey).toBe('sunRouteBaseV1');
expect(checks.routeOrderPopup).toBeTruthy();
expect(checks.calendarMore).toBeTruthy();
expect(await page.locator('header nav button', {hasText:'Меню'}).count()).toBeGreaterThan(0);
});
test('v17.6.3 developer gate bypasses workspace loading and SaaS click is safe', async ({ page }) => {
await page.goto('/index.html', { waitUntil:'domcontentloaded' });
await page.evaluate(()=>{
document.body.innerHTML='<div id="sunCloudAuthGateV3"><div class="sun-cloud-auth-card"><h2>Загружаю рабочую базу</h2></div></div><button type="button" data-saas-admin>Управление SaaS</button>';
window.__devOpenArgs=[];
window.SunCloudV2={
getSession:()=>({user:{id:'dev',email:'developer@example.com'}}),
getWorkspace:()=>null,
status:()=>({membershipsLoading:true,membershipsLoaded:false}),
signOut:()=>{}
};
window.SunDeveloperV22={
open:arg=>{window.__devOpenArgs.push(arg===null?'null':arg?.constructor?.name||typeof arg)},
isPlatformAdmin:()=>true,
checkPlatformAdmin:async()=>true
};
});
const content=fs.readFileSync(path.join(process.cwd(),'public','core','hotfix-v1763.js'),'utf8');
await page.addScriptTag({content});
await page.waitForFunction(()=>document.querySelector('[data-open-dev-v1763]')&&Boolean(window.SunHotfixV1763),null,{timeout:5000});
expect(await page.locator('#sunCloudAuthGateV3 h2').textContent()).toBe('Аккаунт разработчика');
await page.locator('[data-saas-admin]').click();
await page.locator('[data-open-dev-v1763]').click();
const args=await page.evaluate(()=>window.__devOpenArgs);
expect(args).toEqual(['null','null']);
});
test('v17.6.4 auto-completes and fully pays an order one minute after scheduled time', async ({ page }) => {
await page.addInitScript(()=>{
localStorage.setItem('sunOrders',JSON.stringify([{id:764,event:'Тест авто-завершения',date:'2099-09-07',time:'12:00',status:'Новый',prepayment:100,total:1500,lines:[]}]))
});
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await injectCore(page,'ux-fixes-v1764.js','SunUXFixV1764');
const result=await page.evaluate(()=>{
window.orderTotalValue=()=>1500;
const run=window.SunUXFixV1764.autoCompleteOrders(new Date('2099-09-07T12:01:01').getTime(),{silent:true});
const order=JSON.parse(localStorage.getItem('sunOrders')||'[]').find(x=>String(x.id)==='764');
return {run,prepayment:order?.prepayment,balance:order?.balance,status:order?.status,auto:Boolean(order?.sunAutoCompletedAt),classDate:window.SunUXFixV1764.classificationDate(order,'2099-09-07')};
});
expect(result.run.changed).toBe(1);
expect(result.prepayment).toBe(1500);
expect(result.balance).toBe(0);
expect(result.status).toBe('Отдан заказчику');
expect(result.auto).toBeTruthy();
expect(result.classDate).toBe('0001-01-01');
});
test('v17.6.4 keeps proposal designs per order and renders a styled menu icon', async ({ page }) => {
await page.addInitScript(()=>{
localStorage.setItem('sunOrders',JSON.stringify([
{id:801,event:'Editorial',date:'2099-09-08',time:'12:00',clientOfferSnapshot:{version:8,offerTemplateId:'editorial-grid'}},
{id:802,event:'Emerald',date:'2099-09-08',time:'13:00',clientOfferSnapshot:{version:8,offerTemplateId:'emerald-gold'}}
]));
});
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{
let nav=document.querySelector('header nav');
if(!nav){const header=document.createElement('header');nav=document.createElement('nav');header.appendChild(nav);document.body.prepend(header)}
let menu=nav.querySelector('.nav-menu');if(!menu){menu=document.createElement('button');menu.type='button';menu.className='sun-nav-button nav-menu';menu.dataset.navLabel='Меню';menu.textContent='Меню';nav.appendChild(menu)}
});
await injectCore(page,'ux-fixes-v1764.js','SunUXFixV1764');
await page.waitForFunction(()=>document.querySelector('header nav .nav-menu')?.dataset?.sunMenuIconV1766==='1',null,{timeout:5000});
const result=await page.evaluate(()=>{
window.SunUXFixV1764.persistOfferTemplate('801','midnight-glass');
const list=JSON.parse(localStorage.getItem('sunOrders')||'[]');
const one=list.find(x=>String(x.id)==='801'),two=list.find(x=>String(x.id)==='802');
const menu=document.querySelector('header nav .nav-menu');return {one:one?.clientOfferTemplateId,snapOne:one?.clientOfferSnapshot?.offerTemplateId,two:two?.clientOfferSnapshot?.offerTemplateId,icon:menu?.dataset?.sunMenuIconV1766==='1',inlineIcon:menu?.querySelectorAll('.sun-v1764-menu-icon').length||0};
});
expect(result.one).toBe('midnight-glass');
expect(result.snapOne).toBe('midnight-glass');
expect(result.two).toBe('emerald-gold');
expect(result.icon).toBeTruthy();
expect(result.inlineIcon).toBe(0);
});
test('Developer Console memory refresh is bounded and does not react to its own DOM update', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const performance=fs.readFileSync(path.join(process.cwd(),'public','core','performance.js'),'utf8');
await page.evaluate(()=>{
document.body.innerHTML='<section id="sun-developer-console-v22" class="on"><div id="sunDevBody"></div><span id="sunDevReleaseVersion"></span></section>';
window.__rpcCount=0;
window.SunCloudV2={getClient:()=>({rpc:async()=>{window.__rpcCount++;return {data:{server_size:'35 MB',database_size:'21 MB',storage_size:'14 MB',storage_objects:12}}}})};
});
await page.addScriptTag({content:performance});
await page.waitForFunction(()=>Boolean(window.SunPerformance),null,{timeout:5000});
await page.evaluate(async()=>{await window.SunPerformance.refreshDeveloperMemory(true);await new Promise(r=>setTimeout(r,250));});
const result=await page.evaluate(()=>({count:window.__rpcCount,box:document.querySelectorAll('#sunDevMemoryV1761').length,version:document.getElementById('sunDevReleaseVersion')?.textContent}));
expect(result.count).toBeLessThanOrEqual(2);
expect(result.box).toBe(1);
expect(result.version).toBe('17.7.3');
});
test('mobile body does not overflow viewport', async ({ page }, testInfo) => {
test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth}));
expect(dims.scrollWidth).toBeLessThanOrEqual(dims.innerWidth+2); expect(dims.bodyWidth).toBeLessThanOrEqual(dims.innerWidth+2);
});
test('v17.6.5 stays free of timer page errors during idle', async ({ page }, testInfo) => {
test.skip(testInfo.project.name!=='desktop');
const errors=[];page.on('pageerror',e=>errors.push(String(e)));
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.waitForTimeout(5500);
expect(errors).toEqual([]);
const runtimeSource=fs.readFileSync(path.join(process.cwd(),'public','app-runtime.js'),'utf8');
expect(runtimeSource).toContain("const VERSION = '17.7.3'");
});
test('v17.6.5 support refresh uses lightweight cloud API', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{try{window.SunOpsUXV1762?.disconnect?.()}catch(_){}document.getElementById('sunOpsUXV1762Script')?.remove();window.SunOpsUXV1762=undefined;window.__lightRefresh=0;window.__fullEnter=0;let nav=document.querySelector('header nav');if(!nav){const header=document.createElement('header');nav=document.createElement('nav');header.appendChild(nav);document.body.prepend(header)}window.SunCloudV2={getSupportMode:()=>({workspaceId:'support-test',name:'Тест'}),isSupportMode:()=>true,hasPermission:()=>false,getSession:()=>({user:{id:'dev'}}),refreshSupportWorkspace:async()=>{window.__lightRefresh++;return true},enterSupportWorkspace:async()=>{window.__fullEnter++;return true}};window.editBox=window.editBox||(()=>{});});
await injectCore(page,'ops-ux-v1762.js');
await page.waitForFunction(()=>Boolean(window.SunOpsUXV1762),null,{timeout:5000});
await page.evaluate(()=>window.SunOpsUXV1762.refreshSupport(true));
const counts=await page.evaluate(()=>({light:window.__lightRefresh,full:window.__fullEnter}));
expect(counts.light).toBe(1);expect(counts.full).toBe(0);
});
test('v17.6.5 developer hotfix does not poll admin every second', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{window.SunHotfixV1763=undefined;window.__devChecks=0;window.SunCloudV2={getSession:()=>({user:{id:'dev'}}),getWorkspace:()=>null};window.SunDeveloperV22={open:()=>{},isPlatformAdmin:()=>false,checkPlatformAdmin:async()=>{window.__devChecks++;return false}};});
await injectCore(page,'hotfix-v1763.js','SunHotfixV1763');
await page.waitForTimeout(2600);
expect(await page.evaluate(()=>window.__devChecks)).toBeLessThanOrEqual(2);
});
test('v17.6.6 Menu sidebar icon uses native mask and has no duplicate inline SVG', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{let nav=document.querySelector('header nav');if(!nav){const h=document.createElement('header');nav=document.createElement('nav');h.appendChild(nav);document.body.prepend(h)}let b=nav.querySelector('.nav-menu');if(!b){b=document.createElement('button');b.className='sun-nav-button nav-menu';b.dataset.navLabel='Меню';b.textContent='Меню';nav.appendChild(b)}});
await injectCore(page,'ux-fixes-v1764.js','SunUXFixV1764');
await page.waitForFunction(()=>document.querySelector('.nav-menu')?.dataset?.sunMenuIconV1766==='1',null,{timeout:5000});
const r=await page.evaluate(()=>{const b=document.querySelector('.nav-menu');return {inline:b.querySelectorAll('.sun-v1764-menu-icon').length,marker:b.dataset.sunMenuIconV1766,label:b.textContent.trim()}});
const uxSource=fs.readFileSync(path.join(process.cwd(),'public','core','ux-fixes-v1764.js'),'utf8');
expect(r.inline).toBe(0);expect(r.marker).toBe('1');expect(r.label).toContain('Меню');expect(uxSource).toContain('--sun-icon:url(');
});
test('v17.6.6 proposal templates preserve selection and expose four structural previews', async () => {
const runtimeSource=fs.readFileSync(path.join(process.cwd(),'public','app-runtime.js'),'utf8');
const uxSource=fs.readFileSync(path.join(process.cwd(),'public','core','ux-fixes-v1764.js'),'utf8');
expect(runtimeSource).toContain("OFFER_TEMPLATE_IDS.has(explicitTemplate)");
for(const marker of ['mini-light-hero','mini-editorial-hero','mini-midnight-total','mini-emerald-gold'])expect(runtimeSource+uxSource).toContain(marker);
});
test('v17.6.7 classic client PDF renders distinctive cover and keeps preview/download page parity', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const source=fs.readFileSync(path.join(process.cwd(),'public','core','classic-offer-pdf-v1767.js'),'utf8');
await page.addScriptTag({content:source});
await page.waitForFunction(()=>Boolean(window.SunClassicOfferPDFV1767),null,{timeout:5000});
const result=await page.evaluate(async()=>{
const c=document.createElement('canvas');c.width=1600;c.height=1000;const x=c.getContext('2d');x.fillStyle='#d9a757';x.fillRect(0,0,c.width,c.height);x.fillStyle='#173a2d';x.fillRect(500,120,700,700);const photo=c.toDataURL('image/jpeg',.8);
const snapshot={id:'1767',client:'Анна',event:'День рождения',date:'2026-09-20',time:'18:30',guests:24,offerTemplateId:'event-ticket',pricing:{base:23517,itemsTotal:23517,total:23517},items:[{id:'a',name:'Фуршетный бокс',qty:3,categoryId:0,photoData:photo,weight:'1500 г'}]};
const fakeBase=async()=>{const base=document.createElement('canvas');base.width=1600;base.height=2262;return [base]};
const pages=await window.SunClassicOfferPDFV1767.renderPages(snapshot,fakeBase);return {count:pages.length,id:pages[0].dataset.sunClassicTemplate,cover:pages[0].dataset.sunClassicCover,classic:window.SunClassicOfferPDFV1767.CLASSIC_IDS.length,archive:window.SunClassicOfferPDFV1767.ARCHIVE_IDS.length};
});
expect(result.count).toBe(2);expect(result.id).toBe('event-ticket');expect(result.cover).toBe('1');expect(result.classic).toBe(10);expect(result.archive).toBe(3);
});
test('v17.6.8 Developer Console UX module boots', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const source=fs.readFileSync(path.join(process.cwd(),'public','core','developer-console-v1768.js'),'utf8');
await page.evaluate(()=>{
document.body.innerHTML='<section id="sun-developer-console-v22" class="view on"><div class="sun-dev-tabs"><button data-dev-tab="overview" class="on">Обзор</button><button data-dev-tab="companies">Компании</button><button data-dev-tab="accounts">Аккаунты</button><button data-dev-tab="plans">Тарифы</button><button data-dev-tab="journal">Журнал</button></div><div id="sunDevBody"></div></section>';
window.SunDeveloperV22={open:async()=>true,activateTab:async()=>true};
window.SunCloudV2={getClient:()=>null};
window.SunEnterprise={toast:()=>null};
});
await page.addScriptTag({content:source});
await page.waitForFunction(()=>Boolean(window.SunDeveloperUXV1768),null,{timeout:5000});
const result=await page.evaluate(()=>window.SunDeveloperUXV1768.checks());
expect(result.version).toBe('17.6.8');
});
test('v17.6.9 offer workspace module boots', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const source=fs.readFileSync(path.join(process.cwd(),'public','core','offer-workspace-v1769.js'),'utf8');
await page.evaluate(()=>{
document.body.innerHTML='<div id="sunClientOfferModal" class="modal on"><div class="dialog"><div class="sun-offer-modal-head"><button id="sunOfferPdf">Скачать PDF</button><button id="sunOfferEdit">Редактировать</button></div><section id="sunOfferEditor"><div class="sun-offer-editor-gallery"><div class="sun-offer-editor-gallery-images"><img src="offer-gallery/001.jpg"><img src="offer-gallery/002.jpg"></div></div></section><section id="sunOfferTemplateV1764"></section><div class="sun-offer-frame-wrap"><div id="sunClientOfferPreview"></div></div></div></div>';
window.sunOpenClientOffer=async()=>true;window.sunOpenCurrentClientOffer=async()=>true;
window.SunClassicOfferPDFV1767={renderPages:async()=>[]};window.SunEnterprise={toast:()=>null};
});
await page.addScriptTag({content:source});
await page.waitForFunction(()=>Boolean(window.SunOfferWorkspaceV1769),null,{timeout:5000});
const result=await page.evaluate(()=>window.SunOfferWorkspaceV1769.checks());
expect(result.version).toBe('17.6.9');
expect(result.tabs).toBe(true);
expect(await page.locator('[data-offer-gallery-slot]').count()).toBe(2);
});
test('v17.7.1 data layer can update orders without direct UI globals', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
const result=await page.evaluate(()=>{const d=window.CateriumDataV1773;d.orders.replace([{id:77,status:'Новый',total:1000}],{persistLocal:false,reason:'e2e-seed'});const before=d.orders.get(77);d.orders.update(77,o=>({...o,status:'Тест'}),{persistLocal:false});return {before:before.status,after:d.orders.get(77).status,version:d.VERSION,alias:window.CateriumDataV1770===d}});
expect(result).toEqual({before:'Новый',after:'Тест',version:'17.7.3',alias:true});
});
test('v17.7.0 server automation applies returned normalized orders through data layer', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{window.__applied=[];window.__signed=false;window.CateriumDataV1770={isSupportReadOnly:()=>false,diagnostics:{measure:async(_n,fn)=>fn()},orders:{applyServerChanges:(orders)=>window.__applied.push(...orders)}};window.CateriumData=window.CateriumDataV1770;window.SunCloudV2={getSession:()=>window.__signed?({user:{id:'u'}}):null,getWorkspace:()=>({id:'w'}),getClient:()=>({rpc:async()=>({data:{changed:1,orders:[{id:8,status:'Отдан заказчику'}]},error:null})})};});
const source=fs.readFileSync(path.join(process.cwd(),'public','core','server-automation-v1770.js'),'utf8');await page.addScriptTag({content:source});
await page.waitForFunction(()=>Boolean(window.CateriumServerAutomationV1770));await page.evaluate(async()=>{window.__signed=true;await window.CateriumServerAutomationV1770.run({force:true})});
expect(await page.evaluate(()=>window.__applied)).toEqual([{id:8,status:'Отдан заказчику'}]);
});
test('v17.7.1 data layer serves orders and catalog with v17.7.0 compatibility alias', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
const result=await page.evaluate(()=>{
const d=window.CateriumDataV1773,beforeOrders=d.orders.list(),beforeCatalog=d.catalog.list();
d.orders.replace([...beforeOrders,{id:'qa-v1771',event:'QA',lines:[]}],{persistLocal:false,reason:'qa-order'});
d.catalog.replace([...beforeCatalog,{id:'qa-v1771',name:'QA item',price:0,ingredients:[]}],{persistLocal:false,reason:'qa-catalog'});
const hasOrder=Boolean(d.orders.get('qa-v1771')),hasCatalog=Boolean(d.catalog.get('qa-v1771'));
d.orders.replace(beforeOrders,{persistLocal:false,reason:'qa-restore'});d.catalog.replace(beforeCatalog,{persistLocal:false,reason:'qa-restore'});
return {version:d.VERSION,alias:window.CateriumDataV1770===d,hasOrder,hasCatalog};
});
expect(result).toEqual({version:'17.7.3',alias:true,hasOrder:true,hasCatalog:true});
});
test('v17.7.3 client data layer uses phone then normalized name identity', async ({ page }) => {
await page.addInitScript(()=>{
localStorage.setItem('sunOrders',JSON.stringify([{id:1,contact:' Иван Петров ',phone:'',address:'A',total:1000,lines:[]},{id:2,contact:'иван петров',phone:'',address:'B',total:2000,lines:[]},{id:3,contact:'Анна',phone:'+7 (999) 123-45-67',total:3000,lines:[]}]))
localStorage.setItem('sunClientLoyaltyV1',JSON.stringify({'n:иван петров':{enabled:true,fixedDiscount:5}}));
localStorage.setItem('sunClientCommunicationV1',JSON.stringify({'p:79991234567':{preferred:'telegram'}}));
});
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
const result=await page.evaluate(()=>{
const d=window.CateriumDataV1773,clients=d.clients.list();
return {version:d.VERSION,nameKey:d.clients.keyFromValues('', ' Иван Петров '),phoneKey:d.clients.keyFromValues('+7 (999) 123-45-67','Анна'),ivan:clients.find(x=>x.key==='n:иван петров'),anna:clients.find(x=>x.key==='p:79991234567'),alias:window.CateriumDataV1771===d};
});
expect(result.version).toBe('17.7.3');
expect(result.nameKey).toBe('n:иван петров');
expect(result.phoneKey).toBe('p:79991234567');
expect(result.ivan.orderCount).toBe(2);
expect(result.ivan.totalSpent).toBe(3000);
expect(result.ivan.loyalty.fixedDiscount).toBe(5);
expect(result.anna.communication.preferred).toBe('telegram');
expect(result.alias).toBeTruthy();
});
test('v17.7.3 prefers server client profile while preserving legacy order metrics', async ({ page }) => {
await page.addInitScript(()=>{
localStorage.setItem('sunOrders',JSON.stringify([{id:11,contact:' Иван Петров ',phone:'',address:'Local A',date:'2026-09-01',time:'12:00',total:1000,prepayment:400,status:'Новый',lines:[]},{id:12,contact:'иван петров',phone:'',address:'Local B',date:'2026-09-02',time:'13:00',total:2000,prepayment:2000,status:'Новый',lines:[]}]))
});
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{
window.SunCloudV2={getSession:()=>({user:{id:'u'}}),getWorkspace:()=>({id:'w'}),getSupportMode:()=>null,isSupportMode:()=>false,hasPermission:()=>true,getClientId:()=> 'e2e',getClient:()=>({rpc:async(name)=>{
if(name==='sun_v17_clients_snapshot_v1773')return {data:[{client_key:'n:иван петров',name:'Иван Петров · сервер',phone:'',latest_address:'Server address',data:{identity:{name:'Иван Петров · сервер',phone:'',latestAddress:'Server address'},loyalty:{enabled:true,fixedDiscount:7}},version:4,updated_at:'2026-09-09T08:00:00Z',source_keys:['n:иван петров','o:11'],source_count:2,canonical_row_present:true}],error:null};
if(name==='sun_v17_save_client_v1772')return {data:[],error:null};return {data:null,error:null};
}})};
});
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
const result=await page.evaluate(async()=>{const d=window.CateriumDataV1773;await d.clients.refreshServer({force:true,reason:'e2e'});const client=d.clients.get('n:иван петров'),compare=d.clients.compare();return {version:d.VERSION,name:client?.name,address:client?.latestAddress,orders:client?.orderCount,total:client?.totalSpent,discount:client?.loyalty?.fixedDiscount,serverCount:compare.serverCount,metrics:compare.orderMetricsEqual,alias:window.CateriumDataV1772===d};});
expect(result.version).toBe('17.7.3');
expect(result.name).toBe('Иван Петров · сервер');
expect(result.address).toBe('Server address');
expect(result.orders).toBe(2);
expect(result.total).toBe(3000);
expect(result.discount).toBe(7);
expect(result.serverCount).toBe(1);
expect(result.metrics).toBeTruthy();
expect(result.alias).toBeTruthy();
});