mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-28 10:01:42 +00:00
- Eliminated extra space to the right of the map filters. - Made the map filters and mesh live a single line with a divider - Resized the input and dropdowns in the map filters so they meet WCAG 2.5.5 by being at least 44px high, but appearing 30px high - Turned the filters cog and the fullscreen button into native leaflet icons that are large enough to meet WCAV 2.5.5 compliance - Increased the size of the zoom buttons to meet WCAG 2.5.5 compliance on both the live and map pages - If the top nav bar is pinned, it won't disappear during fullscreen but if it isn't pinned, it will disappear with everything else. - The cog and full screen button change color to show they're active Final Outcome in 4k <img width="2878" height="1406" alt="image" src="https://github.com/user-attachments/assets/28db46a2-f1bb-4d9c-9d77-30c444b4ef3d" /> Final Outcome in 1080p <img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/120be8ec-0279-40fc-925a-243e9c0bcc1c" />
This commit is contained in:
+86
-34
@@ -431,11 +431,14 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
flex: 0 0 100%; /* break onto its own row inside flex-wrap header */
|
||||
flex: auto; /* Let it flow on the same row if there is room */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
border-left: 1px solid var(--border); /* Vertical line between metrics and filters */
|
||||
padding-left: 12px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.live-controls-body {
|
||||
display: flex;
|
||||
@@ -447,9 +450,25 @@
|
||||
|
||||
/* Region/Area filters inline in live header controls body */
|
||||
.live-controls-body .live-region-filter-container,
|
||||
.live-controls-body .live-area-filter-container { display: inline-flex; align-items: center; }
|
||||
.live-controls-body .live-area-filter-container,
|
||||
.live-controls-body .live-show-all-region-nodes { display: inline-flex; align-items: center; font-size: 11px; }
|
||||
.live-controls-body .live-region-filter-container .region-dropdown-trigger,
|
||||
.live-controls-body .live-area-filter-container .region-dropdown-trigger { font-size: inherit; padding: 2px 6px; }
|
||||
.live-controls-body .live-area-filter-container .region-dropdown-trigger {
|
||||
font-size: inherit;
|
||||
padding: 2px 8px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
}
|
||||
.live-controls-body .live-region-filter-container .region-dropdown-trigger::after,
|
||||
.live-controls-body .live-area-filter-container .region-dropdown-trigger::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: max(100%, 44px);
|
||||
height: 44px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* #1108 — "Show all nodes" sibling of the region dropdown. Reuses the
|
||||
.live-controls-body label rhythm so it lines up with the other inline toggles. */
|
||||
@@ -457,9 +476,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.live-controls-body .live-show-all-region-nodes input { margin: 0; }
|
||||
|
||||
/* ---- #1532 Live: default-collapsed controls + fullscreen mode ---- */
|
||||
|
||||
@@ -493,39 +512,30 @@ body.live-fullscreen .feed-show-btn { display: none !important; }
|
||||
top-right. The header background/border drop so the stats pills
|
||||
float over the map. */
|
||||
body.live-fullscreen .live-header {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
z-index: 700;
|
||||
pointer-events: none;
|
||||
}
|
||||
body.live-fullscreen .live-stats-row {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
z-index: 700;
|
||||
background: color-mix(in srgb, var(--surface-1) 84%, transparent);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
body.live-fullscreen .live-header-body {
|
||||
display: none !important;
|
||||
}
|
||||
body.live-fullscreen .live-stats-row,
|
||||
body.live-fullscreen .live-header-critical {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Keep the fullscreen exit affordance reachable: a small chip in the
|
||||
top-left so users can leave the mode without remembering the F key. */
|
||||
body.live-fullscreen #liveFullscreenToggle {
|
||||
display: inline-flex !important;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 700;
|
||||
/* Hide top nav in fullscreen UNLESS it was explicitly pinned */
|
||||
body.live-fullscreen:not(.nav-pinned) .top-nav {
|
||||
display: none !important;
|
||||
}
|
||||
/* Shift map controls and live header up when top nav is hidden */
|
||||
body.live-fullscreen:not(.nav-pinned) .leaflet-top.leaflet-right,
|
||||
body.live-fullscreen:not(.nav-pinned) .live-header {
|
||||
top: 12px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ---- Medium breakpoint (#279) + collapse toggles (#1178, #1179) ---- */
|
||||
@media (max-width: 768px) {
|
||||
@@ -535,6 +545,9 @@ body.live-fullscreen #liveFullscreenToggle {
|
||||
.live-header { gap: 6px; padding: 4px 8px; max-height: none; min-height: 48px; }
|
||||
.live-stat-pill { font-size: 11px; padding: 1px 7px; }
|
||||
.live-toggles { font-size: 10px; gap: 6px; }
|
||||
.live-controls-body .live-region-filter-container,
|
||||
.live-controls-body .live-area-filter-container,
|
||||
.live-controls-body .live-show-all-region-nodes { font-size: 10px; }
|
||||
|
||||
/* Show toggle buttons */
|
||||
.live-header-toggle,
|
||||
@@ -621,6 +634,11 @@ body.live-fullscreen #liveFullscreenToggle {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-width: 0;
|
||||
}
|
||||
.live-controls {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* #1234 — hide top app navbar on /live route at ≤640px.
|
||||
* Uses :has() (Chromium 105+, Safari 15.4+, Firefox 121+) to scope the
|
||||
@@ -709,6 +727,38 @@ body.live-fullscreen #liveFullscreenToggle {
|
||||
.live-page .leaflet-top.leaflet-right { top: 56px; }
|
||||
}
|
||||
|
||||
/* Custom Live Toggles (Settings & Fullscreen) */
|
||||
.live-leaflet-toggle {
|
||||
/* No absolute positioning needed; they stack normally in leaflet-right */
|
||||
}
|
||||
.live-leaflet-toggle a {
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
font-size: 20px !important;
|
||||
background-color: var(--surface-1) !important;
|
||||
color: var(--text) !important;
|
||||
border-bottom: none !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.live-leaflet-toggle a:hover {
|
||||
background-color: var(--surface-2) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
/* Active states for cog and fullscreen toggles */
|
||||
body.live-fullscreen #liveFullscreenToggle,
|
||||
#liveControlsToggle[aria-expanded="true"],
|
||||
body.live-fullscreen #liveFullscreenToggle:hover,
|
||||
#liveControlsToggle[aria-expanded="true"]:hover {
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Feed item hover */
|
||||
.live-feed-item:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
|
||||
|
||||
@@ -888,7 +938,10 @@ body.live-fullscreen #liveFullscreenToggle {
|
||||
|
||||
/* #1110 Live page node filter — match toolbar control sizing & theme */
|
||||
.live-node-filter-wrap { position: relative; display: inline-flex; align-items: center; }
|
||||
.live-node-filter-input {
|
||||
input.live-node-filter-input {
|
||||
box-sizing: border-box;
|
||||
height: 30px;
|
||||
min-height: 30px; /* Override global 48px min-height on text inputs */
|
||||
background: color-mix(in srgb, var(--text) 6%, transparent);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
@@ -896,11 +949,10 @@ body.live-fullscreen #liveFullscreenToggle {
|
||||
padding: 3px 8px;
|
||||
font-size: inherit;
|
||||
line-height: 1.3;
|
||||
height: auto;
|
||||
min-width: 140px;
|
||||
outline: none;
|
||||
}
|
||||
.live-node-filter-input:focus {
|
||||
input.live-node-filter-input:focus {
|
||||
border-color: color-mix(in srgb, var(--text) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--text) 10%, transparent);
|
||||
}
|
||||
|
||||
+90
-42
@@ -1054,6 +1054,11 @@
|
||||
<div class="live-page">
|
||||
<div id="liveMap" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1"></div>
|
||||
<div class="live-overlay live-header" id="liveHeader">
|
||||
<div class="live-header-body" data-live-header-body id="liveHeaderBody">
|
||||
<div class="live-title">
|
||||
MESH LIVE
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-header-critical" data-live-header-critical>
|
||||
<span class="live-beacon" aria-label="WebSocket connection beacon"></span>
|
||||
<div class="live-stat-pill live-stat-pill--critical"><span id="livePktCount">0</span> pkts</div>
|
||||
@@ -1070,11 +1075,7 @@
|
||||
<button class="live-header-toggle" data-live-header-toggle id="liveHeaderToggle"
|
||||
aria-expanded="false" aria-controls="liveHeaderBody"
|
||||
aria-label="Show live stats">📊</button>
|
||||
<div class="live-header-body" data-live-header-body id="liveHeaderBody">
|
||||
<div class="live-title">
|
||||
MESH LIVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #1205: settings toggles are children of the MESH LIVE panel
|
||||
(#liveHeader), not a free-floating .live-overlay. PR #1180
|
||||
detached them; this restores the pre-regression structure. -->
|
||||
@@ -1099,12 +1100,16 @@
|
||||
<span id="favDesc" class="sr-only">Show only favorited and claimed nodes</span>
|
||||
<label id="liveGeoFilterLabel" style="display:none"><input type="checkbox" id="liveGeoFilterToggle"> Mesh live area</label>
|
||||
</div>
|
||||
<div class="live-node-filter-wrap" style="position:relative">
|
||||
<input type="text" id="liveNodeFilterInput" placeholder="Filter by node…" autocomplete="off" class="live-node-filter-input" role="combobox" aria-expanded="false" aria-owns="liveNodeFilterDropdown" aria-autocomplete="list" aria-activedescendant="">
|
||||
<div id="liveNodeFilterDropdown" class="live-node-filter-dropdown hidden" role="listbox"></div>
|
||||
<button id="liveNodeFilterClear" class="vcr-btn" title="Clear node filter" style="display:none">×</button>
|
||||
<div class="live-toggles">
|
||||
<div class="live-node-filter-wrap" style="position:relative">
|
||||
<label class="live-node-filter-hitarea" style="display:inline-flex; align-items:center; min-height:44px; cursor:text;">
|
||||
<input type="text" id="liveNodeFilterInput" placeholder="Filter by node…" autocomplete="off" class="live-node-filter-input" role="combobox" aria-expanded="false" aria-owns="liveNodeFilterDropdown" aria-autocomplete="list" aria-activedescendant="">
|
||||
</label>
|
||||
<div id="liveNodeFilterDropdown" class="live-node-filter-dropdown hidden" role="listbox"></div>
|
||||
<button id="liveNodeFilterClear" class="vcr-btn" title="Clear node filter" style="display:none">×</button>
|
||||
</div>
|
||||
<div id="liveNodeFilterCount" class="live-filter-count hidden"></div>
|
||||
</div>
|
||||
<div id="liveNodeFilterCount" class="live-filter-count hidden"></div>
|
||||
<div id="liveRegionFilter" class="region-filter-container live-region-filter-container" aria-label="Filter live packets by IATA region"></div>
|
||||
<div id="liveAreaFilter" class="live-area-filter-container"></div>
|
||||
<div class="audio-controls hidden" id="audioControls">
|
||||
@@ -1113,13 +1118,6 @@
|
||||
<label class="audio-slider-label">Vol <input type="range" id="audioVolSlider" min="0" max="100" value="30" class="audio-slider"><span id="audioVolVal">30</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="live-controls-toggle" data-live-controls-toggle id="liveControlsToggle"
|
||||
aria-expanded="false" aria-controls="liveControlsBody"
|
||||
aria-label="Show live controls">⚙</button>
|
||||
<button class="live-controls-toggle live-fullscreen-toggle" id="liveFullscreenToggle"
|
||||
aria-pressed="false"
|
||||
aria-label="Toggle fullscreen (F) — hide chrome, keep stats"
|
||||
title="Fullscreen (F)">⛶</button>
|
||||
</div>
|
||||
</div><!-- /#liveHeader -->
|
||||
<div class="live-overlay live-feed" id="liveFeed">
|
||||
@@ -1409,7 +1407,49 @@
|
||||
if (typeof window.MC_createLayerControl === 'function') {
|
||||
window.MC_createLayerControl(map, liveAutoLayerGroup, 'topright');
|
||||
}
|
||||
|
||||
|
||||
// Add custom Leaflet Control for Fullscreen
|
||||
const LiveFullscreenControl = L.Control.extend({
|
||||
options: { position: 'topright' },
|
||||
onAdd: function() {
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control live-leaflet-toggle');
|
||||
const btn = L.DomUtil.create('a', '', container);
|
||||
btn.href = 'javascript:void(0)';
|
||||
btn.innerHTML = '⛶';
|
||||
btn.id = 'liveFullscreenToggle';
|
||||
btn.title = 'Fullscreen (F)';
|
||||
btn.setAttribute('aria-label', 'Toggle fullscreen');
|
||||
btn.setAttribute('role', 'button');
|
||||
btn.setAttribute('aria-pressed', 'false');
|
||||
L.DomEvent.disableClickPropagation(btn);
|
||||
return container;
|
||||
}
|
||||
});
|
||||
map.addControl(new LiveFullscreenControl());
|
||||
|
||||
// Add custom Leaflet Control for Settings
|
||||
const LiveSettingsControl = L.Control.extend({
|
||||
options: { position: 'topright' },
|
||||
onAdd: function() {
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control live-leaflet-toggle');
|
||||
const btn = L.DomUtil.create('a', 'live-controls-toggle', container);
|
||||
btn.href = 'javascript:void(0)';
|
||||
btn.innerHTML = '⚙';
|
||||
btn.id = 'liveControlsToggle';
|
||||
btn.setAttribute('data-live-controls-toggle', '');
|
||||
btn.title = 'Settings';
|
||||
btn.setAttribute('aria-label', 'Show live controls');
|
||||
btn.setAttribute('role', 'button');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.setAttribute('aria-controls', 'liveControlsBody');
|
||||
L.DomEvent.disableClickPropagation(btn);
|
||||
return container;
|
||||
}
|
||||
});
|
||||
map.addControl(new LiveSettingsControl());
|
||||
|
||||
|
||||
|
||||
// Swap tiles when theme changes
|
||||
const _themeObs = new MutationObserver(function () {
|
||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
@@ -1808,6 +1848,10 @@
|
||||
});
|
||||
voiceSelect.value = MeshAudio.getVoiceName() || voices[0] || '';
|
||||
voiceSelect.addEventListener('change', (e) => MeshAudio.setVoice(e.target.value));
|
||||
|
||||
if (voices.length <= 1) {
|
||||
voiceSelect.parentElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
audioToggle.addEventListener('change', (e) => {
|
||||
@@ -1893,32 +1937,33 @@
|
||||
function applyForViewport() {
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var p = pairs[i];
|
||||
// #1532 — `liveControls` defaults collapsed at ALL viewports
|
||||
// (previously narrow-only). Operators reveal the toggle row
|
||||
// via the ⚙ pin, parity with map-controls accordion.
|
||||
var defaultCollapsed = (p.rootId === 'liveControls') ? true : false;
|
||||
// Respect the user's prior choice across reloads.
|
||||
var root = document.getElementById(p.rootId);
|
||||
if (!root) continue;
|
||||
|
||||
if (p.rootId === 'liveControls') {
|
||||
try {
|
||||
var pref = localStorage.getItem('live-controls-expanded');
|
||||
if (pref === 'true') defaultCollapsed = false;
|
||||
if (pref === 'false') defaultCollapsed = true;
|
||||
} catch (_) { /* private browsing */ }
|
||||
}
|
||||
if (narrowMql.matches || defaultCollapsed) {
|
||||
// Default collapsed; preserve existing expansion if user
|
||||
// already opened it this mount.
|
||||
var root = document.getElementById(p.rootId);
|
||||
var alreadyExpanded = root && root.classList.contains('is-expanded');
|
||||
if (!alreadyExpanded) setExpanded(p, false);
|
||||
// #1532 - liveControls is an accordion on all viewports,
|
||||
// persisting state across reloads via localStorage.
|
||||
if (!root.classList.contains('is-expanded') && !root.classList.contains('is-collapsed')) {
|
||||
var startExpanded = false;
|
||||
try {
|
||||
if (localStorage.getItem('live-controls-expanded') === 'true') {
|
||||
startExpanded = true;
|
||||
}
|
||||
} catch (_) { /* private browsing */ }
|
||||
setExpanded(p, startExpanded);
|
||||
}
|
||||
} else {
|
||||
// Always expanded; no hidden attr; no collapse class
|
||||
var root = document.getElementById(p.rootId);
|
||||
var body = document.getElementById(p.bodyId);
|
||||
var tog = document.getElementById(p.togId);
|
||||
if (body) body.removeAttribute('hidden');
|
||||
if (root) { root.classList.remove('is-collapsed'); root.classList.remove('is-expanded'); }
|
||||
if (tog) { tog.setAttribute('aria-expanded', 'true'); }
|
||||
// liveHeader is collapsible on narrow screens, permanently open on wide
|
||||
if (narrowMql.matches) {
|
||||
if (!root.classList.contains('is-expanded')) setExpanded(p, false);
|
||||
} else {
|
||||
var body = document.getElementById(p.bodyId);
|
||||
var tog = document.getElementById(p.togId);
|
||||
if (body) body.removeAttribute('hidden');
|
||||
root.classList.remove('is-collapsed');
|
||||
root.classList.remove('is-expanded');
|
||||
if (tog) tog.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2331,6 +2376,7 @@
|
||||
_navCleanup.pinned = !_navCleanup.pinned;
|
||||
pinBtn.classList.toggle('pinned', _navCleanup.pinned);
|
||||
pinBtn.setAttribute('aria-pressed', _navCleanup.pinned);
|
||||
document.body.classList.toggle('nav-pinned', _navCleanup.pinned);
|
||||
try { localStorage.setItem('live-nav-pinned', _navCleanup.pinned); } catch (_) {}
|
||||
if (_navCleanup.pinned) {
|
||||
clearTimeout(_navCleanup.timeout);
|
||||
@@ -2342,6 +2388,7 @@
|
||||
if (_navCleanup.pinned) {
|
||||
pinBtn.classList.add('pinned');
|
||||
pinBtn.setAttribute('aria-pressed', 'true');
|
||||
document.body.classList.add('nav-pinned');
|
||||
topNav.classList.remove('nav-autohide');
|
||||
}
|
||||
const navRight = topNav.querySelector('.nav-right');
|
||||
@@ -4298,6 +4345,7 @@
|
||||
if (topNav) { topNav.classList.remove('nav-autohide'); topNav.style.position = ''; topNav.style.width = ''; topNav.style.zIndex = ''; }
|
||||
const existingPin = document.getElementById('navPinBtn');
|
||||
if (existingPin) existingPin.remove();
|
||||
if (document.body) document.body.classList.remove('nav-pinned');
|
||||
if (_navCleanup) {
|
||||
clearTimeout(_navCleanup.timeout);
|
||||
const livePage = document.querySelector('.live-page');
|
||||
|
||||
@@ -2308,6 +2308,10 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
|
||||
backdrop-filter: blur(12px);
|
||||
color: var(--text) !important;
|
||||
border-color: var(--border) !important;
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: rgba(59, 130, 246, 0.2) !important;
|
||||
|
||||
@@ -3301,6 +3301,97 @@ async function run() {
|
||||
}
|
||||
});
|
||||
|
||||
// === Live page Fullscreen tests (#1532) ===
|
||||
await test('Live page: Fullscreen hides unpinned nav and live-header', async () => {
|
||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Ensure we are not pinned
|
||||
await page.evaluate(() => localStorage.setItem('live-nav-pinned', 'false'));
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const isFullscreen = await page.evaluate(() => document.body.classList.contains('live-fullscreen'));
|
||||
if (isFullscreen) {
|
||||
await page.click('#liveFullscreenToggle');
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Enter fullscreen
|
||||
await page.click('#liveFullscreenToggle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify fullscreen class is on body
|
||||
assert(await page.evaluate(() => document.body.classList.contains('live-fullscreen')), 'Body should have live-fullscreen class');
|
||||
|
||||
// Verify top nav is hidden
|
||||
const navHidden = await page.evaluate(() => {
|
||||
const nav = document.querySelector('.top-nav');
|
||||
return window.getComputedStyle(nav).display === 'none';
|
||||
});
|
||||
assert(navHidden, 'Top nav should be hidden in fullscreen when unpinned');
|
||||
|
||||
// Verify live header body is hidden
|
||||
const headerBodyHidden = await page.evaluate(() => {
|
||||
const headerBody = document.querySelector('.live-header-body');
|
||||
return !headerBody || window.getComputedStyle(headerBody).display === 'none';
|
||||
});
|
||||
assert(headerBodyHidden, 'Live header body should be hidden in fullscreen');
|
||||
|
||||
// Verify stats row is visible
|
||||
const statsVisible = await page.evaluate(() => {
|
||||
const stats = document.querySelector('.live-stats-row');
|
||||
return stats && window.getComputedStyle(stats).display !== 'none';
|
||||
});
|
||||
assert(statsVisible, 'Live stats row should be visible in fullscreen');
|
||||
|
||||
// Exit fullscreen
|
||||
await page.click('#liveFullscreenToggle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const navVisible = await page.evaluate(() => {
|
||||
const nav = document.querySelector('.top-nav');
|
||||
return window.getComputedStyle(nav).display !== 'none';
|
||||
});
|
||||
assert(navVisible, 'Top nav should be visible again after exiting fullscreen');
|
||||
});
|
||||
|
||||
// === Live page Controls Cog tests (#1532) ===
|
||||
await test('Live page: Controls cog persistence across reloads', async () => {
|
||||
// Clear state first
|
||||
await page.evaluate(() => localStorage.removeItem('live-controls-expanded'));
|
||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Expand the cog menu
|
||||
const cog = await page.$('#liveControlsToggle');
|
||||
if (cog) {
|
||||
let isExpanded = await page.$eval('#liveControls', el => el.classList.contains('is-expanded'));
|
||||
if (!isExpanded) {
|
||||
await cog.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Verify it's expanded
|
||||
isExpanded = await page.$eval('#liveControls', el => el.classList.contains('is-expanded'));
|
||||
assert(isExpanded, 'Controls should have is-expanded class after clicking cog');
|
||||
|
||||
// Reload page and verify persistence
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Should STILL be expanded
|
||||
isExpanded = await page.$eval('#liveControls', el => el.classList.contains('is-expanded'));
|
||||
assert(isExpanded, 'Controls should persist is-expanded class across reload');
|
||||
|
||||
// Click it again, it should immediately close
|
||||
await page.click('#liveControlsToggle');
|
||||
await page.waitForTimeout(500);
|
||||
isExpanded = await page.$eval('#liveControls', el => el.classList.contains('is-expanded'));
|
||||
assert(!isExpanded, 'Controls should collapse on the first click after a reload');
|
||||
}
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Summary
|
||||
|
||||
@@ -35,25 +35,17 @@ const liveCss = fs.readFileSync(path.join(__dirname, 'public', 'live.css'), 'utf
|
||||
console.log('\n=== #1532 A: #liveFullscreenToggle button declared ===');
|
||||
|
||||
assert(
|
||||
/id=["']liveFullscreenToggle["']/.test(liveJs),
|
||||
'#liveFullscreenToggle id appears in live.js init() HTML template'
|
||||
/id\s*=\s*['"]liveFullscreenToggle['"]/.test(liveJs),
|
||||
'#liveFullscreenToggle id appears in live.js init() HTML template or JS'
|
||||
);
|
||||
|
||||
assert(
|
||||
/liveFullscreenToggle[\s\S]{0,400}aria-label/.test(liveJs),
|
||||
/liveFullscreenToggle[\s\S]{0,400}aria-label/.test(liveJs) || /setAttribute\('aria-label',\s*'Toggle fullscreen'\)/.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'
|
||||
);
|
||||
}
|
||||
// Co-location check removed because Leaflet controls are added differently.
|
||||
console.log(' ✓ #liveFullscreenToggle exists in Leaflet controls');
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 B: fullscreen toggle wires body.live-fullscreen ===');
|
||||
@@ -116,18 +108,9 @@ assert(cssHides('.vcr-controls'),
|
||||
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'
|
||||
);
|
||||
// The entire live-header is now hidden in fullscreen per updated user request.
|
||||
assert(cssHides('.live-header'),
|
||||
'body.live-fullscreen hides .live-header (display:none)');
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
console.log('\n=== #1532 E: .live-controls collapsed by default on desktop ===');
|
||||
|
||||
@@ -82,6 +82,15 @@ function makeSandbox() {
|
||||
MutationObserver: function() { this.observe = () => {}; this.disconnect = () => {}; },
|
||||
WebSocket: function() { this.close = () => {}; },
|
||||
IATA_COORDS_GEO: {},
|
||||
AreaFilter: {
|
||||
init: () => {},
|
||||
onChange: () => {},
|
||||
areaQueryString: () => ''
|
||||
},
|
||||
RegionFilter: {
|
||||
nodesRegionQueryString: () => '',
|
||||
packetsRegionQueryString: () => ''
|
||||
},
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
return ctx;
|
||||
|
||||
Reference in New Issue
Block a user