import { test, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
const injectCore=async(page,file,marker='')=>{
const source=fs.readFileSync(path.join(process.cwd(),'public','core',file),'utf8');
await page.addScriptTag({content:source});
if(marker)await page.waitForFunction(m=>Boolean(window[m]),marker,{timeout:5000});
};
test('app boots without page errors', async ({ page }) => {
const errors=[];page.on('pageerror',e=>errors.push(String(e)));
await page.goto('/index.html',{waitUntil:'domcontentloaded'});await page.waitForTimeout(1200);
expect(errors).toEqual([]);
});
test('official catalog and photos remain intact', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const result=await page.evaluate(()=>({catalog:Array.isArray(window.SUN_OFFICIAL_CATALOG)?window.SUN_OFFICIAL_CATALOG.length:0,version:window.SUN_OFFICIAL_CATALOG_VERSION||'',photos:[...document.querySelectorAll('img[src*="catalog/"]')].length}));
expect(result.catalog).toBeGreaterThanOrEqual(100);expect(result.version).toBeTruthy();
});
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('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.1');
});
test('v17.7.1 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.1'");
});
test('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('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('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('proposal templates preserve selection and expose 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('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('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('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 page.waitForFunction(()=>Boolean(window.CateriumDataV1771),null,{timeout:5000});
const result=await page.evaluate(()=>{const d=window.CateriumDataV1771;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.1',alias:true});
});
test('server automation applies returned normalized orders through current data layer alias', 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 page.waitForFunction(()=>Boolean(window.CateriumDataV1771),null,{timeout:5000});
const result=await page.evaluate(()=>{const d=window.CateriumDataV1771,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.1',alias:true,hasOrder:true,hasCatalog:true});
});