import {test,expect} from '@playwright/test'; import {trainingInit} from './training-catalog-fixture.mjs'; import {demo} from '../ops/demo/trial-demo-data.mjs'; test.use({serviceWorkers:'block'}); const nav=page=>page.locator('header nav'); const setting=page=>page.locator('#ctTrainingEnabled'); async function settings(page){await nav(page).getByRole('button',{name:'Настройки',exact:true}).click();await expect(setting(page)).toBeVisible();} async function boot(page){ page.on('dialog',d=>d.accept()); await page.route('**/*',route=>{const r=route.request(),u=new URL(r.url());return r.method()==='GET'&&u.origin==='http://127.0.0.1:4173'&&!u.pathname.startsWith('/api/')?route.continue():route.abort();}); await page.addInitScript(trainingInit); await page.goto('/index.html',{waitUntil:'domcontentloaded'}); await page.waitForFunction(()=>window.CateriumDataV1773&&window.SunOpsUXV1762&&window.CateriumTrialDemo&&window.__cateriumOrderEnhancementsV1775); await expect(page.locator('body > header')).toBeVisible();await settings(page); } async function toggle(page,on){await setting(page).setChecked(on);await expect(setting(page)).toBeEnabled();await expect.poll(()=>page.evaluate(()=>CateriumTrainingCatalog.enabled())).toBe(on);} const state=page=>page.evaluate(()=>Object.fromEntries(['sunBoxes','sunStock','sunSuppliers','sunOrders','sunStockMoves','sunTrialDemoV1'].map(k=>[k,localStorage.getItem(k)]))); const own=page=>page.evaluate(()=>({box:JSON.parse(localStorage.sunBoxes).find(x=>x.id==='own-box'),stock:JSON.parse(localStorage.sunStock).find(x=>x.id==='own-stock'),supplier:JSON.parse(localStorage.sunSuppliers).find(x=>x.id==='own-supplier'),orders:localStorage.sunOrders,moves:localStorage.getItem('sunStockMoves')})); test('training is opt-in in an existing paid profile, adds complete examples, hides them and preserves originals',async({page},info)=>{ test.setTimeout(60000); // Full multi-page round trip plus reload in iPhone WebKit. await boot(page);await expect(setting(page)).not.toBeChecked(); const before=await own(page);expect(await page.evaluate(()=>boxes.length)).toBe(1); await toggle(page,true);expect(await page.evaluate(()=>boxes.length)).toBe(45); expect(await own(page)).toEqual(before); const source=await page.evaluate(()=>boxes.find(x=>x.id==='demo-v1-bruschetta-tomato')); expect(source.photo).toBeTruthy();expect(source.composition.length).toBeGreaterThan(0);expect(source.ttk.rows.length).toBeGreaterThan(0);expect(source.ingredients.some(i=>i[0]==='Учебное: Томаты')).toBe(true); await page.locator('#ctTrainingSettings').scrollIntoViewIfNeeded(); await page.screenshot({path:info.outputPath('training-settings.png'),animations:'disabled'}); await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(11); await expect(page.locator('[data-menu-item-v1762="demo-v1-bruschetta-tomato"] .ct-training-badge')).toHaveText('Учебное'); await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click(); await page.getByRole('button',{name:'Банкетное меню',exact:true}).click();await expect(page.locator('[data-banquet-row]')).toHaveCount(18); await settings(page);await toggle(page,false); await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await page.locator('[data-menu-cat-v1762="0"]').click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1); await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();await page.getByRole('button',{name:'Банкетное меню',exact:true}).click();await expect(page.locator('[data-banquet-row]')).toHaveCount(0);await page.getByRole('button',{name:'Боксы',exact:true}).click();await expect(page.locator('#tiles .tile:not(.add)')).toHaveCount(1);await expect(page.locator('#ctTrialDemo')).toHaveCount(0); await nav(page).getByRole('button',{name:'Склад',exact:true}).click();await expect(page.locator('#stockContent')).not.toContainText('Учебное:'); await nav(page).getByRole('button',{name:'Поставщики',exact:true}).click();await expect(page.locator('#supList')).not.toContainText('Демо:'); expect(await own(page)).toEqual(before);expect(await page.evaluate(()=>boxes.length)).toBe(45); await page.reload();await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773);await settings(page);await expect(setting(page)).not.toBeChecked();await toggle(page,true); expect(await page.evaluate(()=>boxes.length)).toBe(45);expect(await own(page)).toEqual(before); }); test('TTKs, trial order and preparation keep their complete records after hiding and restore without resetting edits',async({page})=>{ await boot(page);await toggle(page,true); await expect(page.locator('#ctTrialDemo')).toBeVisible(); // The order screen only shows a one-line notice; its link leads back to the switch and the guide. await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click(); await expect(page.locator('#ctTrainingNotice')).toContainText('Включён учебный режим');await expect(page.locator('#ctTrialDemo')).toBeHidden(); await page.locator('#ctTrainingNotice button').click();await expect(page.locator('#ctTrialDemo')).toBeVisible();await expect(setting(page)).toBeFocused(); await page.evaluate(()=>{window.ctGuideEvents=[];const card=document.getElementById('ctTrialDemo');new MutationObserver(rs=>{ctGuideEvents.push(...rs.map(r=>({type:r.type,attribute:r.attributeName,open:card.querySelector('details')?.open,signature:card.dataset.signature})));}).observe(card,{childList:true,subtree:true,attributes:true,attributeFilter:['open','data-signature']});}); await page.locator('#ctTrialDemo summary').click();await page.locator('[data-demo-ttk]').click();await expect(page.locator('#ctDemoTtk')).toContainText('Брутто');await expect(page.locator('#ctDemoTtk')).toContainText('840 г');await page.getByRole('button',{name:'Закрыть ТТК',exact:true}).click(); await page.locator('[data-demo-order]').click();await expect(page.locator('#ctDemoMessage')).toContainText('9 700 ₽'); const order=await page.evaluate(()=>orders.find(o=>o.demo===true));expect(order.autoCompletionDisabled).toBe(true); await page.evaluate(()=>{const items=CateriumDataV1773.catalog.list();const box=items.find(b=>b.id==='demo-v1-bruschetta-tomato');box.price=2222;box.name='Мой учебный рецепт';CateriumDataV1773.catalog.replace(items,{persistLocal:false});const stock=JSON.parse(localStorage.sunStock);stock.find(s=>s.id==='demo-v1-stock-baguette').qty=7;localStorage.setItem('sunStock',JSON.stringify(stock));}); const before=await state(page);await settings(page);await toggle(page,false); expect(await page.evaluate(id=>orders.find(o=>o.id===id),order.id)).toEqual(order); expect(await page.evaluate(()=>CateriumDataV1773.catalog.get('demo-v1-bruschetta-tomato').ttk.rows.length)).toBeGreaterThan(0); await toggle(page,true); const after=await state(page);for(const k of ['sunBoxes','sunOrders','sunStock','sunSuppliers','sunStockMoves'])expect(after[k]).toBe(before[k]); }); test('old automatically seeded boxes are hidden until opt-in and their customised prices are not overwritten',async({page})=>{ await boot(page); await page.evaluate(d=>{d.boxes[0].price=4321;CateriumDataV1773.catalog.replace([...CateriumDataV1773.catalog.list(),...d.boxes],{persistLocal:false});localStorage.setItem('sunTrialDemoV1',JSON.stringify({version:1,batch:d.batch,scenario:d.scenario}));CateriumTrainingCatalog.refresh();},demo); await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1); await settings(page);await toggle(page,true);expect(await page.evaluate(()=>boxes.find(b=>b.id==='demo-v1-bruschetta-tomato').price)).toBe(4321); }); test('the checkbox is profile- and company-specific; read-only and support access do not gain write rights',async({page})=>{ await boot(page);await toggle(page,true);const before=await state(page); await page.evaluate(()=>{window.savedSession=SunCloudV2.getSession;SunCloudV2.getSession=()=>({user:{id:'another-user'}});CateriumTrainingCatalog.refresh();});await expect(setting(page)).not.toBeChecked(); await page.evaluate(()=>{SunCloudV2.getSession=window.savedSession;window.savedWorkspace=SunCloudV2.getWorkspace;SunCloudV2.getWorkspace=()=>({id:'another-workspace'});CateriumTrainingCatalog.refresh();});await expect(setting(page)).not.toBeChecked(); // Keep read access, deny writes. Revoking read access too correctly triggers // the existing RBAC cache redaction and is not a training-catalog mutation. await page.evaluate(()=>{SunCloudV2.getWorkspace=window.savedWorkspace;window.savedPermission=SunCloudV2.hasPermission;SunCloudV2.hasPermission=p=>p==='app.read'||p.endsWith('.view');CateriumTrainingCatalog.refresh();});await expect(setting(page)).toBeDisabled(); expect(await page.evaluate(()=>CateriumTrainingCatalog.setEnabled(false).then(()=>false,()=>true))).toBe(true); await page.evaluate(()=>{SunCloudV2.hasPermission=window.savedPermission;SunCloudV2.isSupportMode=()=>true;CateriumTrainingCatalog.refresh();});await expect(setting(page)).toBeDisabled(); expect(await state(page)).toEqual(before); }); test('failed example download leaves the catalog, inventory and preference unchanged and can be retried',async({page})=>{ await boot(page);const before=await state(page); await page.route('**/demo/catalog-v1.json?**',route=>route.fulfill({status:503,body:'temporarily unavailable'})); // A fast rejected download correctly reverts the checkbox before check() // could assert a checked state. Click and assert the intended error state. await setting(page).click();await expect(page.locator('#ctTrainingStatus')).toContainText('Не удалось загрузить');await expect(setting(page)).not.toBeChecked();await expect(setting(page)).toBeEnabled();expect(await state(page)).toEqual(before); await page.unroute('**/demo/catalog-v1.json?**');await toggle(page,true);expect(await page.evaluate(()=>boxes.length)).toBe(45); }); test('a delayed seed cannot be applied to another company after switching',async({page})=>{ await boot(page);const before=await state(page);let release,requested=false;const wait=new Promise(resolve=>{release=resolve;}); await page.route('**/demo/catalog-v1.json?**',async route=>{requested=true;await wait;await route.continue();}); await setting(page).check();await expect.poll(()=>requested).toBe(true); await page.evaluate(()=>{window.dispatchEvent(new Event('sun:cloud-tenant-changing'));SunCloudV2.getWorkspace=()=>({id:'new-company'});window.dispatchEvent(new Event('sun:cloud-state-applied'));});release(); await expect(setting(page)).not.toBeChecked();await page.waitForTimeout(200);expect(await state(page)).toEqual(before); }); test.afterEach(async({page},info)=>{if(info.status!==info.expectedStatus){const events=await page.evaluate(()=>window.ctGuideEvents||[]).catch(()=>[]);await info.attach('training-guide-events',{body:JSON.stringify(events, null, 2),contentType:'application/json'});console.log('GUIDE EVENTS',JSON.stringify(events));}});