import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import {chromium,expect} from '@playwright/test'; // Fetch real production assets, but use a synthetic SDK session and workspace. // All backend traffic is blocked: this verifies published UI, not server login, // and never reads or modifies a customer account or database. const base=new URL(process.env.TIMEWEB_BASE_URL||'https://app.caterium.ru'); assert.equal(base.protocol,'https:'); const output='production-ui-results'; await fs.mkdir(output,{recursive:true}); const browser=await chromium.launch(); const results=[]; try{ for(const width of [1440,390]){ const context=await browser.newContext({viewport:{width,height:900},serviceWorkers:'block'}); try{ await context.route('**/*',route=>{ const request=route.request(),url=new URL(request.url()); if(request.method()==='GET'&&url.origin===base.origin&&!url.pathname.startsWith('/api/'))return route.continue(); return route.abort(); }); await context.addInitScript(()=>{ const workspaceId='ui-smoke-workspace'; localStorage.setItem('sunBoxes',JSON.stringify([{id:'ui-smoke-box',name:'Праздничный бокс',category:0,price:1000,ingredients:[],weight:'600 г',pieces:12}])); localStorage.setItem('sunOrders',JSON.stringify(Array.from({length:24},(_,i)=>({id:i+1,contact:`Клиент ${String(i+1).padStart(2,'0')}`,phone:`+7 900 000 ${String(i).padStart(4,'0')}`,event:'Фуршет',address:'Адрес клиента',date:'2099-12-20',time:'12:00',status:'Новый',lines:[{id:'ui-smoke-box',qty:1,price:1000}],total:1000})))); const user={id:'ui-smoke-user',email:'ui-smoke@example.invalid',email_confirmed_at:'2026-01-01T00:00:00Z'}; localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId,localWorkspaceId:workspaceId,tenantStorageReady:true,autoSync:false})); window.supabase={createClient:()=>({ auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:async()=>({data:{session:{user}},error:null}),getUser:async()=>({data:{user},error:null})}, rpc:async name=>{ if(name==='sun_my_workspaces')return new Promise(resolve=>{window.finishUiSmokeWorkspace=()=>resolve({data:[{id:workspaceId,name:'UI verification',role:'admin',is_active:true,permissions:{}}],error:null});}); if(name==='sun_subscription_snapshot')return {data:{plan_id:'full',plan_name:'Full',status:'active',access_mode:'full',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]))},error:null}; if(name==='caterium_trial_demo_status')return {data:{canInstall:false,canUpgrade:false},error:null}; if(name==='sun_is_platform_admin')return {data:false,error:null}; return {data:null,error:null}; }, channel:()=>({on(){return this;},subscribe(){return this;}}),removeChannel(){} })}; }); const page=await context.newPage(); await page.clock.setFixedTime(new Date()); await page.goto(base.href,{waitUntil:'domcontentloaded',timeout:60000}); const gate=page.locator('#sunCloudAuthGateV3'); await expect(gate).toHaveAttribute('data-auth-state','loading',{timeout:20000}); await expect(gate.locator('h2')).toHaveText('Загружаю рабочую базу'); await expect(gate.locator('button:visible,input:visible,a:visible')).toHaveCount(0); await expect(gate.locator('.sun-cloud-auth-error')).toBeHidden(); await expect(page.locator('body > header')).toBeHidden(); await page.screenshot({path:`${output}/loading-${width}.png`}); await page.evaluate(()=>window.finishUiSmokeWorkspace()); await expect(gate).toHaveCount(0,{timeout:20000}); await expect(page.locator('body > header')).toBeVisible(); const help=page.locator('#ctHelpNav'); await expect(help).toHaveClass(/sun-nav-button/); await help.scrollIntoViewIfNeeded(); let icon=null; if(width>900){ await expect.poll(()=>help.evaluate(el=>getComputedStyle(el,'::before').maskImage||getComputedStyle(el,'::before').webkitMaskImage)).toContain('data:image/svg+xml'); icon=await help.evaluate(el=>{ const peer=document.querySelector('header nav .nav-settings'); const size=node=>{const s=getComputedStyle(node,'::before');return {width:s.width,height:s.height,marginRight:s.marginRight,background:s.backgroundColor};}; const s=getComputedStyle(el,'::before'); return {help:size(el),peer:size(peer),mask:s.maskImage||s.webkitMaskImage}; }); assert.deepEqual(icon.help,icon.peer); assert.match(decodeURIComponent(icon.mask),/]*r="9"/); assert.match(decodeURIComponent(icon.mask),/M12 17h\.01/); await page.locator('body > header').screenshot({path:`${output}/sidebar-${width}.png`}); } await help.click(); await expect(page.locator('#ctHelpDialog')).toBeVisible(); await expect(page.locator('#ctHelpTitle')).toHaveText('Помощь в Caterium'); await expect.poll(()=>page.locator('#ctHelpResults details').count()).toBeGreaterThan(0); await page.locator('#ctHelpClose').click(); await expect(page.locator('#ctHelpDialog')).not.toBeVisible(); await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773&&window.CateriumPricing); const nav=page.locator('header nav'); await nav.getByRole('button',{name:'Клиенты',exact:true}).click(); const clients=page.locator('#client-list .client-card'); await expect(clients).toHaveCount(24); const clientSize=await clients.first().boundingBox(); assert(clientSize.height<=96); await page.screenshot({path:`${output}/clients-${width}.png`,animations:'disabled'}); await nav.getByRole('button',{name:'Меню',exact:true}).click(); await page.locator('[data-menu-item-v1762="ui-smoke-box"]').click(); await page.locator('#ctPromotionEnabled').check(); await page.locator('#ctDiscountType').selectOption('percent'); await page.locator('#ctDiscountValue').fill('15'); await expect(page.locator('#ctPromotionPreview')).toContainText('850 ₽'); await page.locator('#ctPromotionEditor').screenshot({path:`${output}/promotion-editor-${width}.png`}); await page.locator('#saveBoxButton').click(); const row=page.locator('[data-menu-item-v1762="ui-smoke-box"]'); await expect(row).toContainText('850 ₽'); await expect(row.locator('.ct-promotion-badge')).toHaveText('Акция'); // Saving normally redraws the menu list on a queued task. A viewport // screenshot does not retain an element handle that may detach meanwhile. // Keep the price/badge checks above and the expiry/order checks below. await row.scrollIntoViewIfNeeded(); await page.screenshot({path:`${output}/promotion-card-${width}.png`,animations:'disabled'}); const beforeOrders=await page.evaluate(()=>localStorage.sunOrders); const promo=await page.evaluate(()=>JSON.parse(localStorage.sunBoxes)[0]); assert.equal(promo.price,1000); assert.equal(Date.parse(promo.promotion.endsAt)-Date.parse(promo.promotion.startsAt),7*86400000); await page.clock.setFixedTime(new Date(promo.promotion.endsAt)); await page.evaluate(()=>window.dispatchEvent(new Event('focus'))); await expect(row).toContainText('1 000 ₽'); await expect(row.locator('.ct-promotion-badge')).toHaveCount(0); assert.equal(await page.evaluate(()=>localStorage.sunOrders),beforeOrders); results.push({width,quietLoading:true,automaticOpen:true,helpDialog:true,icon,clientSize,clients:24,percentPromotion:true,automaticExpiry:true,savedOrdersUnchanged:true}); console.log(`PASS published UI ${width}px: quiet loading, automatic open, Help dialog, compact clients and timed discount${icon?', native question-circle icon':''}`); }finally{await context.close();} } await fs.writeFile(`${output}/result.json`,JSON.stringify({base:base.href,checkedAt:new Date().toISOString(),backend:'mocked and network-blocked',results},null,2)); }finally{await browser.close();}