Files
meshcore-analyzer/tests/unit/test-issue-1107-live-layout.js
T
liquidraver 84eba41592 fix(live): anchor the legend toggle to .live-page so the VCR bar stops eating its clicks (#2049)
The PACKET TYPES legend could not be dismissed on Live: clicking the palette toggle did nothing, while document.querySelector('#legendToggleBtn').click() from the console worked. That asymmetry was the diagnosis, since a synthesised click skips hit testing.

Residual half of #1833. That fix corrected the button's offset to calc(var(--vcr-bar-height) + 10px) but left the anchor: .legend-toggle-btn was position: fixed, so the offset resolved against the viewport, while the .vcr-bar it clears is absolute inside .live-page, whose height subtracts --bottom-nav-reserve (56px + safe-area at <=768). The two anchors disagreed by exactly the reserve, dropping the button into the bar's band, and .vcr-bar at z-index 1000 against the button's 500 took every real click. The window is 641-768px: below 641 both buttons are display:none, above 768 the reserve is 0, which is why neither the desktop nor the phone layout test saw it.

Both buttons are now position: absolute, resolving against the same containing block as the bar. .feed-show-btn carried the identical defect but missed the bar by 16px because --legend-toggle-stack parks it a row higher, so that change prevents a break rather than repairs one.

Three review rounds, each verified rather than argued:
- The new E2E was classified in non-unit-tests.json but had no deploy.yml line, so it would never have run (#2037). Now wired in, and it covers both buttons.
- The '.feed-show-btn is display:none at <=768' claim was wrong (it is <=640), which the author measured and corrected.
- Its first real execution failed on its own setup: getComputedStyle().getPropertyValue() on an unregistered custom property returns the unevaluated declaration ('calc(56px + 0px)'), so the non-zero check could not work. It now asserts the measured gap instead, and deliberately does not pin 56 because the gap measures 58 on CI.

Final run: 14 passed, 0 failed. The discriminating assertion is (f), each button's offset net of the bar being identical at 720 and 1440 (11px and 123px slack); under the old anchor those differ by the reserve. (g) pins the desktop layout unmoved.

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

Fixes #1833
2026-09-20 14:00:45 +02:00

219 lines
9.1 KiB
JavaScript

/**
* #1107 — Live view: PACKET TYPES legend oversized + bottom toggle buttons
* cramped.
*
* Per triage fix path (Kpa-clawbot/CoreScope#1107):
* 1. `.live-legend` panel must be content-driven (`height: max-content`)
* with a `max-width` cap so it doesn't dominate the map.
* 2. The activate/hide toggle button group at the bottom of the map
* (`.legend-toggle-btn`, `.feed-show-btn`) must be pinned via
* `position: absolute; bottom: 1rem; right: 1rem` so they dock as one
* tidy bottom-right group instead of being scattered/cramped.
* (#1833 r2 changed the anchor from `fixed` to `absolute`: the group
* must resolve its offsets against .live-page, the same box .vcr-bar
* is in, or it drifts into the bar by --bottom-nav-reserve at <=768
* and stops taking clicks. #1107's intent — one docked bottom-right
* cluster — is unchanged; .live-page spans the whole map area.)
* 3. Theming uses existing CSS variables only — no new hex colors.
*
* Source-invariant assertions on public/live.css, same approach as
* test-issue-1532-live-fullscreen.js (runs in the JS unit test gate).
*/
'use strict';
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
const fs = require('fs');
const path = require('path');
let passed = 0, failed = 0;
function assert(cond, msg) {
if (cond) { passed++; console.log(' \u2713 ' + msg); }
else { failed++; console.error(' \u2717 ' + msg); }
}
const liveCss = fs.readFileSync(path.join(REPO_ROOT, 'public', 'live.css'), 'utf8');
// Extract the .live-legend base block (first occurrence, not the media
// queries, not the .matrix-theme override, not .live-legend.hidden).
function ruleBlock(css, selector) {
// Match the LARGEST rule block for the given selector (multiple may
// exist: a base rule + media-query overrides). We pick the largest body
// because it is the canonical declaration with full property set.
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(
'(?:^|[\\n,}])\\s*' + escaped + '\\s*\\{([^}]*)\\}',
'gm'
);
let m, best = null;
while ((m = re.exec(css)) !== null) {
if (best == null || m[1].length > best.length) best = m[1];
}
return best;
}
function allRuleBlocks(css, selector) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(
'(?:^|[\\n,}])\\s*' + escaped + '\\s*\\{([^}]*)\\}',
'gm'
);
const out = [];
let m;
while ((m = re.exec(css)) !== null) out.push(m[1]);
return out;
}
function anyBlockMatches(css, selector, pattern) {
return allRuleBlocks(css, selector).some(b => pattern.test(b));
}
// ─────────────────────────────────────────────────────────────────────
console.log('\n=== #1107 A: .live-legend is content-driven + width-capped ===');
const legendBase = ruleBlock(liveCss, '.live-legend');
assert(legendBase != null, '.live-legend base rule block found in live.css');
if (legendBase) {
assert(
/height\s*:\s*max-content/.test(legendBase),
'.live-legend declares `height: max-content` (content-driven, not oversized)'
);
assert(
/max-width\s*:/.test(legendBase),
'.live-legend declares a `max-width` cap (does not dominate map)'
);
}
// ─────────────────────────────────────────────────────────────────────
console.log('\n=== #1107 B: bottom toggle button group pinned bottom-right ===');
const legendBtn = ruleBlock(liveCss, '.legend-toggle-btn');
assert(legendBtn != null, '.legend-toggle-btn rule block found');
if (legendBtn) {
assert(
/position\s*:\s*absolute/.test(legendBtn),
'.legend-toggle-btn uses position: absolute (docked to .live-page, #1833 r2)'
);
assert(
/bottom\s*:\s*(1rem|max\(1rem|calc\(\s*var\(--vcr-bar-height)/.test(legendBtn),
'.legend-toggle-btn pinned at bottom: 1rem / max(1rem,...) / calc(var(--vcr-bar-height,...)+...) (VCR-bar-aware, #1833)'
);
assert(
/right\s*:\s*1rem/.test(legendBtn),
'.legend-toggle-btn pinned at right: 1rem'
);
}
const feedShowBtn = ruleBlock(liveCss, '.feed-show-btn');
assert(feedShowBtn != null, '.feed-show-btn rule block found');
if (feedShowBtn) {
assert(
/position\s*:\s*absolute/.test(feedShowBtn),
'.feed-show-btn uses position: absolute (docked to .live-page, #1833 r2)'
);
assert(
/bottom\s*:\s*(1rem|max\(1rem)/.test(feedShowBtn),
'.feed-show-btn pinned at bottom: 1rem or max(1rem,...) (grouped with legend toggle)'
);
assert(
/right\s*:\s*1rem/.test(feedShowBtn),
'.feed-show-btn pinned at right: 1rem (grouped with legend toggle)'
);
}
// ─────────────────────────────────────────────────────────────────────
console.log('\n=== #1107 B2: cascade-final bottom invariant (no !important conflict) ===');
// Scan ALL .feed-show-btn rule blocks. If any declares `bottom` with
// `!important`, there must NOT be a separate block setting a different
// `bottom` without `!important` — otherwise the cascade silently wins
// and the buttons don't actually dock together.
(function cascadeFinalCheck() {
const feedBlocks = allRuleBlocks(liveCss, '.feed-show-btn');
const legendBlocks = allRuleBlocks(liveCss, '.legend-toggle-btn');
// Collect all bottom declarations from .feed-show-btn blocks
const feedBottoms = [];
for (const block of feedBlocks) {
const m = block.match(/bottom\s*:\s*([^;]+)/);
if (m) {
feedBottoms.push({
value: m[1].trim(),
important: /!important/.test(m[1])
});
}
}
// If any .feed-show-btn block uses !important on bottom, then EITHER:
// (a) it is the ONLY bottom declaration (canonical), OR
// (b) all bottom declarations agree (no cascade conflict)
const importantBottoms = feedBottoms.filter(b => b.important);
const nonImportantBottoms = feedBottoms.filter(b => !b.important);
if (importantBottoms.length > 0 && nonImportantBottoms.length > 0) {
// Cascade conflict: an !important override coexists with a non-!important
// declaration. The non-!important block (the PR's docking fix) will LOSE.
assert(false,
'.feed-show-btn has NO cascade conflict: found ' +
importantBottoms.length + ' !important bottom declaration(s) AND ' +
nonImportantBottoms.length + ' non-!important bottom declaration(s) — ' +
'the !important wins at runtime, breaking docking');
} else {
assert(true, '.feed-show-btn bottom declarations have no !important cascade conflict');
}
// Both buttons must resolve to the same `right` value across all blocks
const feedRights = [];
for (const block of feedBlocks) {
const m = block.match(/right\s*:\s*([^;]+)/);
if (m) feedRights.push(m[1].trim().replace(/\s*!important/, ''));
}
const legendRights = [];
for (const block of legendBlocks) {
const m = block.match(/right\s*:\s*([^;]+)/);
if (m) legendRights.push(m[1].trim().replace(/\s*!important/, ''));
}
// All right values should be identical across both selectors
const allRights = [...new Set([...feedRights, ...legendRights])];
assert(
allRights.length === 1,
'.feed-show-btn and .legend-toggle-btn share identical `right` value' +
(allRights.length !== 1 ? ' — found: ' + allRights.join(', ') : ' (both ' + allRights[0] + ')')
);
})();
// ─────────────────────────────────────────────────────────────────────
console.log('\n=== #1107 C: no new hex colors introduced for #1107 changes ===');
// Lightweight invariant: the .live-legend and toggle-button rules use
// CSS variables (no raw #hex in their base bodies). Existing rules in
// this repo already follow that convention; this gate prevents the fix
// from regressing it.
function noHexInBlock(block, name) {
if (!block) return;
// Strip /* ... */ comments first — issue refs like "#1206" in comments
// are not hex colors. Also restrict to canonical 3/6/8-digit hex (not 4/5/7).
const stripped = block.replace(/\/\*[\s\S]*?\*\//g, '');
const hex = stripped.match(/#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/);
assert(
!hex,
`${name} contains no raw hex color (uses CSS variables only)` +
(hex ? ` — found ${hex[0]}` : '')
);
}
noHexInBlock(legendBase, '.live-legend base');
noHexInBlock(legendBtn, '.legend-toggle-btn');
noHexInBlock(feedShowBtn, '.feed-show-btn');
// ─────────────────────────────────────────────────────────────────────
console.log(`\nResults: ${passed} passed, ${failed} failed`);
if (failed > 0) {
console.error('FAIL — #1107 layout invariants not met');
process.exit(1);
}
console.log('PASS — #1107 layout invariants enforced');