mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-29 01:38:19 +00:00
## What was broken
The nav-pin button state was not persisted across page loads. Every
refresh reset the nav to unpinned regardless of what the user had set,
forcing them to re-pin on every visit.
## What was added
- On init: reads `localStorage.getItem('live-nav-pinned')` and restores
the pinned state into `_navCleanup.pinned` before the button is created;
if pinned, the button gets the `pinned` class, `aria-pressed="true"`,
and `nav-autohide` is removed from the nav.
- On click: after toggling, writes
`localStorage.setItem('live-nav-pinned', _navCleanup.pinned)` inside a
`try/catch` (quota guard, consistent with other live.js localStorage
writes).
localStorage key: `live-nav-pinned`
Closes #1510
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3850600130
commit
878d162b71
@@ -363,6 +363,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1234-live-chrome-pass2-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-vcr-overlap-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1244-live-vcr-row-hints-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1510-live-nav-pin-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
+8
-1
@@ -2132,7 +2132,8 @@
|
||||
// Auto-hide nav with pin toggle (#62)
|
||||
const topNav = document.querySelector('.top-nav');
|
||||
if (topNav) { topNav.style.position = 'fixed'; topNav.style.width = '100%'; topNav.style.zIndex = '1100'; }
|
||||
_navCleanup = { timeout: null, fn: null, pinned: false };
|
||||
const _savedPin = localStorage.getItem('live-nav-pinned') === 'true';
|
||||
_navCleanup = { timeout: null, fn: null, pinned: _savedPin };
|
||||
// Add pin button to nav (guard against duplicate)
|
||||
if (topNav && !document.getElementById('navPinBtn')) {
|
||||
const pinBtn = document.createElement('button');
|
||||
@@ -2146,6 +2147,7 @@
|
||||
_navCleanup.pinned = !_navCleanup.pinned;
|
||||
pinBtn.classList.toggle('pinned', _navCleanup.pinned);
|
||||
pinBtn.setAttribute('aria-pressed', _navCleanup.pinned);
|
||||
try { localStorage.setItem('live-nav-pinned', _navCleanup.pinned); } catch (_) {}
|
||||
if (_navCleanup.pinned) {
|
||||
clearTimeout(_navCleanup.timeout);
|
||||
topNav.classList.remove('nav-autohide');
|
||||
@@ -2153,6 +2155,11 @@
|
||||
_navCleanup.timeout = setTimeout(() => { topNav.classList.add('nav-autohide'); }, 4000);
|
||||
}
|
||||
});
|
||||
if (_navCleanup.pinned) {
|
||||
pinBtn.classList.add('pinned');
|
||||
pinBtn.setAttribute('aria-pressed', 'true');
|
||||
topNav.classList.remove('nav-autohide');
|
||||
}
|
||||
topNav.appendChild(pinBtn);
|
||||
}
|
||||
function showNav() {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* E2E for #1510 — live-page nav pin must persist across refresh.
|
||||
*
|
||||
* Symptom: clicking #navPinBtn pinned the nav, but reloading the page reset
|
||||
* pinned=false regardless. Fix: store 'live-nav-pinned' in localStorage and
|
||||
* restore on init.
|
||||
*
|
||||
* Acceptance:
|
||||
* 1. Clicking #navPinBtn adds class 'pinned' and sets localStorage.
|
||||
* 2. After page reload the pin button still has class 'pinned' (restored from storage).
|
||||
* 3. Clicking again removes 'pinned'; reload leaves button unpinned.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
const LS_KEY = 'live-nav-pinned';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
|
||||
async function step(name, fn) {
|
||||
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
||||
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
||||
}
|
||||
function assert(cond, msg) { if (!cond) throw new Error(msg || 'assertion failed'); }
|
||||
|
||||
async function gotoLive(page) {
|
||||
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#navPinBtn', { timeout: 10000 });
|
||||
await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
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 (process.env.CHROMIUM_REQUIRE === '1') {
|
||||
console.error('test-issue-1510-live-nav-pin-e2e.js: FAIL — Chromium unavailable: ' + err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('test-issue-1510-live-nav-pin-e2e.js: SKIP (Chromium unavailable: ' + err.message.split('\n')[0] + ')');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(12000);
|
||||
|
||||
// Clear any prior state.
|
||||
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate((k) => localStorage.removeItem(k), LS_KEY);
|
||||
|
||||
// (1) Initial state: pin button exists and is unpinned.
|
||||
await step('pin button renders without pinned class on first load', async () => {
|
||||
await gotoLive(page);
|
||||
const pinned = await page.$eval('#navPinBtn', el => el.classList.contains('pinned'));
|
||||
assert(!pinned, 'pin button must not have class "pinned" before first click');
|
||||
const stored = await page.evaluate((k) => localStorage.getItem(k), LS_KEY);
|
||||
assert(stored !== 'true', 'localStorage must not be "true" before first click');
|
||||
});
|
||||
|
||||
// (2) Click pin → class added, localStorage updated.
|
||||
await step('click sets pinned class and persists to localStorage', async () => {
|
||||
await page.$eval('#navPinBtn', btn => btn.click());
|
||||
await page.evaluate(() => new Promise(r => requestAnimationFrame(r)));
|
||||
const pinned = await page.$eval('#navPinBtn', el => el.classList.contains('pinned'));
|
||||
assert(pinned, 'pin button must have class "pinned" after click');
|
||||
const stored = await page.evaluate((k) => localStorage.getItem(k), LS_KEY);
|
||||
assert(stored === 'true', `localStorage["${LS_KEY}"] must be "true", got "${stored}"`);
|
||||
});
|
||||
|
||||
// (3) Reload → pin state restored from localStorage.
|
||||
await step('pin state survives page reload', async () => {
|
||||
await gotoLive(page);
|
||||
const pinned = await page.$eval('#navPinBtn', el => el.classList.contains('pinned'));
|
||||
assert(pinned, 'pin button must have class "pinned" after reload (restored from localStorage)');
|
||||
});
|
||||
|
||||
// (4) Click again to unpin → class removed, localStorage updated.
|
||||
await step('second click removes pinned class and updates localStorage', async () => {
|
||||
await page.$eval('#navPinBtn', btn => btn.click());
|
||||
await page.evaluate(() => new Promise(r => requestAnimationFrame(r)));
|
||||
const pinned = await page.$eval('#navPinBtn', el => el.classList.contains('pinned'));
|
||||
assert(!pinned, 'pin button must not have class "pinned" after unpin click');
|
||||
const stored = await page.evaluate((k) => localStorage.getItem(k), LS_KEY);
|
||||
assert(stored === 'false', `localStorage["${LS_KEY}"] must be "false", got "${stored}"`);
|
||||
});
|
||||
|
||||
// (5) Reload after unpin → button is not pinned.
|
||||
await step('unpinned state survives page reload', async () => {
|
||||
await gotoLive(page);
|
||||
const pinned = await page.$eval('#navPinBtn', el => el.classList.contains('pinned'));
|
||||
assert(!pinned, 'pin button must not have class "pinned" after reload in unpinned state');
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
const total = passed + failed;
|
||||
console.log(`\ntest-issue-1510-live-nav-pin-e2e.js: ${failed === 0 ? 'OK' : 'FAIL'} — ${passed}/${total} passed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('test-issue-1510-live-nav-pin-e2e.js: ERROR', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user