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}; }