caterium-app/tests/training-catalog.spec.mjs
pavlov346346-source 32d6eb86c8 feat: replace the large training banner with a one-line notice
The big "Учебный каталог включён" card with its buttons sat on top of the
order screen. While training mode is on, the order screen now shows only
"Включён учебный режим · отключить в настройках"; the link opens Settings
at the training switch. The guide (trial order, stock, purchasing, TTK)
moves into the "Обучение и знакомство" settings card, so nothing is lost.
The invitation card for companies with an empty catalog is unchanged.

Specs updated for the new placement, plus a check that the notice link
lands on (and focuses) the switch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 17:36:00 +03:00

101 lines
11 KiB
JavaScript
Raw Permalink 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 {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));}});