// 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=`
Хоккей · BUILD190
Матч · Зенит — Динамо
`; 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:'',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)});