Files
hockey_new/tests/program_log_browser.cjs

80 lines
8.5 KiB
JavaScript

// Optional browser regression: install Playwright + Chromium, then node tests/program_log_browser.cjs.
const http=require('node:http');
const fs=require('node:fs');
const path=require('node:path');
const assert=require('node:assert/strict');
const os = require('node:os');
const {chromium}=require('playwright');
const root=path.resolve(__dirname, '..');
const out=process.env.HOCKEY_UI_QA_DIR || fs.mkdtempSync(path.join(os.tmpdir(), 'hockey-log-ui-'));
let admin=true, fail=false;
const requests=[];
const html=`<!doctype html><html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;background:#080d15;color:#ddd;font:13px Arial}.runtime-topbar{display:flex;align-items:center;justify-content:space-between;padding:20px;background:#152335}.runtime-actions{display:flex;gap:5px}.icon-btn{height:36px;background:#192f42;border:1px solid #375266;color:#ddd;border-radius:8px}</style><link rel="stylesheet" href="/program-log.css"><body><header class="runtime-topbar"><b>Хоккей · BUILD190</b><div class="runtime-actions"><button class="icon-btn">⚙</button></div></header><main>Матч · Зенит — Динамо</main><script>window.UI_BUILDER_BOOT={mode:'runtime'};window.shortcutCount=0;window.addEventListener('keydown',event=>{if(window.HockeyProgramLog?.isOpen?.())return;if(event.code==='Space'||event.key==='F1')window.shortcutCount++;},true);</script><script src="/program-log.js"></script></body></html>`;
const item=(id,msg)=>({id,timestamp:'2026-10-21T09:35:06Z',level:id===1?'ERROR':'INFO',category:'mapping',event:'mapping.confirmed',message:msg,actor_id:'u1',actor_login:'Юрий',game_id:'902918',device_id:'GFX-01',request_id:'r'+id,details:{value:'<img src=x onerror="window.xss=true">',applied:33}});
const server=http.createServer((req,res)=>{
const url=new URL(req.url,'http://127.0.0.1');requests.push(url);
const json=(data,status=200)=>{res.writeHead(status,{'Content-Type':'application/json'});res.end(JSON.stringify(data))};
if(url.pathname==='/'){res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'});res.end(html);return}
if(['/program-log.js','/program-log.css'].includes(url.pathname)){res.writeHead(200,{'Content-Type':url.pathname.endsWith('.js')?'text/javascript':'text/css'});res.end(fs.readFileSync(root+'/hockey_data/static'+url.pathname));return}
if(url.pathname==='/api/hockey/me'){json({id:'u1',is_admin:admin,login:'admin'});return}
if(url.pathname==='/api/hockey/program-logs/options'){json({categories:['mapping','server'],actors:[{id:'u1',login:'Юрий'}],game_ids:['902918'],device_ids:['GFX-01']});return}
if(url.pathname==='/api/hockey/program-logs/download'){res.writeHead(200,{'Content-Type':'text/plain;charset=utf-8','Content-Disposition':'attachment; filename="hockey-program-log-test.'+(url.searchParams.get('format')==='jsonl'?'jsonl':'txt')+'"'});res.end('Selected logs for '+url.searchParams.toString());return}
if(url.pathname==='/api/hockey/program-logs'){
if(fail){json({detail:'Тестовая ошибка журнала'},500);return}
const q=url.searchParams.get('q')||'',cursor=url.searchParams.get('cursor');
const payload={items:[item(cursor?3:1,q||'Подключился Agent'),item(cursor?4:2,'Команда подтверждена')],next_cursor:cursor?'':'page2',has_more:!cursor,timezone:'Europe/Moscow',stats:{dropped:0,write_errors:0}};
if(q==='old')setTimeout(()=>json(payload),700);else json(payload);return;
}
res.writeHead(404);res.end();
});
(async()=>{
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
const base='http://127.0.0.1:'+server.address().port;
const browser=await chromium.launch({...(process.env.HOCKEY_CHROMIUM_PATH ? {executablePath: process.env.HOCKEY_CHROMIUM_PATH} : {}), headless:true, args:['--no-sandbox','--disable-dev-shm-usage', ...(process.env.HOCKEY_CHROMIUM_PATH ? ['--no-zygote','--disable-gpu','--disable-gpu-compositing','--disable-software-rasterizer','--use-gl=disabled'] : [])]});
const page=await browser.newPage({viewport:{width:1366,height:1000}});
const errors=[];page.on('pageerror',e=>errors.push(String(e)));
await page.goto(base);
await page.getByRole('button',{name:'Журнал',exact:true}).click();
await page.getByText('Подключился Agent',{exact:true}).waitFor();
assert.equal(await page.locator('.hpl-dialog').evaluate(e=>e.open),true);
assert.match(await page.locator('.hpl-entry-head time').first().innerText(),/12:35:06/);
await page.locator('.hpl-details').first().locator('summary').click();
assert.equal(await page.locator('.hpl-details img').count(),0);
assert.equal(await page.evaluate(()=>window.xss),undefined);
await page.screenshot({path:out+'/desktop.png',fullPage:true});
await page.locator('input[name=q]').fill('mapping');
await page.waitForTimeout(600);
assert.equal(await page.locator('.hpl-message').first().innerText(),'mapping');
const todayResponse=page.waitForResponse(r=>r.url().includes('program-logs?')&&new URL(r.url()).searchParams.get('preset')==='today');
await page.getByRole('button',{name:'Сегодня',exact:true}).click();await todayResponse;
await page.locator('input[name=date_from]').fill('2026-10-01');
await page.locator('input[name=date_from]').dispatchEvent('change');
await page.locator('input[name=date_to]').fill('2026-10-19');
await page.locator('input[name=date_to]').dispatchEvent('change');
await page.waitForTimeout(150);
await page.locator('select[name=sort]').selectOption('asc');
await page.waitForTimeout(150);
let last=requests.filter(u=>u.pathname==='/api/hockey/program-logs').at(-1);
assert.equal(last.searchParams.get('date_from'),'2026-10-01');assert.equal(last.searchParams.get('date_to'),'2026-10-19');assert.equal(last.searchParams.has('preset'),false);assert.equal(last.searchParams.get('sort'),'asc');
await page.getByRole('button',{name:'Далее →',exact:true}).click();
await page.waitForTimeout(150);assert.match(await page.locator('.hpl-page').innerText(),/Страница 2/);
const downloadPromise=page.waitForEvent('download');await page.getByRole('link',{name:'Скачать TXT',exact:true}).click();const download=await downloadPromise;
assert.equal(download.suggestedFilename(),'hockey-program-log-test.txt');
await download.saveAs(out+'/download.txt');assert.match(fs.readFileSync(out+'/download.txt','utf8'),/date_from=2026-10-01/);assert.doesNotMatch(fs.readFileSync(out+'/download.txt','utf8'),/cursor=/);
await page.getByRole('button',{name:'← Назад',exact:true}).click();await page.waitForTimeout(150);
await page.locator('input[name=q]').fill('old');await page.waitForTimeout(500);await page.locator('input[name=q]').fill('new');await page.waitForTimeout(1300);assert.equal(await page.locator('.hpl-message').first().innerText(),'new');
await page.locator('input[name=q]').focus();await page.keyboard.type(' with spaces');await page.keyboard.press('F1');assert.equal(await page.evaluate(()=>window.shortcutCount),0);
fail=true;await page.getByRole('button',{name:'Обновить',exact:true}).click();await page.getByRole('alert').waitFor();assert.equal(await page.getByRole('alert').innerText(),'Тестовая ошибка журнала');fail=false;
await page.getByRole('button',{name:'Закрыть журнал',exact:true}).click();assert.equal(await page.locator('.hpl-dialog').evaluate(e=>e.open),false);
const countClosed=requests.filter(u=>u.pathname==='/api/hockey/program-logs').length;await page.waitForTimeout(5100);assert.equal(requests.filter(u=>u.pathname==='/api/hockey/program-logs').length,countClosed);
await page.setViewportSize({width:390,height:844});await page.getByRole('button',{name:'Журнал',exact:true}).click();await page.waitForTimeout(200);
assert.equal(await page.locator('.hpl-dialog').evaluate(e=>e.scrollWidth<=e.clientWidth+1),true);
assert.equal(await page.locator('.hpl-content').evaluate(e=>e.scrollWidth<=e.clientWidth+1),true);
await page.screenshot({path:out+'/mobile.png',fullPage:true});
await page.locator('.hpl-list').scrollIntoViewIfNeeded();await page.screenshot({path:out+'/mobile-entries.png',fullPage:true});
admin=false;const viewer=await browser.newPage();await viewer.goto(base);await viewer.waitForTimeout(200);assert.equal(await viewer.getByRole('button',{name:'Журнал',exact:true}).count(),0);
assert.deepEqual(errors,[]);
console.log('PASS: desktop/mobile, dates/presets/search/sort/pages, streamed download, stale responses, XSS, hotkeys, server errors, closed polling, non-admin gating.');
await browser.close();server.close();
})().catch(e=>{console.error(e);server.close();process.exit(1)});