mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-12 07:11:39 +00:00
test(live): RED — assert fullscreen toggle + default-collapsed controls (#1532)
This commit is contained in:
@@ -128,6 +128,7 @@ jobs:
|
||||
node test-issue-1438-marker-css-vars.js
|
||||
node test-issue-1562-observers-summary.js
|
||||
node test-live.js
|
||||
node test-issue-1532-live-fullscreen.js
|
||||
node test-xss-escape-sinks.js
|
||||
node test-preflight-xss-gate.js
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ node test-issue-1456-score-labels.js
|
||||
node test-issue-1461-mobile-page-actions.js
|
||||
node test-issue-1470-node-tile-helper.js
|
||||
node test-issue-1485-live-anim-z.js
|
||||
node test-issue-1532-live-fullscreen.js
|
||||
node test-issue-1473-reserved-prefixes.js
|
||||
node test-issue-1473-prefix-generator.js
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* #1532 — Live page: fullscreen toggle + collapse controls by default.
|
||||
*
|
||||
* Per triage fix path (Kpa-clawbot/CoreScope#1532):
|
||||
* 1. `.live-controls` is collapsed by default on desktop too, not just
|
||||
* mobile (existing `#liveControlsToggle` reveals it).
|
||||
* 2. A new `#liveFullscreenToggle` button sits next to ⚙ — toggles a
|
||||
* `body.live-fullscreen` class. CSS under that class hides
|
||||
* `.live-header-body`, `.live-controls-body`, `.vcr-controls`, and
|
||||
* `.bottom-nav`; `.live-stats-row` stays pinned (top-right).
|
||||
* 3. Pin-icon parity with the map-controls accordion in map.js.
|
||||
*
|
||||
* Plus: keyboard shortcut `F` toggles fullscreen, with focus-in-input
|
||||
* guard so it doesn't fire while typing in the node-filter.
|
||||
*
|
||||
* Source-invariant assertions on public/live.js + public/live.css. Same
|
||||
* approach as test-issue-1485-live-anim-z.js so the test runs in the JS
|
||||
* unit-test gate (no playwright/server needed).
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
||||
else { failed++; console.error(' ✗ ' + msg); }
|
||||
}
|
||||
|
||||
const liveJs = fs.readFileSync(path.join(__dirname, 'public', 'live.js'), 'utf8');
|
||||
const liveCss = fs.readFileSync(path.join(__dirname, 'public', 'live.css'), 'utf8');
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 A: #liveFullscreenToggle button declared ===');
|
||||
|
||||
assert(
|
||||
/id=["']liveFullscreenToggle["']/.test(liveJs),
|
||||
'#liveFullscreenToggle id appears in live.js init() HTML template'
|
||||
);
|
||||
|
||||
assert(
|
||||
/liveFullscreenToggle[\s\S]{0,400}aria-label/.test(liveJs),
|
||||
'#liveFullscreenToggle has an aria-label attribute (a11y)'
|
||||
);
|
||||
|
||||
// Button sits *next to* the existing settings (⚙) toggle. Cheap proxy:
|
||||
// both ids appear within ~600 chars of each other in the source.
|
||||
{
|
||||
const cIdx = liveJs.indexOf('liveControlsToggle');
|
||||
const fIdx = liveJs.indexOf('liveFullscreenToggle');
|
||||
assert(
|
||||
cIdx > 0 && fIdx > 0 && Math.abs(cIdx - fIdx) < 1200,
|
||||
'#liveFullscreenToggle is co-located with #liveControlsToggle in the header template'
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 B: fullscreen toggle wires body.live-fullscreen ===');
|
||||
|
||||
// A click handler + a body class toggle. The handler must reference
|
||||
// 'live-fullscreen' (the class name CSS hangs hides off of).
|
||||
assert(
|
||||
/liveFullscreenToggle[\s\S]{0,800}addEventListener\(\s*['"]click['"]/.test(liveJs),
|
||||
'#liveFullscreenToggle has a click listener'
|
||||
);
|
||||
assert(
|
||||
/document\.body\.classList\.toggle\(\s*['"]live-fullscreen['"]/.test(liveJs),
|
||||
'click handler toggles document.body.classList["live-fullscreen"]'
|
||||
);
|
||||
|
||||
// Persist via localStorage so the choice survives reloads.
|
||||
assert(
|
||||
/localStorage[\s\S]{0,200}live-fullscreen/.test(liveJs),
|
||||
'fullscreen state is persisted to localStorage (key contains "live-fullscreen")'
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 C: keyboard shortcut F toggles fullscreen ===');
|
||||
|
||||
// keydown listener gated on the 'f'/'F' key, with input/textarea guard
|
||||
// so the shortcut doesn't fire while focus is in the node-filter input.
|
||||
assert(
|
||||
/addEventListener\(\s*['"]keydown['"]/.test(liveJs),
|
||||
'a keydown listener is registered'
|
||||
);
|
||||
assert(
|
||||
/(key\s*===?\s*['"]f['"]|key\s*===?\s*['"]F['"]|toLowerCase\(\)\s*===?\s*['"]f['"])/.test(liveJs),
|
||||
'keydown handler matches the F key'
|
||||
);
|
||||
// Guard: don't fire when focus is in an input/textarea/contenteditable.
|
||||
assert(
|
||||
/(tagName[\s\S]{0,80}(INPUT|TEXTAREA)|isContentEditable|matches\([^)]*input)/i.test(liveJs),
|
||||
'keydown handler skips when focus is in an INPUT/TEXTAREA/contenteditable'
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 D: CSS hides chrome under body.live-fullscreen ===');
|
||||
|
||||
function cssHides(selector) {
|
||||
// Match: body.live-fullscreen <whitespace> <selector> { ... display: none ... }
|
||||
// OR a comma-list containing the selector.
|
||||
const re = new RegExp(
|
||||
'body\\.live-fullscreen[^{}]*' + selector.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') +
|
||||
'[\\s\\S]{0,600}?display\\s*:\\s*none'
|
||||
);
|
||||
return re.test(liveCss);
|
||||
}
|
||||
|
||||
assert(cssHides('.live-header-body'),
|
||||
'body.live-fullscreen hides .live-header-body (display:none)');
|
||||
assert(cssHides('.live-controls-body'),
|
||||
'body.live-fullscreen hides .live-controls-body (display:none)');
|
||||
assert(cssHides('.vcr-controls'),
|
||||
'body.live-fullscreen hides .vcr-controls (display:none)');
|
||||
assert(cssHides('.bottom-nav'),
|
||||
'body.live-fullscreen hides .bottom-nav (display:none)');
|
||||
|
||||
// .live-stats-row must remain visible AND get pinned positioning.
|
||||
// Negative: no `body.live-fullscreen .live-stats-row { display: none }`.
|
||||
{
|
||||
const re = /body\.live-fullscreen[^{}]*\.live-stats-row[\s\S]{0,400}?display\s*:\s*none/;
|
||||
assert(!re.test(liveCss),
|
||||
'.live-stats-row is NOT hidden by body.live-fullscreen (must stay visible)');
|
||||
}
|
||||
// Positive: pinned positioning under fullscreen.
|
||||
assert(
|
||||
/body\.live-fullscreen[\s\S]{0,800}?\.live-stats-row[\s\S]{0,400}?position\s*:\s*(fixed|absolute)/.test(liveCss),
|
||||
'.live-stats-row gets fixed/absolute positioning under body.live-fullscreen'
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 E: .live-controls collapsed by default on desktop ===');
|
||||
|
||||
// The pre-#1532 collapse rule lived inside @media (max-width: 768px).
|
||||
// Post-#1532 the body-toggle / hidden-attribute path must apply
|
||||
// regardless of viewport. We detect this by asserting that the
|
||||
// applyForViewport() function does NOT condition the "default collapsed"
|
||||
// behavior on narrowMql.matches alone — i.e. the unconditional path
|
||||
// also calls setExpanded(p, false) for the controls pair, OR an
|
||||
// equivalent .is-collapsed default is asserted on #liveControls.
|
||||
{
|
||||
// Heuristic: the code path that handles wide viewports must NOT
|
||||
// force the controls panel to always-expanded. Either both branches
|
||||
// collapse by default (preferred), or a dedicated initial collapse
|
||||
// is applied to liveControls regardless of MQL.
|
||||
//
|
||||
// We assert one of:
|
||||
// (a) setExpanded called with the controls pair AND false in the
|
||||
// unconditional / wide branch.
|
||||
// (b) An explicit "liveControls" / .live-controls .is-collapsed
|
||||
// initialization that runs at all viewports.
|
||||
const wideBranch = /narrowMql\.matches[\s\S]{0,2000}/.exec(liveJs);
|
||||
const elseBlockHasAlwaysExpanded =
|
||||
/else\s*\{[\s\S]{0,800}?removeAttribute\(\s*['"]hidden['"]\s*\);[\s\S]{0,200}?remove\(\s*['"]is-collapsed['"]/.test(liveJs);
|
||||
|
||||
// Acceptable: code has been updated so the controls pair defaults
|
||||
// collapsed even on desktop. We pass if either the explicit "always
|
||||
// expanded" else-branch no longer applies to liveControls, or a
|
||||
// separate desktop-default-collapse step is present.
|
||||
const desktopCollapseHook =
|
||||
/liveControls[\s\S]{0,400}?(is-collapsed|setAttribute\(\s*['"]hidden['"])/.test(liveJs) ||
|
||||
/setExpanded\(\s*pairs\[1\][\s\S]{0,80}?,\s*false\s*\)/.test(liveJs) ||
|
||||
/defaultCollapsed[\s\S]{0,80}?true/.test(liveJs);
|
||||
|
||||
assert(desktopCollapseHook,
|
||||
'.live-controls defaults to collapsed at all viewports (not just ≤768px)');
|
||||
}
|
||||
|
||||
// CSS supporting rule: the .is-collapsed → hide body rule must NOT be
|
||||
// gated to ≤768px any more. Detect a top-level (non-media-scoped) rule.
|
||||
{
|
||||
// Find first top-level occurrence of `.live-controls.is-collapsed .live-controls-body`
|
||||
// outside an @media block. Cheap test: split on @media and search the
|
||||
// pre-media chunk.
|
||||
const beforeFirstMedia = liveCss.split(/@media/)[0];
|
||||
const ruleRe = /\.live-controls\.is-collapsed\s+\.live-controls-body[\s\S]{0,200}?display\s*:\s*none/;
|
||||
// Either the rule exists outside any @media, OR the body class path
|
||||
// (body.live-fullscreen) is what does the hiding (which the D-block
|
||||
// already asserts). We accept the body-class path AND additionally
|
||||
// a non-mobile-gated .is-collapsed rule for the pin-only default.
|
||||
const collapsedOutsideMedia = ruleRe.test(beforeFirstMedia);
|
||||
assert(collapsedOutsideMedia,
|
||||
'.live-controls.is-collapsed → hides .live-controls-body at all viewports (rule lives outside @media max-width)');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 results ===');
|
||||
console.log(` ${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user