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.
85 lines
6.4 KiB
JavaScript
85 lines
6.4 KiB
JavaScript
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};
|
||
}
|