caterium-app/tests/trial-demo.spec.mjs

112 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {test,expect} from '@playwright/test';
import fs from 'node:fs';
import {demo} from '../ops/demo/trial-demo-data.mjs';
import {banquetDemo} from '../ops/demo/trial-banquet-data.mjs';
const completeDemo={...demo,boxes:[...demo.boxes,...banquetDemo.banquet],stock:[...demo.stock,...banquetDemo.stock]};
test('installed app serves old trial PNG links as cached lightweight images even offline',async({page,context})=>{
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.waitForFunction(()=>Boolean(navigator.serviceWorker.controller));
const load=()=>page.evaluate(async d=>Promise.all(d.boxes.map(async b=>{const r=await fetch('/'+b.photo);return {type:r.headers.get('content-type'),bytes:(await r.arrayBuffer()).byteLength}})),demo);
const online=await load();expect(online.every(r=>r.type.includes('webp'))).toBe(true);expect(online.reduce((s,r)=>s+r.bytes,0)).toBe(1343650);
await expect.poll(()=>page.evaluate(async d=>(await Promise.all(d.boxes.map(b=>caches.match('/'+b.photo.replace('.png','.webp'))))).filter(Boolean).length,demo)).toBe(10);
await context.setOffline(true);expect(await load()).toEqual(online);await context.setOffline(false);
});
async function ready(page){
page.on('dialog',d=>d.accept());
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.waitForFunction(()=>window.CateriumDataV1773&&window.CateriumTrialDemo&&window.SunAdminRBACV3);
await page.evaluate(d=>{
window.SunCloudV2={getSession:()=>({user:{id:'test'}}),getWorkspace:()=>({id:'test',role:'admin',permissions:{}}),getClient:()=>({rpc:async(name)=>({data:name==='sun_subscription_snapshot'?{access_mode:'full',status:'trialing',plan_name:'Полный',features:Object.fromEntries(['orders','shopping','stock','catalog_view','catalog_edit','suppliers','settings','clients','calendar','print'].map(k=>[k,true]))}:name==='caterium_trial_demo_status'?{eligible:true,installed:true,empty:false,canInstall:false}:null})}),status:()=>({connected:true,signedIn:true,membershipsLoaded:true}),hasPermission:()=>true};
// Hydrate AFTER stock UI boot: the real cloud path must update its closure.
for(const [key,value] of Object.entries({sunBoxes:d.boxes,sunStock:d.stock,sunSuppliers:d.suppliers,sunStockMoves:[],sunOrders:[],sunTrialDemoV1:{version:1,batch:d.batch,scenario:d.scenario}}))localStorage.setItem(key,JSON.stringify(value));
boxes=structuredClone(d.boxes);orders=[];window.dispatchEvent(new Event('sun:cloud-state-applied'));window.dispatchEvent(new Event('sun:cloud-permissions-changed'));window.render();window.CateriumTrialDemo.render();
},completeDemo);
await expect(page.locator('#ctTrialDemo')).toBeVisible();
}
test('demo TTK, order, shortages, receipt and one-time write-off work in the full app',async({page})=>{
await ready(page);
await page.locator('#ctTrialDemo summary').click();await page.locator('[data-demo-ttk]').click();
await expect(page.locator('#ctDemoTtk')).toBeVisible();await expect(page.locator('#ctDemoTtk')).toContainText('840 г');await expect(page.locator('#ctDemoTtk')).toContainText('Брутто');
expect(await page.locator('#ctDemoTtk').evaluate(el=>el.getBoundingClientRect().width<=innerWidth)).toBe(true);
await page.getByRole('button',{name:'Закрыть ТТК',exact:true}).click();
await page.locator('[data-demo-order]').click();await expect(page.locator('#ctDemoMessage')).toContainText('9 700 ₽');
await page.locator('[data-demo-order]').click();expect(await page.evaluate(()=>orders.length)).toBe(1);
expect(await page.evaluate(()=>orders[0].autoCompletionDisabled)).toBe(true);
await page.locator('[data-demo-go="shopping"]').click();
await expect(page.locator('[data-shop-key="багет|кг"]')).toContainText('0,96');
await expect(page.locator('[data-shop-key="багет|кг"]')).toContainText('230');
await page.getByRole('button',{name:'Склад',exact:true}).first().click();
await page.evaluate(()=>window.stockMovement('demo-v1-stock-baguette','in'));
await page.locator('#stockMoveQty').fill('2');await page.locator('#stockSaveMove').click();
expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-v1-stock-baguette').qty)).toBe(2);
expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-v1-stock-baguette').cost)).toBe(240);
await page.getByRole('button',{name:'Закупки',exact:true}).first().click();await expect(page.locator('[data-shop-key="багет|кг"]')).toHaveCount(0);
await page.evaluate(()=>window.writeOffOrderStock(orders[0].id));
const written=await page.evaluate(()=>({qty:JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-v1-stock-baguette').qty,moves:JSON.parse(localStorage.sunStockMoves).length,done:orders[0].stockWrittenOff}));
expect(written.qty).toBeCloseTo(1.04,3);expect(written.done).toBe(true);expect(written.moves).toBeGreaterThan(5);
await page.evaluate(()=>window.writeOffOrderStock(orders[0].id));expect(await page.evaluate(()=>JSON.parse(localStorage.sunStockMoves).length)).toBe(written.moves);
});
test('demo photo assets load and company changes remove the trial guide',async({page})=>{
await ready(page);
expect(await page.evaluate(async d=>{const images=await Promise.all(d.boxes.map(b=>new Promise(resolve=>{const i=new Image();i.onload=()=>resolve(i.naturalWidth===1024);i.onerror=()=>resolve(false);i.src=SunSafe.imageAssetSrc(b.photo)})));return images.every(Boolean)},demo)).toBe(true);
const sources=await page.locator('#tiles img[src*="demo/images/"]').evaluateAll(images=>images.map(i=>i.getAttribute('src')));
expect(sources).toHaveLength(10);expect(sources.every(s=>s.endsWith('.webp'))).toBe(true);
const bytes=demo.boxes.reduce((sum,b)=>sum+fs.statSync('public/'+b.photo.replace('.png','.webp')).size,0);expect(bytes).toBeLessThan(1500000);
expect(await page.evaluate(()=>['catalog/001.jpg','data:image/png;base64,AAAA','https://other.example/demo/images/caprese.png','demo/images/custom.png'].map(SunSafe.imageAssetSrc))).toEqual(['catalog/001.jpg','data:image/png;base64,AAAA','https://other.example/demo/images/caprese.png','demo/images/custom.png']);
await page.evaluate(()=>{window.dispatchEvent(new Event('sun:cloud-tenant-changing'));window.SunCloudV2.getSession=()=>null;localStorage.removeItem('sunTrialDemoV1');window.CateriumTrialDemo.render()});
await expect(page.locator('#ctTrialDemo')).toHaveCount(0);
});
test('trial banquet categories, per-guest TTK, shopping and stock work together',async({page})=>{
await ready(page);
await page.getByRole('button',{name:'Банкетное меню',exact:true}).click();
await expect(page.locator('[data-banquet-row]')).toHaveCount(18);
await page.locator('[data-banquet-group="sides"]').click();await expect(page.locator('[data-banquet-row]')).toHaveCount(2);
await page.locator('[data-banquet-ttk="demo-banquet-v1-mash"]').click();
await expect(page.locator('#ctDemoTtk')).toContainText('На 1 порцию / 1 гостя');
await expect(page.locator('#ctDemoTtk')).toContainText('150 г');await expect(page.locator('#ctDemoTtk')).toContainText('Цена порции: 160 ₽');
await page.getByRole('button',{name:'Закрыть ТТК',exact:true}).click();
await page.locator('[data-banquet-item="demo-banquet-v1-mash"]').check();
await page.locator('[data-banquet-group="desserts"]').click();await expect(page.locator('[data-banquet-row]')).toHaveCount(2);
await page.locator('[data-banquet-group="main"]').click();await page.locator('[data-banquet-item="demo-banquet-v1-chicken"]').check();
await page.locator('[data-banquet-guests]').fill('4');await expect(page.locator('[data-banquet-total]')).toHaveText('2 600 ₽');
await page.locator('[data-banquet-apply]').click();
await page.evaluate(()=>{const day=new Date();day.setDate(day.getDate()+1);draft.event='Пробный банкет';draft.date=`${day.getFullYear()}-${String(day.getMonth()+1).padStart(2,'0')}-${String(day.getDate()).padStart(2,'0')}`;draft.time='12:00';draft.autoCompletionDisabled=true;document.getElementById('date').value=draft.date;document.getElementById('time').value=draft.time;window.saveOrder()});
expect(await page.evaluate(()=>orders.at(-1).lines.map(l=>({id:l.id,qty:l.qty})))).toEqual([{id:'demo-banquet-v1-chicken',qty:4},{id:'demo-banquet-v1-mash',qty:4}]);
await page.getByRole('button',{name:'Закупки',exact:true}).first().click();
await expect(page.locator('[data-shop-key="картофель|кг"]')).toContainText('0,68');
await expect(page.locator('[data-shop-key="филе куриное сырое|кг"]')).toContainText('0,72');
await page.getByRole('button',{name:'Склад',exact:true}).first().click();
await page.evaluate(()=>window.stockMovement('demo-banquet-v1-stock-potato','in'));
await page.locator('#stockMoveQty').fill('1');await page.locator('#stockSaveMove').click();
await page.evaluate(()=>window.writeOffOrderStock(orders.at(-1).id));
expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-banquet-v1-stock-potato').qty)).toBeCloseTo(.32,3);
});
test('owner onboarding preserves server demo when saving the company profile',async({page})=>{
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));await page.goto('/index.html');
await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'});
const runtime=fs.readFileSync('public/app-runtime.js','utf8');let source=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js'));
source=source.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testInit:c=>{client=c;session={user:{id:'test-user'}};config.tenantStorageReady=true;config.localWorkspaceId='';config.workspaceId=''},`);
await page.addScriptTag({content:'var boxes=[],orders=[];async function compactSunBoxesForStorage(x){return x};'+source});
const result=await page.evaluate(async d=>{
let created=false,saved=null;const payload={format:'sun-cloud-v2',version:2,storage:{sunBoxes:{t:'j',v:d.boxes},sunStock:{t:'j',v:d.stock},sunSuppliers:{t:'j',v:d.suppliers},sunTrialDemoV1:{t:'j',v:{version:1,scenario:d.scenario}},sunCompanyProfileV1:{t:'j',v:{email:'owner@example.invalid',name:''}}}};
const row={payload,revision:1,updated_at:new Date().toISOString()};
window.SunCloudV2.testInit({rpc:async(name,args)=>{
if(name==='sun_my_workspaces')return {data:created?[{id:'company',name:'Test',role:'admin',is_active:true}]:[]};
if(name==='sun_create_workspace'){created=true;return {data:'company'}}
if(name==='sun_fetch_app_state')return {data:[row]};
if(name==='sun_save_app_state_v17'){saved=structuredClone(args.p_payload);return {data:[{...row,payload:saved,revision:2}]}}
return {data:null};
},channel:()=>({on(){return this},subscribe(){return this}})});
await window.SunCloudV2.createTrialWorkspace('My trial company');
return {saved,local:JSON.parse(localStorage.sunBoxes),workspace:window.SunCloudV2.getWorkspace()};
},demo);
expect(result.saved.storage.sunBoxes.v).toHaveLength(10);expect(result.local).toHaveLength(10);
expect(result.saved.storage.sunStock.v).toEqual(demo.stock);expect(result.saved.storage.sunCompanyProfileV1.v).toMatchObject({name:'My trial company',email:'owner@example.invalid'});
});