caterium-app/tests/help-center.spec.mjs

95 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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 {test,expect} from '@playwright/test';
import fs from 'node:fs';
const kb=JSON.parse(fs.readFileSync('public/help/knowledge-v1.json','utf8'));
async function fixture(page){
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html; charset=utf-8',body:'<!doctype html><html><body><header><nav></nav></header><main>Рабочая база</main></body></html>'}));
await page.goto('/index.html');await page.addStyleTag({url:'/core/help-center.css'});await page.addScriptTag({url:'/core/help-center.js'});
}
test('manual search, categories and related instructions work without sending company data',async({page})=>{
expect(new Set(kb.articles.map(a=>a.id)).size).toBe(kb.articles.length);
expect(kb.articles.every(a=>a.related.every(id=>kb.articles.some(b=>b.id===id)))).toBe(true);
await fixture(page);await page.evaluate(()=>localStorage.setItem('sunOrders','private-order-canary'));
const requests=[];page.on('request',r=>requests.push(r.url()));
await page.getByRole('button',{name:'Помощь',exact:true}).click();
await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
await page.getByRole('searchbox').fill('предоплата');
await expect(page.locator('[data-article="payment"]')).toBeVisible();
await page.locator('[data-article="payment"] summary').click();await expect(page.locator('[data-article="payment"]')).toContainText('синий');
await page.locator('[data-article="payment"] [data-help-article="delivery"]').click();await expect(page.locator('[data-article="delivery"]')).toHaveAttribute('open','');
await page.getByRole('combobox',{name:'Категория',exact:true}).selectOption('Меню и ТТК');await expect(page.locator('#ctHelpResults details')).toHaveCount(3);
await page.getByRole('searchbox').fill('<img src=x onerror=alert(1)>');await expect(page.locator('#ctHelpStatus')).toContainText('Ничего не найдено');
expect(requests.every(u=>u.includes('/help/knowledge-v1.json'))).toBe(true);
expect(await page.evaluate(()=>localStorage.sunOrders)).toBe('private-order-canary');
});
test('support clearly stays offline, question opens a guide and dialog fits mobile',async({page})=>{
await fixture(page);await page.getByRole('button',{name:'Помощь',exact:true}).click();
await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
await page.getByRole('button',{name:'Поддержка',exact:true}).click();await expect(page.locator('#ctHelpSupport')).toContainText('пока не подключены');
await page.getByRole('button',{name:'Как отметить оплату?',exact:true}).click();await expect(page.getByRole('searchbox')).toHaveValue('оплата');
expect(await page.locator('#ctHelpDialog').evaluate(el=>el.getBoundingClientRect().width<=innerWidth&&el.scrollWidth<=el.clientWidth)).toBe(true);
await page.keyboard.press('Escape');await expect(page.locator('#ctHelpDialog')).not.toBeVisible();await expect(page.getByRole('button',{name:'Помощь',exact:true})).toBeFocused();
});
test('failed handbook load provides a working retry',async({page})=>{
let count=0;await page.route('**/help/knowledge-v1.json*',r=>++count===1?r.fulfill({status:503,body:''}):r.continue());
await fixture(page);await page.getByRole('button',{name:'Помощь',exact:true}).click();await expect(page.locator('#ctHelpStatus')).toContainText('Не удалось');
await page.getByRole('button',{name:'Повторить загрузку',exact:true}).click();await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
});
test('full app exposes help on login without exposing the workspace',async({page})=>{
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.getByRole('button',{name:'Помощь со входом',exact:true}).click();
await expect(page.locator('[data-article="iphone"]')).toBeVisible();await expect(page.locator('body > header')).toBeHidden();
await page.getByRole('button',{name:'Закрыть помощь',exact:true}).click();await expect(page.locator('#sunGateEmailV3')).toBeVisible();
});
test('Help uses a question-circle mask with the native sidebar icon geometry and colors',async({page})=>{
// No production account or network is needed to inspect the real navigation styles.
await page.route('https://**',r=>r.abort());
await page.route('**/api/index.php*',r=>r.fulfill({status:503,contentType:'application/json',body:'{}'}));
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
const help=page.locator('#ctHelpNav');
await expect(help).toHaveClass(/sun-nav-button/);
await page.locator('header nav .nav-settings').waitFor({state:'attached'});
// The narrow layout uses the common compact markers, not desktop SVG masks.
// Check its native geometry before exercising the actual left sidebar in each engine.
if(page.viewportSize().width<=900){
const sizes=await help.evaluate(el=>{
const size=node=>{const s=getComputedStyle(node,'::before');return {width:s.width,height:s.height,margin:s.margin};};
return {help:size(el),peer:size(document.querySelector('header nav .nav-settings'))};
});
expect(sizes.help).toEqual(sizes.peer);
await page.setViewportSize({width:1440,height:900});
}
await expect(page.locator('body')).toHaveClass(/sun-enterprise-sidebar/);
await expect.poll(()=>help.evaluate(el=>getComputedStyle(el,'::before').getPropertyValue('mask-image')||getComputedStyle(el,'::before').getPropertyValue('-webkit-mask-image'))).toContain('data:image/svg+xml');
const result=await help.evaluate(el=>{
const peer=document.querySelector('header nav .nav-settings');
const pseudo=node=>getComputedStyle(node,'::before');
const mask=pseudo(el).getPropertyValue('mask-image')||pseudo(el).getPropertyValue('-webkit-mask-image');
const dataUrl=mask.match(/url\(["']?(data:image\/svg\+xml,[^"')]+)["']?\)/)?.[1];
const svg=dataUrl?new DOMParser().parseFromString(decodeURIComponent(dataUrl.slice(dataUrl.indexOf(',')+1)),'image/svg+xml'):null;
const geometry=node=>{
const s=pseudo(node);
return {width:s.width,height:s.height,marginRight:s.marginRight,background:s.backgroundColor,opacity:s.opacity,maskSize:s.getPropertyValue('mask-size')||s.getPropertyValue('-webkit-mask-size')};
};
const normal={help:geometry(el),peer:geometry(peer)};
const hadHelp=el.classList.contains('on'),hadPeer=peer.classList.contains('on');
el.classList.add('on');peer.classList.add('on');
const active={help:geometry(el),peer:geometry(peer)};
el.classList.toggle('on',hadHelp);peer.classList.toggle('on',hadPeer);
return {normal,active,viewBox:svg?.documentElement.getAttribute('viewBox'),fill:svg?.documentElement.getAttribute('fill'),stroke:svg?.documentElement.getAttribute('stroke-width'),circle:svg?.querySelector('circle')?.getAttribute('r'),question:svg?.querySelector('path')?.getAttribute('d')};
});
expect(result.viewBox).toBe('0 0 24 24');expect(result.fill).toBe('none');expect(result.stroke).toBe('1.9');expect(result.circle).toBe('9');expect(result.question).toContain('M12 17h.01');
// Compare against the final app styles rather than an intermediate 20px rule.
expect(result.normal.help).toEqual(result.normal.peer);expect(result.active.help).toEqual(result.active.peer);
expect(parseFloat(result.normal.help.width)).toBeGreaterThanOrEqual(16);
expect(result.normal.help.width).toBe(result.normal.help.height);
await expect(help.locator('svg,img')).toHaveCount(0);
await expect(help).toHaveText('Помощь');
});