caterium-app/tests/production-ui-smoke.mjs
pavlov346346-source 424bf8ce87
Compact clients and explicit timed menu promotions (#31)
Add compact accessible client summaries and menu discounts in percent or rubles with explicit durations. Preserve ordinary catalog prices and order line snapshots; derive current prices and expire promotions automatically without background writes. Keep fractional prices, legacy sale compatibility, and regression coverage. Full pull-request QA passed. Publication remains gated by full main QA and byte-for-byte production asset and UI verification.
2026-09-19 09:34:50 +03:00

112 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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),/<circle[^>]*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`});
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('Акция');
await row.screenshot({path:`${output}/promotion-card-${width}.png`});
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();}