Files
hockey_new/tests/build199_aux_header.cjs

174 lines
13 KiB
JavaScript

'use strict';
// Native Chromium + complete production header and app. Only startup/rendering
// I/O and external account/server responses are replaced in memory.
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
const root = process.argv[3] || path.join(__dirname, '..');
const workspace = path.resolve(__dirname, '../../..');
let playwright;
for (const candidate of [process.env.HOCKEY_TEST_PLAYWRIGHT, 'playwright',
process.env.CODEX_PRIMARY_RUNTIME_NODE_MODULES && path.join(process.env.CODEX_PRIMARY_RUNTIME_NODE_MODULES, 'playwright'),
'/opt/codex/runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'].filter(Boolean)) {
try { playwright = require(candidate); break; } catch (_) {}
}
assert.ok(playwright, 'Playwright is required');
const asset = name => fs.readFileSync(path.join(root, name), 'utf8');
const originalApp = asset('ui_builder/static/app.js');
const bootstrap = ' init();\n})();';
assert.equal(originalApp.split(bootstrap).length, 2, 'Expected exactly one production bootstrap');
const app = originalApp.replace(bootstrap, `
// Rendering the full roster/canvas is I/O outside this header integration.
renderRuntime = () => {};
window.__auxHeader = { state, bindEvents, shortcutReferenceItems };
})();`);
let browser;
async function fixture({ holdFlags = false } = {}) {
const chromium = process.env.HOCKEY_TEST_CHROMIUM || path.join(workspace, 'audit/browser_test/chromium');
browser = await playwright.chromium.launch({ headless: true, executablePath: chromium,
env: { ...process.env, LD_LIBRARY_PATH: path.join(workspace, 'audit/browser_test/libs') },
args: ['--no-sandbox', '--no-zygote', '--disable-dev-shm-usage', '--disable-gpu', '--use-gl=disabled'] });
const context = await browser.newContext({ viewport: { width: 1150, height: 780 } });
const unexpected = [], errors = [];
await context.route('**/*', route => {
if (route.request().url() === 'http://aux-header-199.invalid/') return route.fulfill({ contentType: 'text/html', body:
'<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><header class="runtime-topbar"></header><div id="modalHost"></div><input id="textEditor"><button id="focusBody">Focus</button></body></html>' });
unexpected.push(route.request().url()); return route.abort();
});
const page = await context.newPage(); page.setDefaultTimeout(6000);
page.on('pageerror', error => errors.push(String(error)));
await page.goto('http://aux-header-199.invalid/');
for (const name of ['ui_builder/static/styles.css', 'hockey_data/static/match-status-header.css']) await page.addStyleTag({ content: asset(name) });
await page.evaluate(({ holdFlags }) => {
window.UI_BUILDER_BOOT = { mode: 'runtime', editorHotkey: 'Ctrl+Shift+E' };
window.__rights = true; window.__accessGeneration = 1; window.__flagRequests = [];
window.__api = []; window.__unexpectedApi = []; window.__holdFlags = holdFlags; window.__heldFlags = []; window.__legacyF2 = 0; window.__openedTimers = 0;
localStorage.setItem('hockey.accountId', 'operator199');
localStorage.setItem('hockey.operatorSessionToken', 'session199');
localStorage.setItem('hockey.vmix.selected_device', 'agent199');
window.HockeyAgentRuntime = { currentDeviceId: () => 'agent199' };
window.HockeyMatchAccess = {
storage: localStorage, canControl: () => window.__rights,
snapshot: () => ({ generation: window.__accessGeneration, participant_id: 'participant199' }),
paint() {}, isPeriodPending: () => false, isWritePending: () => false,
};
window.__control = { game_id: 'g', revision: 1, current_period: '1', period_options: [{ value: '1', label: '1 период' }],
flags: {}, values: {}, score: { home: 0, away: 0, manual: true }, strength: {}, timers: {},
timer_rules: { minutes: 20 }, scoreboard_team_states: {} };
window.fetch = async (input, init = {}) => {
const url = new URL(String(input), location.href), body = init.body ? JSON.parse(init.body) : null;
window.__api.push({ path: url.pathname, method: init.method || 'GET', body });
const response = value => new Response(JSON.stringify(value), { headers: { 'Content-Type': 'application/json' } });
if (url.pathname === '/api/hockey/games/g/control/flags') {
window.__flagRequests.push(body);
if (window.__holdFlags) await new Promise(resolve => window.__heldFlags.push(resolve));
Object.assign(window.__control.flags, body.flags); window.__control.revision++;
return response(structuredClone(window.__control));
}
if (url.pathname === '/api/hockey/games/g/control') return response(structuredClone(window.__control));
if (url.pathname === '/api/hockey/games/g/control/timers') return response(structuredClone(window.__control));
if (url.pathname === '/api/hockey/vmix/sequence') return response({ ok: true, confirmation: 'ack', applied: body.commands.length,
results: body.commands.map(command => ({ ok: true, function: command.Function, ack_waited: true })) });
if (/\/apply-mapping$/.test(url.pathname)) return response({ ok: true, complete: true, mapping_ack_waited: true,
total_commands: 1, text_fields_total: 1, text_fields_confirmed: 1, errors: [], missing: [], rule_missing: [], warnings: [] });
window.__unexpectedApi.push(url.pathname);
throw new Error('Unexpected browser API: ' + url.pathname);
};
window.addEventListener('legacy-f2', () => window.__legacyF2++);
}, { holdFlags });
await page.addScriptTag({ content: app });
await page.evaluate(() => {
const state = window.__auxHeader.state;
const timer = { id: 'main', action_id: 'hockey_game_timer', type: 'timer', title: 'Время периода', hidden: false,
props: { runtimeHidden: true, mode: 'count_down', startTime: '20:00', endTime: '00:00', format: 'mm_ss', persist: false } };
state.config.components = [timer, { id: 'board', type: 'hockey_penalty_dashboard', action_id: 'hockey_penalty_dashboard', props: {} }];
state.config.shortcut_sequences = [{ id: 'legacyF2', name: 'Old saved F2', combo: 'F2', enabled: true, scope: 'runtime',
steps: [{ id: 'dispatch', type: 'dispatch_event', enabled: true, condition: 'always', event_name: 'legacy-f2', detail: '{}' }] }];
state.data = { hockey: { selected_game: { external_id: 'g', date: '2026-11-03', home: { name: 'HOME', score: 0, players: [] }, away: { name: 'AWAY', score: 0, players: [] } },
language: { display: 'ru' }, operator_session: { token: 'session199' }, game_control: structuredClone(window.__control) } };
state.hockeyGameControl.g = structuredClone(window.__control);
state.hockeyTimerGameId = 'g'; state.hockeySelectionContextGameId = 'g'; state.hockeySelectionGeneration = 1;
window.UIBuilderRuntime.getTimer('hockey_game_timer');
Object.assign(state.timers.hockey_game_timer, { currentMs: 1043000, running: true, paused: false, startedOnce: true });
window.UIBuilderRuntime.openTimersEditor = () => window.__openedTimers++;
window.__auxHeader.bindEvents();
});
await page.addScriptTag({ content: asset('hockey_data/static/match-status-header.js') });
const checkbox = page.locator('#hockeyMatchStatusHeader [data-hockey-auxiliary-toggle]');
await checkbox.waitFor({ state: 'visible' });
const settled = async () => page.waitForFunction(() => !window.UIBuilderRuntime.getAuxiliaryGate().pending);
const flags = async () => page.evaluate(() => window.__flagRequests.map(row => row.flags.scoreboard_auxiliaries_enabled));
const clean = async () => { assert.deepEqual(errors, [], 'No browser page errors'); assert.deepEqual(unexpected, [], 'No external requests');
assert.deepEqual(await page.evaluate(() => window.__unexpectedApi), [], 'No swallowed unexpected API failures'); };
return { page, checkbox, settled, flags, clean, errors, unexpected };
}
const cases = {};
cases.header_operator_f2 = async () => {
const h = await fixture({ holdFlags: true }), { page, checkbox } = h;
assert.equal(await checkbox.count(), 1, 'Hidden standalone timer must not duplicate the visible header checkbox');
assert.equal(await checkbox.isChecked(), false);
assert.equal(await page.locator('.hockey-status-clock > span').textContent(), 'ОСНОВНОЙ ТАЙМЕР');
await checkbox.click();
await page.waitForFunction(() => window.__heldFlags.length === 1);
assert.equal(await checkbox.isChecked(), true, 'Pending checkbox shows the requested ON state');
assert.deepEqual(await page.evaluate(() => { const gate = window.UIBuilderRuntime.getAuxiliaryGate(); return { pending: gate.pending, enabled: gate.enabled }; }), { pending: true, enabled: false });
await page.evaluate(() => { window.__holdFlags = false; window.__heldFlags.shift()(); });
await h.settled(); assert.equal(await checkbox.isChecked(), true);
await page.locator('#focusBody').focus();
await page.keyboard.down('F2'); await page.keyboard.down('F2'); await h.settled(); await page.keyboard.up('F2');
assert.deepEqual(await h.flags(), [true, false], 'One physical F2 press performs exactly one flag write');
assert.equal(await checkbox.isChecked(), false);
await page.keyboard.press('F2'); await h.settled();
assert.deepEqual(await h.flags(), [true, false, true]);
assert.equal(await checkbox.isChecked(), true);
assert.equal(await page.evaluate(() => window.__legacyF2), 0, 'Reserved F2 must not execute an old saved graphics shortcut');
await page.locator('#textEditor').focus(); await page.keyboard.press('F2');
await page.waitForTimeout(50); assert.deepEqual(await h.flags(), [true, false, true]);
await page.locator('#focusBody').focus();
await page.evaluate(() => document.documentElement.classList.add('hockey-match-loading-active'));
await page.keyboard.press('F2');
await page.evaluate(() => document.documentElement.classList.remove('hockey-match-loading-active'));
await page.evaluate(() => { window.HockeyProgramLog = { isOpen: () => true }; });
await page.keyboard.press('F2');
await page.evaluate(() => { window.HockeyProgramLog = { isOpen: () => false }; });
assert.deepEqual(await h.flags(), [true, false, true]);
const reference = await page.evaluate(() => window.__auxHeader.shortcutReferenceItems().filter(item => item.combo === 'F2'));
assert.equal(reference.length, 1); assert.equal(reference[0].name, 'Плашки поверх счёта');
for (let n = 0; n < 3; n++) {
await page.evaluate(() => { window.__auxHeader.state.timers.hockey_game_timer.currentMs -= 1000; window.dispatchEvent(new CustomEvent('hockey:auxiliary-gate-changed')); });
}
await checkbox.click(); await h.settled();
assert.deepEqual(await h.flags(), [true, false, true, false], 'Header refreshes must not install duplicate checkbox listeners');
const geometry = await page.locator('.hockey-status-clock').evaluate(clock => {
const outer = clock.getBoundingClientRect(), gate = clock.querySelector('.hockey-auxiliary-gate').getBoundingClientRect();
return { left: gate.left >= outer.left, right: gate.right <= outer.right, top: gate.top >= outer.top, bottom: gate.bottom <= outer.bottom };
});
assert.deepEqual(geometry, { left: true, right: true, top: true, bottom: true }, 'The control remains inside the main-timer card');
await h.clean();
};
cases.header_viewer_clock = async () => {
const h = await fixture(), { page, checkbox } = h;
await page.evaluate(() => { window.__checkboxNode = document.querySelector('[data-hockey-auxiliary-toggle]'); window.__clockNode = document.querySelector('.hockey-status-clock'); });
assert.equal(await page.locator('.hockey-status-clock strong').textContent(), '17:23');
await page.evaluate(() => { window.__auxHeader.state.timers.hockey_game_timer.currentMs = 1042000; });
await page.waitForFunction(() => document.querySelector('.hockey-status-clock strong').textContent === '17:22');
assert.equal(await page.evaluate(() => window.__checkboxNode === document.querySelector('[data-hockey-auxiliary-toggle]')), true, 'Clock ticks must retain the focused checkbox node');
assert.equal(await page.evaluate(() => window.__clockNode === document.querySelector('.hockey-status-clock')), true, 'Clock node remains stable for double-click editing');
await checkbox.dblclick(); await h.settled();
assert.equal(await page.evaluate(() => window.__openedTimers), 0, 'Checkbox double-click must not open the clock editor');
await page.locator('.hockey-status-clock strong').dblclick();
assert.equal(await page.evaluate(() => window.__openedTimers), 1);
const before = await h.flags();
await page.evaluate(() => { window.__rights = false; window.__accessGeneration++; window.dispatchEvent(new CustomEvent('hockey:access-changed')); });
await page.waitForFunction(() => document.querySelector('[data-hockey-auxiliary-toggle]').disabled);
assert.equal(await checkbox.isDisabled(), true);
await page.locator('#focusBody').focus(); await page.keyboard.press('F2');
await page.waitForTimeout(50); assert.deepEqual(await h.flags(), before);
assert.equal(await page.evaluate(() => window.__legacyF2), 0);
await h.clean();
};
async function main() {
const name = process.argv[2]; assert.ok(cases[name], name);
try { await cases[name](); console.log(`PASS BUILD199 auxiliary header: ${name}`); }
finally { await browser?.close(); }
}
main().catch(error => { console.error(error); process.exitCode = 1; });