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.
This commit is contained in:
parent
ee70c66ed8
commit
5ce7d8d353
4
.github/workflows/deploy-timeweb.yml
vendored
4
.github/workflows/deploy-timeweb.yml
vendored
@ -50,6 +50,7 @@ jobs:
|
||||
core/catalog-pricing.js
|
||||
core/client-menu.css
|
||||
core/ops-ux-v1762.js
|
||||
core/mobile-order.js
|
||||
core/banquet-menu.js
|
||||
core/data-layer-v1773.js
|
||||
core/trial-demo.js
|
||||
@ -112,6 +113,9 @@ jobs:
|
||||
- name: Check the published loading screen, Help, clients and promotions
|
||||
timeout-minutes: 4
|
||||
run: node tests/production-ui-smoke.mjs
|
||||
- name: Check the published mobile menu form and return arrow
|
||||
timeout-minutes: 4
|
||||
run: node tests/production-mobile-menu.mjs
|
||||
- name: Save production UI verification
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@ -35,5 +35,79 @@
|
||||
const observer=new MutationObserver(update);observer.observe(view,{attributes:true,attributeFilter:['class']});observer.observe(document.body,{attributes:true,attributeFilter:['class']});
|
||||
layout();
|
||||
}
|
||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||||
// The menu page is created later by ops-ux. Attach once when it exists, then
|
||||
// observe only its visibility and direct detail children, never the whole app.
|
||||
function bootMenu(){
|
||||
const view=document.getElementById('sun-menu-editor-v1762'),layout=view?.querySelector('.sun-menu-layout');
|
||||
const list=layout?.querySelector(':scope > .sun-menu-list-pane'),detail=document.getElementById('sunMenuDetailV1762');
|
||||
if(!view||!layout||!list||!detail)return false;
|
||||
if(view.dataset.ctMobileMenu==='1')return true;
|
||||
view.dataset.ctMobileMenu='1';
|
||||
const mobile=matchMedia('(max-width:760px)');
|
||||
const button=document.createElement('button');button.id='ctMenuTop';button.type='button';button.hidden=true;
|
||||
button.setAttribute('aria-label','Наверх к редактору меню');button.title='Наверх к редактору меню';
|
||||
button.setAttribute('aria-controls','sunMenuDetailV1762');
|
||||
button.innerHTML='<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M12 19V5m-6 6 6-6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Наверх</span>';
|
||||
document.body.appendChild(button);
|
||||
const style=document.createElement('style');style.id='ctMobileMenuStyle';style.textContent=`
|
||||
#ctMenuTop{display:none}
|
||||
@media(max-width:760px){
|
||||
#sun-menu-editor-v1762 .sun-menu-layout{grid-template-columns:minmax(0,1fr)}
|
||||
#sun-menu-editor-v1762 #sunMenuDetailV1762{grid-column:1;grid-row:1;min-width:0;min-height:0}
|
||||
#sun-menu-editor-v1762 .sun-menu-list-pane{grid-column:1;grid-row:2;min-width:0;min-height:0}
|
||||
#sun-menu-editor-v1762 .sun-menu-list{max-height:none;overflow:visible}
|
||||
#sun-menu-editor-v1762 .sun-menu-detail-placeholder{min-height:76px}
|
||||
#ctMenuTop:not([hidden]){display:flex;align-items:center;justify-content:center;gap:6px;position:fixed;right:16px;bottom:calc(88px + env(safe-area-inset-bottom,0px));z-index:900;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid #d7bb65;border-radius:24px;background:#f6e7af;color:#302919;box-shadow:0 4px 18px #0002;font:700 13px/1.2 Arial,sans-serif;cursor:pointer}
|
||||
#ctMenuTop:focus-visible{outline:3px solid #546747;outline-offset:3px}
|
||||
}
|
||||
@media print{#ctMenuTop{display:none!important}}
|
||||
`;document.head.appendChild(style);
|
||||
function active(){return mobile.matches&&view.classList.contains('on')&&!document.body.classList.contains('sun-cloud-auth-required');}
|
||||
function visibility(){button.hidden=!active()||document.body.classList.contains('sun-modal-open')||window.scrollY<300;}
|
||||
function neutralHint(){
|
||||
const label=detail.querySelector('.sun-menu-detail-placeholder b');
|
||||
if(label?.textContent==='Выберите позицию слева, чтобы открыть карточку.')label.textContent='Выберите позицию из списка, чтобы открыть карточку.';
|
||||
}
|
||||
function arrange(){
|
||||
// Move the LIST, not the form: an unsaved or focused editor keeps its node,
|
||||
// input values, file selection and listeners across viewport changes.
|
||||
if(mobile.matches){if(detail.nextElementSibling!==list)detail.after(list);}
|
||||
else if(list.nextElementSibling!==detail)layout.insertBefore(list,detail);
|
||||
neutralHint();visibility();
|
||||
}
|
||||
function returnToEditor(){
|
||||
if(!active())return;
|
||||
window.scrollTo({top:0,left:0,behavior:'instant'});
|
||||
detail.setAttribute('tabindex','-1');detail.focus({preventScroll:true});visibility();
|
||||
}
|
||||
let frame=0;
|
||||
function update(){if(frame)return;frame=requestAnimationFrame(()=>{frame=0;neutralHint();visibility();});}
|
||||
button.addEventListener('click',returnToEditor);
|
||||
view.addEventListener('click',e=>{
|
||||
const target=e.target.closest?.('[data-menu-item-v1762],#sunMenuAddV1762');
|
||||
if(!target||target.disabled||!active())return;
|
||||
// The existing click handler docks the editor in a zero-delay task. Wait
|
||||
// for it, and do not scroll if the user has left Menu in the meantime.
|
||||
setTimeout(()=>requestAnimationFrame(()=>{
|
||||
// Never take focus or scroll away after the user has started editing.
|
||||
const focused=document.activeElement;
|
||||
if(detail.contains(focused)&&focused?.matches('input,textarea,select,[contenteditable="true"]'))return;
|
||||
if(detail.querySelector(':scope > .dialog,:scope > .sun-menu-readonly'))returnToEditor();
|
||||
}),0);
|
||||
});
|
||||
window.addEventListener('scroll',update,{passive:true});mobile.addEventListener('change',arrange);
|
||||
const observer=new MutationObserver(update);
|
||||
observer.observe(view,{attributes:true,attributeFilter:['class']});
|
||||
observer.observe(document.body,{attributes:true,attributeFilter:['class']});
|
||||
observer.observe(detail,{childList:true});
|
||||
arrange();return true;
|
||||
}
|
||||
function start(){
|
||||
boot();
|
||||
if(!bootMenu()){
|
||||
const waiting=new MutationObserver(()=>{if(bootMenu())waiting.disconnect();});
|
||||
waiting.observe(document.body,{childList:true});
|
||||
}
|
||||
}
|
||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
|
||||
})();
|
||||
|
||||
84
tests/mobile-menu-scenario.mjs
Normal file
84
tests/mobile-menu-scenario.mjs
Normal file
@ -0,0 +1,84 @@
|
||||
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};
|
||||
}
|
||||
81
tests/mobile-menu.spec.mjs
Normal file
81
tests/mobile-menu.spec.mjs
Normal file
@ -0,0 +1,81 @@
|
||||
import {test,expect} from '@playwright/test';
|
||||
import {bootMenuFixture,menuGeometry,exerciseMenuNavigation} from './mobile-menu-scenario.mjs';
|
||||
|
||||
test.use({serviceWorkers:'block'});
|
||||
|
||||
test('mobile menu puts editing above boxes and returns without losing unsaved values',async({page})=>{
|
||||
await bootMenuFixture(page);await exerciseMenuNavigation(page);
|
||||
});
|
||||
|
||||
test('mobile menu viewport changes move the list, not the focused editor',async({page})=>{
|
||||
await bootMenuFixture(page);
|
||||
await page.locator('[data-menu-item-v1762="mobile-box-1"]').click();
|
||||
await expect(page.locator('#sunMenuDetailV1762 > .dialog')).toBeVisible();
|
||||
// The automatic return is phone-only; desktop keeps its existing scroll.
|
||||
if(await page.evaluate(()=>matchMedia('(max-width:760px)').matches))await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0);
|
||||
await page.waitForTimeout(80);
|
||||
await page.locator('#boxName').fill('Сохрани ввод при повороте');
|
||||
await page.locator('#boxName').focus();
|
||||
await page.evaluate(()=>{window.formInput=document.getElementById('boxName');window.inputsSeen=0;formInput.addEventListener('input',()=>window.inputsSeen++);});
|
||||
for(const width of [1440,390,760,1440,390]){
|
||||
await page.setViewportSize({width,height:900});
|
||||
await expect.poll(()=>page.evaluate(()=>{const d=document.getElementById('sunMenuDetailV1762'),list=document.querySelector('.sun-menu-list-pane');return matchMedia('(max-width:760px)').matches?d.nextElementSibling===list:list.nextElementSibling===d;})).toBe(true);
|
||||
await menuGeometry(page);
|
||||
await expect(page.locator('#boxName')).toHaveValue('Сохрани ввод при повороте');
|
||||
await expect(page.locator('#boxName')).toBeFocused();
|
||||
expect(await page.evaluate(()=>formInput===document.getElementById('boxName'))).toBe(true);
|
||||
}
|
||||
await page.locator('#boxName').fill('Обработчик остался');
|
||||
expect(await page.evaluate(()=>inputsSeen)).toBeGreaterThan(0);
|
||||
await expect(page.locator('#ctMenuTop')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('mobile menu search, category and new item keep working; leaving prevents a delayed scroll',async({page})=>{
|
||||
await bootMenuFixture(page);
|
||||
await page.locator('#sunMenuSearchV1762').fill('Бокс 07');
|
||||
await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1);
|
||||
await page.locator('[data-menu-cat-v1762="5"]').click();
|
||||
await page.locator('#sunMenuAddV1762').click();
|
||||
await expect(page.locator('#sunMenuDetailV1762 > .dialog')).toBeVisible();
|
||||
await expect(page.locator('#itemCategory')).toHaveValue('5');
|
||||
if(await page.evaluate(()=>matchMedia('(max-width:760px)').matches))await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0);
|
||||
await menuGeometry(page);
|
||||
await page.locator('#boxName').fill('Премиум с телефона');await page.locator('#boxPrice').fill('2200');await page.locator('#saveBoxButton').click();
|
||||
await expect.poll(()=>page.evaluate(()=>JSON.parse(localStorage.sunBoxes).find(x=>x.name==='Премиум с телефона')?.category)).toBe(5);
|
||||
await page.evaluate(()=>{document.getElementById('sunMenuAddV1762').click();[...document.querySelectorAll('header nav button')].find(b=>b.textContent.trim()==='Заказы').click();});
|
||||
await page.waitForTimeout(100);
|
||||
await expect(page.locator('#editor > .dialog')).toHaveCount(1);
|
||||
await expect(page.locator('#ctMenuTop')).toBeHidden();
|
||||
await page.locator('header nav').getByRole('button',{name:'Меню',exact:true}).click();
|
||||
await expect(page.locator('#ctMenuTop')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('read-only mobile menu opens the card above the list without enabling editing',async({page})=>{
|
||||
await bootMenuFixture(page);
|
||||
const before=await page.evaluate(()=>localStorage.sunBoxes);
|
||||
await page.evaluate(()=>{SunCloudV2.hasPermission=p=>p==='catalog.view'||p==='app.read';window.dispatchEvent(new Event('sun:cloud-permissions-changed'));});
|
||||
await expect(page.locator('#sunMenuAddV1762')).toBeDisabled();
|
||||
await page.locator('[data-menu-item-v1762="mobile-box-24"]').click();
|
||||
await expect(page.locator('#sunMenuDetailV1762 .sun-menu-readonly')).toContainText('Бокс 24');
|
||||
if(await page.evaluate(()=>matchMedia('(max-width:760px)').matches))await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0);
|
||||
await menuGeometry(page);
|
||||
await expect(page.locator('#sunMenuDetailV1762 > .dialog')).toHaveCount(0);
|
||||
expect(await page.evaluate(()=>localStorage.sunBoxes)).toBe(before);
|
||||
});
|
||||
|
||||
|
||||
test('a queued mobile return does not steal focus once the user starts typing',async({page})=>{
|
||||
await bootMenuFixture(page);
|
||||
await page.evaluate(()=>{
|
||||
document.querySelector('[data-menu-item-v1762="mobile-box-1"]').click();
|
||||
// Docking is queued by the existing menu handler. Start typing in the next
|
||||
// task, before the mobile requestAnimationFrame navigation callback runs.
|
||||
setTimeout(()=>{
|
||||
const input=document.getElementById('boxName');input.value='Уже редактирую';input.focus({preventScroll:true});
|
||||
},0);
|
||||
});
|
||||
await expect(page.locator('#sunMenuDetailV1762 > .dialog #boxName')).toHaveValue('Уже редактирую');
|
||||
await page.waitForTimeout(150);
|
||||
await expect(page.locator('#boxName')).toBeFocused();
|
||||
await expect(page.locator('#boxName')).toHaveValue('Уже редактирую');
|
||||
});
|
||||
@ -2,13 +2,13 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
export default defineConfig({
|
||||
testDir:'.',
|
||||
testMatch:['client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
||||
testMatch:['mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
||||
timeout:30000,
|
||||
use:{baseURL:'http://127.0.0.1:4173'},
|
||||
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
||||
projects:[
|
||||
{name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||
{name:'iphone-webkit',testMatch:['client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||
{name:'iphone-webkit',testMatch:['mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||
{name:'desktop',use:{...devices['Desktop Chrome']}},
|
||||
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
|
||||
]
|
||||
|
||||
19
tests/production-mobile-menu.mjs
Normal file
19
tests/production-mobile-menu.mjs
Normal file
@ -0,0 +1,19 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import {chromium} from '@playwright/test';
|
||||
import {bootMenuFixture,exerciseMenuNavigation} from './mobile-menu-scenario.mjs';
|
||||
const base=new URL(process.env.TIMEWEB_BASE_URL||'https://app.caterium.ru');
|
||||
if(base.protocol!=='https:')throw new Error('Production UI verification requires HTTPS');
|
||||
const output='production-ui-results';await fs.mkdir(output,{recursive:true});
|
||||
const browser=await chromium.launch(),results=[];
|
||||
try{
|
||||
for(const width of [390,1440]){
|
||||
const context=await browser.newContext({viewport:{width,height:900},serviceWorkers:'block'});
|
||||
try{
|
||||
const page=await context.newPage();
|
||||
await bootMenuFixture(page,base.href);
|
||||
const result=await exerciseMenuNavigation(page,{screenshot:async name=>{await page.screenshot({path:`${output}/${name}-${width}.png`,animations:'disabled'});}});
|
||||
results.push({width,...result});console.log(`PASS published menu ${width}px: form placement, arrow, unsaved values, save, unchanged orders`);
|
||||
}finally{await context.close();}
|
||||
}
|
||||
await fs.writeFile(`${output}/mobile-menu-result.json`,JSON.stringify({base:base.href,checkedAt:new Date().toISOString(),backend:'synthetic, network-blocked',results},null,2));
|
||||
}finally{await browser.close();}
|
||||
Loading…
Reference in New Issue
Block a user