caterium-app/tests/mobile-menu-scenario.mjs
pavlov346346-source 5ce7d8d353
Mobile menu editor above boxes with a matching return arrow (#34)
On phones place the existing menu editor above the catalog and add the same return-to-top control used by New Order. Preserve form nodes, unsaved values and active input focus; keep desktop/tablet layout and read-only permissions. Full QA passed, including iPhone WebKit and existing promotion focus regression. Extend real-asset production verification with a backend-blocked mobile-menu scenario.
2026-09-19 20:02:43 +03:00

85 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 assert from 'node:assert/strict';
import {expect} from '@playwright/test';
// The real application assets run with a synthetic local workspace. No backend
// traffic or real customer credentials are allowed, including on production.
export async function bootMenuFixture(page,baseUrl='http://127.0.0.1:4173/'){
const base=new URL(baseUrl);
await page.route('**/*',route=>{
const req=route.request(),url=new URL(req.url());
if(req.method()==='GET'&&url.origin===base.origin&&!url.pathname.startsWith('/api/'))return route.continue();
return route.abort();
});
await page.addInitScript(()=>{
const id='mobile-menu-workspace',user={id:'mobile-menu-user',email:'menu-test@example.invalid',email_confirmed_at:'2026-01-01T00:00:00Z'};
localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId:id,localWorkspaceId:id,tenantStorageReady:true,autoSync:false}));
localStorage.setItem('sunBoxes',JSON.stringify(Array.from({length:24},(_,i)=>({id:`mobile-box-${i+1}`,name:`Бокс ${String(i+1).padStart(2,'0')}`,category:0,price:1000+i*10,weight:'600 г',pieces:12,ingredients:[['Томаты',1,'кг']]}))));
localStorage.setItem('sunOrders',JSON.stringify([{id:1,event:'Тестовый заказ',date:'2099-12-20',time:'12:00',status:'Новый',lines:[{id:'mobile-box-1',qty:2,price:1000}],total:2000}]));
const features=Object.fromEntries('orders calendar clients catalog_view catalog_edit production shopping stock routes mailings money stats_basic stats_advanced team suppliers print settings branding client_offers offer_templates backups audit users_manage'.split(' ').map(k=>[k,true]));
window.supabase={createClient:()=>({
auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:async()=>({data:{session:{user}},error:null}),getUser:async()=>({data:{user},error:null})},
rpc:async name=>({data:name==='sun_my_workspaces'?[{id,name:'Тестовая компания',role:'admin',is_active:true,permissions:{}}]:name==='sun_subscription_snapshot'?{plan_id:'full',plan_name:'Полный',status:'active',access_mode:'full',features}:name==='sun_is_platform_admin'?false:name==='caterium_trial_demo_status'?{canInstall:false,canUpgrade:false}:null,error:null}),
channel:()=>({on(){return this},subscribe(){return this}}),removeChannel(){}
})};
});
await page.goto(base.href,{waitUntil:'domcontentloaded',timeout:60000});
await expect(page.locator('body > header')).toBeVisible({timeout:20000});
await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773&&window.__cateriumOrderEnhancementsV1775);
await page.locator('header nav').getByRole('button',{name:'Меню',exact:true}).click();
await expect(page.locator('#sun-menu-editor-v1762')).toHaveAttribute('data-ct-mobile-menu','1');
await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(24);
}
export async function menuGeometry(page){
let geometry;
// Read rectangles and media queries atomically. WebKit/mobile Chromium can
// update the layout viewport a frame after a resize or keyboard transition.
await expect.poll(async()=>{
geometry=await page.evaluate(()=>{
const d=document.getElementById('sunMenuDetailV1762'),l=document.querySelector('.sun-menu-list-pane');
return {detail:d.getBoundingClientRect().toJSON(),list:l.getBoundingClientRect().toJSON(),mobile:matchMedia('(max-width:760px)').matches,singleColumn:matchMedia('(max-width:950px)').matches,formFirst:d.nextElementSibling===l,listFirst:l.nextElementSibling===d,viewport:innerWidth,scrollWidth:document.documentElement.scrollWidth};
});
const {detail:d,list:l,mobile,singleColumn,formFirst,listFirst}=geometry;
return mobile?(formFirst&&d.bottom<=l.y+1):(listFirst&&(singleColumn?l.bottom<=d.y+1:l.right<=d.x+1));
},{message:'Menu layout and reading order must match the current breakpoint'}).toBe(true);
if(geometry.mobile)assert(geometry.scrollWidth<=geometry.viewport+1,'No horizontal overflow on phones');
return geometry;
}
export async function exerciseMenuNavigation(page,{screenshot=async()=>{}}={}){
const geometry=await menuGeometry(page),top=page.locator('#ctMenuTop');
await expect(top).toBeHidden();
const before=await page.evaluate(()=>({orders:localStorage.sunOrders,boxes:localStorage.sunBoxes}));
await page.locator('[data-menu-item-v1762="mobile-box-24"]').click();
await expect(page.locator('#sunMenuDetailV1762 > .dialog #boxName')).toHaveValue('Бокс 24');
if(geometry.mobile)await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0);
await menuGeometry(page);
await screenshot('menu-form-top');
await page.locator('#boxName').fill('Несохранённый бокс');
await page.locator('#boxPrice').fill('1350');
await page.evaluate(()=>{window.mobileEditorNode=document.getElementById('boxName');window.mobileEditorInputEvents=0;mobileEditorNode.addEventListener('input',()=>window.mobileEditorInputEvents++);});
await page.locator('[data-menu-item-v1762="mobile-box-24"]').scrollIntoViewIfNeeded();
if(geometry.mobile){
await expect(top).toBeVisible();
await expect(page.locator('#ctOrderTop')).toBeHidden();
const bounds=await top.boundingBox();assert(bounds.width>=48&&bounds.height>=48);
await screenshot('menu-arrow');
await top.click();
await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0);
await expect(page.locator('#sunMenuDetailV1762')).toBeFocused();
await expect(top).toBeHidden();
}else await expect(top).toBeHidden();
await expect(page.locator('#boxName')).toHaveValue('Несохранённый бокс');
await expect(page.locator('#boxPrice')).toHaveValue('1350');
assert(await page.evaluate(()=>window.mobileEditorNode===document.getElementById('boxName')),'The form must not be recreated');
assert.deepEqual(await page.evaluate(()=>({orders:localStorage.sunOrders,boxes:localStorage.sunBoxes})),before,'Navigation must not save or change data');
await page.locator('#saveBoxButton').click();
await expect.poll(()=>page.evaluate(()=>JSON.parse(localStorage.sunBoxes).find(x=>x.id==='mobile-box-24')?.name)).toBe('Несохранённый бокс');
assert.equal(await page.evaluate(()=>JSON.parse(localStorage.sunBoxes).find(x=>x.id==='mobile-box-24').price),1350);
assert.equal(await page.evaluate(()=>localStorage.sunOrders),before.orders);
await page.locator('header nav').getByRole('button',{name:'Заказы',exact:true}).click();
await expect(top).toBeHidden();
await expect(page.locator('#editor > .dialog')).toHaveCount(1);
return {...geometry,navigationPreservesDraft:true,saveWorks:true,ordersUnchanged:true};
}