Add Settings > Учебный каталог, off by default and available on ordinary writable profiles. Load ready example boxes, photos, TTKs and linked sample inventory additively; preserve own data, saved orders and edited examples when hiding or re-enabling. Respect company/profile scope, read-only permissions, failed downloads and tenant changes. Preserve the opened recipe guide across real catalog refreshes on iPhone. Integrated feature checks and full pull-request QA passed. Standard main QA and exact-asset production UI verification remain in place.
97 lines
11 KiB
JavaScript
97 lines
11 KiB
JavaScript
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 nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();await expect(page.locator('#ctTrialDemo')).toBeVisible();
|
||
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));}});
|