caterium-app/tests/app.spec.mjs
pavlov346346-source 4ba730990b
Caterium v17.6.8 — Developer Console UX and error fixes
* Add v17.6.8 Developer Console UX and race fixes

* Add Developer Console numbering delete and grouped error RPCs

* Add one-time v17.6.8 release preparation workflow

* Prepare Caterium v17.6.8 Developer Console UX

* Remove one-time v17.6.8 release workflow

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-08 07:58:54 +03:00

243 lines
18 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 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.6.8');
});
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.6.5'");
});
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');
});