174 lines
13 KiB
JavaScript
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; });
|