Files
meshcore-analyzer/tests/e2e/test-live-fullscreen-1572-e2e.js
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00

211 lines
9.5 KiB
JavaScript

#!/usr/bin/env node
/* Issue #1572 round-1 — Live fullscreen behavioral E2E.
*
* Replaces source-grep assertions (which a hidden no-op button or a
* dead-branch input guard would pass) with computed-style /
* keystroke-bus assertions in a real browser.
*
* Findings under test:
* A. body.live-fullscreen MUST be cleared on SPA route exit (mobile
* .bottom-nav is hidden by that class — leaking it strands the
* user on a navless page).
* B. Escape MUST exit fullscreen (no F-key dance to escape).
* C. F-key input guard: typing 'f' in an input MUST land in the input
* and MUST NOT enter fullscreen.
* D. Toggle round-trip: click → in fullscreen + .live-header-body
* computed display:none; click again → out + visible.
*/
'use strict';
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:13581';
async function main() {
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
let browser;
try {
browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
} catch (err) {
if (requireChromium) {
console.error(`test-live-fullscreen-1572-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
process.exit(1);
}
console.log(`test-live-fullscreen-1572-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
process.exit(0);
}
let failures = 0, passes = 0;
const fail = (m) => { failures++; console.error(' FAIL: ' + m); };
const pass = (m) => { passes++; console.log(' PASS: ' + m); };
const ctx = await browser.newContext();
const page = await ctx.newPage();
page.setDefaultTimeout(15000);
// Ensure no fullscreen pref leaks from a previous test run.
await page.addInitScript(() => {
try { localStorage.removeItem('live-fullscreen'); } catch (_) {}
});
async function waitForLive() {
await page.waitForSelector('.live-page', { timeout: 15000 });
await page.waitForSelector('#liveFullscreenToggle', { timeout: 15000 });
}
try {
// ─────────────────────────────────────────────────────────────
// Mobile viewport so .bottom-nav is the active nav (finding A).
await page.setViewportSize({ width: 390, height: 844 });
// ── D. Toggle round-trip ────────────────────────────────────
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });
await waitForLive();
let state = await page.evaluate(() => ({
hasClass: document.body.classList.contains('live-fullscreen'),
headerDisplay: getComputedStyle(document.querySelector('.live-header-body')).display,
}));
if (!state.hasClass) pass('D: body lacks live-fullscreen on initial load');
else fail(`D: body has live-fullscreen on initial load (display=${state.headerDisplay})`);
await page.click('#liveFullscreenToggle');
state = await page.evaluate(() => ({
hasClass: document.body.classList.contains('live-fullscreen'),
headerDisplay: getComputedStyle(document.querySelector('.live-header-body')).display,
}));
if (state.hasClass && state.headerDisplay === 'none') {
pass('D: click #1 → body.live-fullscreen AND .live-header-body display:none');
} else {
fail(`D: click #1 expected (true, none), got (${state.hasClass}, ${state.headerDisplay})`);
}
await page.click('#liveFullscreenToggle');
state = await page.evaluate(() => {
const bn = document.querySelector('[data-bottom-nav], .bottom-nav');
const sr = document.querySelector('.live-stats-row');
return {
hasClass: document.body.classList.contains('live-fullscreen'),
bnDisplay: bn ? getComputedStyle(bn).display : null,
statsPosition: sr ? getComputedStyle(sr).position : null,
};
});
// Round-trip success: body class cleared AND the user-visible
// fullscreen-only side-effects revert. `.bottom-nav` going back to
// display!=none is the key mobile signal; `.live-stats-row` losing
// its fixed/absolute pin is the desktop one. At least one must
// revert (depends on viewport and whether the nav is present).
if (!state.hasClass) {
const reverted =
(state.bnDisplay && state.bnDisplay !== 'none') ||
(state.statsPosition && state.statsPosition !== 'fixed' && state.statsPosition !== 'absolute');
if (reverted) {
pass(`D: click #2 → body cleared, fullscreen side-effects reverted (bnDisplay=${state.bnDisplay}, statsPos=${state.statsPosition})`);
} else {
fail(`D: click #2 cleared body but side-effects did not revert (bnDisplay=${state.bnDisplay}, statsPos=${state.statsPosition})`);
}
} else {
fail(`D: click #2 did NOT clear body.live-fullscreen`);
}
// ── B. Escape exits fullscreen ──────────────────────────────
await page.click('#liveFullscreenToggle'); // re-enter fullscreen
state = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
if (state) pass('B: re-entered fullscreen before Escape test');
else fail('B: setup — re-enter fullscreen failed');
// Press Escape on body (not in an input).
await page.evaluate(() => document.body.focus());
await page.keyboard.press('Escape');
state = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
if (!state) pass('B: Escape cleared body.live-fullscreen');
else fail('B: Escape did NOT clear body.live-fullscreen');
// ── C. F-key input guard (behavioral) ───────────────────────
// Make sure we are NOT in fullscreen.
let fs = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
if (fs) await page.click('#liveFullscreenToggle');
const filterSel = '#liveNodeFilterInput';
// Controls are collapsed by default — expand so the filter input is
// focusable. If the toggle is absent for some reason, fall through.
const ctlToggle = await page.$('#liveControlsToggle');
if (ctlToggle) await ctlToggle.click();
await page.waitForTimeout(100);
const hasFilter = await page.$(filterSel);
if (!hasFilter) {
fail('C: #liveNodeFilterInput not present — cannot run input-guard test');
} else {
const visible = await page.evaluate((sel) => {
const el = document.querySelector(sel);
const cs = el ? getComputedStyle(el) : null;
const r = el ? el.getBoundingClientRect() : null;
return !!el && cs.display !== 'none' && cs.visibility !== 'hidden' && r.width > 0 && r.height > 0;
}, filterSel);
if (!visible) {
fail('C: #liveNodeFilterInput is not visible after expanding controls');
} else {
await page.focus(filterSel);
await page.keyboard.type('f');
const result = await page.evaluate((sel) => ({
hasClass: document.body.classList.contains('live-fullscreen'),
value: document.querySelector(sel).value,
}), filterSel);
if (!result.hasClass && /f/i.test(result.value)) {
pass(`C: typing 'f' in #liveNodeFilterInput did NOT toggle fullscreen (input value="${result.value}")`);
} else {
fail(`C: F-key leaked into toggle — hasClass=${result.hasClass}, value="${result.value}"`);
}
}
}
// ── A. body class cleared on route exit ─────────────────────
// Enter fullscreen, then navigate to /#/nodes; assert body class
// gone AND .bottom-nav not display:none.
fs = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
if (!fs) await page.click('#liveFullscreenToggle');
fs = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
if (!fs) {
fail('A: setup — could not enter fullscreen');
} else {
pass('A: setup — in fullscreen before SPA nav');
}
// SPA navigate via hash change (no full reload).
await page.evaluate(() => { location.hash = '#/nodes'; });
await page.waitForFunction(() => location.hash.indexOf('#/nodes') === 0);
// Give the router and destroy() a tick.
await page.waitForTimeout(150);
const post = await page.evaluate(() => {
const bn = document.querySelector('[data-bottom-nav], .bottom-nav');
return {
hasClass: document.body.classList.contains('live-fullscreen'),
bnPresent: !!bn,
bnDisplay: bn ? getComputedStyle(bn).display : null,
};
});
if (!post.hasClass) pass('A: body.live-fullscreen cleared after SPA nav to /#/nodes');
else fail('A: body.live-fullscreen LEAKED after SPA nav (.bottom-nav would be hidden on mobile)');
if (post.bnPresent && post.bnDisplay && post.bnDisplay !== 'none') {
pass(`A: .bottom-nav visible after nav (display=${post.bnDisplay})`);
} else if (!post.bnPresent) {
fail('A: .bottom-nav not present in DOM after SPA nav');
} else {
fail(`A: .bottom-nav has display:none after SPA nav — user stranded`);
}
} finally {
await browser.close();
}
console.log(`\n#1572 fullscreen E2E: ${passes} passed, ${failures} failed`);
process.exit(failures > 0 ? 1 : 0);
}
main().catch((e) => { console.error(e); process.exit(1); });