import fs from 'node:fs'; import path from 'node:path'; import { test, expect } from '@playwright/test'; // Module tests replace the cloud/data globals and load the module under test // themselves. Give those tests an explicit same-origin fixture so real auth // timers cannot overwrite the mocks. Integration tests still load the full app. const fullAppTests=new Set([ 'legacy localStorage payload cannot execute XSS', 'v17.6.5 stays free of timer page errors during idle' ]); test.beforeEach(async({page},testInfo)=>{ if(!fullAppTests.has(testInfo.title))await page.route('**/index.html',route=>route.fulfill({contentType:'text/html',body:''})); }); 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:'',price:100,photo:'javascript:alert(1)',ingredients:[['',1,'шт.']]}])); localStorage.setItem('sunOrders', JSON.stringify([{id:1,event:'',date:'2026-09-07',time:'12:00',address:'',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 }) => { // This is a module fixture: real auth timers must not overwrite its mocked session. await page.setContent(''); await page.evaluate(()=>{ document.body.innerHTML='

Загружаю рабочую базу

'; 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='
'; 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='
'; 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=''; 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(); });