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:'
Рабочая база'}));
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('
');await expect(page.locator('#ctHelpStatus')).toContainText('Ничего не найдено');
expect(requests.every(u=>['/help/knowledge-v1.json','/core/support-form.js'].includes(new URL(u).pathname))).toBe(true);
expect(await page.evaluate(()=>localStorage.sunOrders)).toBe('private-order-canary');
});
test('support assistant connects only after an explicit click, inside a sandboxed iframe',async({page})=>{
await fixture(page);
const external=[];page.on('request',r=>{if(!r.url().startsWith('http://127.0.0.1'))external.push(r.url())});
await page.route('https://ai-staff-alpha.vercel.app/**',r=>r.fulfill({contentType:'text/html',body:'stub'}));
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(external).toEqual([]);
await page.getByRole('button',{name:'Поддержка',exact:true}).click();
await page.getByRole('button',{name:'Задать вопрос помощнику',exact:true}).click();
const frame=page.locator('#ctHelpBot iframe');
await expect(frame).toHaveAttribute('src',/^https:\/\/ai-staff-alpha\.vercel\.app\/embed\/[0-9a-f-]{36}$/);
await expect(frame).toHaveAttribute('referrerpolicy','no-referrer');
const sandbox=await frame.getAttribute('sandbox');expect(sandbox).toContain('allow-scripts');expect(sandbox).not.toMatch(/allow-top-navigation|allow-forms|allow-modals/);
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');
expect(result.normal.help).toEqual(result.normal.peer);expect(result.active.help).toEqual(result.active.peer);
// The shared legacy flex basis reserves 24px, while the mask itself is square.
// Validate the actual drawn mask, not an assumed square layout slot.
const [maskWidth,maskHeight=maskWidth]=result.normal.help.maskSize.split(/\s+/).map(parseFloat);
expect(maskWidth).toBeGreaterThanOrEqual(16);expect(maskWidth).toBe(maskHeight);
expect(maskWidth).toBeLessThanOrEqual(parseFloat(result.normal.help.width));
expect(maskHeight).toBeLessThanOrEqual(parseFloat(result.normal.help.height));
await expect(help.locator('svg,img')).toHaveCount(0);
await expect(help).toHaveText('Помощь');
});