Fix Menu sidebar icon alignment and make all four client proposal templates structurally distinct in preview and PDF.
212 lines
16 KiB
JavaScript
212 lines
16 KiB
JavaScript
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.6');
|
|
});
|
|
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);
|
|
});
|