mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 06:25:06 +00:00
## Summary Fixes the chained async init race identified in RCA #3 of #955. `navigate()` (which dispatches page handlers and fetches data) was gated behind `/api/config/theme` resolving via `.finally()`. Tests use `waitUntil: 'domcontentloaded'` which returns BEFORE theme fetch resolves, creating a race condition where 3+ serial network requests must complete before any DOM rows appear. ## Changes ### Decouple navigate() from theme fetch (public/app.js) - Move `navigate()` call out of the theme fetch `.finally()` block - Call it immediately on DOMContentLoaded — theme is purely cosmetic and applies in parallel ### Add data-loaded sync attributes (public/nodes.js, map.js, packets.js) - Set `data-loaded="true"` on the container element after each page's data fetch resolves and DOM renders - Nodes: set on `#nodesLeft` after `loadNodes()` renders rows - Map: set on `#leaflet-map` after `renderMarkers()` completes - Packets: set on `#pktLeft` after `loadPackets()` renders rows ### Update E2E tests (test-e2e-playwright.js) - Add `await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 })` before row/marker assertions - Increase map marker timeout from 3s to 8s as additional safety margin - Tests now synchronize on data readiness rather than racing DOM appearance ## Verification - Spun up local server on port 13586 with e2e-fixture.db - Confirmed navigate() is called immediately (not gated on theme) - Confirmed data-loaded attributes are present in served JS - API returns data correctly (2 nodes from fixture) Closes #955 (RCA #3) Co-authored-by: you <you@example.com>
This commit is contained in:
+4
-3
@@ -965,10 +965,11 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}).catch(() => {
|
||||
window.SITE_CONFIG = { timestamps: { defaultMode: 'ago', timezone: 'local', formatPreset: 'iso', customFormat: '', allowCustomFormat: false } };
|
||||
if (window._customizerV2) window._customizerV2.init(window.SITE_CONFIG);
|
||||
}).finally(() => {
|
||||
if (!location.hash || location.hash === '#/') location.hash = '#/home';
|
||||
else navigate();
|
||||
});
|
||||
|
||||
// Navigate immediately — don't gate data-fetching pages on cosmetic theme fetch
|
||||
if (!location.hash || location.hash === '#/') location.hash = '#/home';
|
||||
else navigate();
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -549,6 +549,10 @@
|
||||
|
||||
renderMarkers();
|
||||
|
||||
// Signal that map data is loaded and markers rendered (used by E2E tests)
|
||||
var mapContainer = document.getElementById('leaflet-map');
|
||||
if (mapContainer) mapContainer.setAttribute('data-loaded', 'true');
|
||||
|
||||
// Restore heatmap if previously enabled
|
||||
if (localStorage.getItem('meshcore-map-heatmap') === 'true') {
|
||||
toggleHeatmap(true);
|
||||
|
||||
@@ -951,6 +951,9 @@
|
||||
} else {
|
||||
renderLeft();
|
||||
}
|
||||
// Signal that node data is loaded and rendered (used by E2E tests)
|
||||
var nodesContainer = document.getElementById('nodesLeft') || document.getElementById('nodesBody');
|
||||
if (nodesContainer) nodesContainer.setAttribute('data-loaded', 'true');
|
||||
} catch (e) {
|
||||
console.error('Failed to load nodes:', e);
|
||||
const tbody = document.getElementById('nodesBody');
|
||||
|
||||
@@ -744,6 +744,9 @@
|
||||
|
||||
sortPacketsArray();
|
||||
renderLeft();
|
||||
// Signal that packet data is loaded and rendered (used by E2E tests)
|
||||
var pktContainer = document.getElementById('pktLeft') || document.getElementById('pktBody');
|
||||
if (pktContainer) pktContainer.setAttribute('data-loaded', 'true');
|
||||
} catch (e) {
|
||||
console.error('Failed to load packets:', e);
|
||||
const tbody = document.getElementById('pktBody');
|
||||
|
||||
@@ -211,6 +211,7 @@ async function run() {
|
||||
// Test 2: Nodes page loads with data
|
||||
await test('Nodes page loads with data', async () => {
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
|
||||
await page.waitForSelector('table tbody tr');
|
||||
const headers = await page.$$eval('th', els => els.map(e => e.textContent.trim()));
|
||||
for (const col of ['Name', 'Public Key', 'Role']) {
|
||||
@@ -236,6 +237,7 @@ async function run() {
|
||||
// Test: Node side panel Details link navigates to full detail page (#778)
|
||||
await test('Node side panel Details link navigates', async () => {
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
|
||||
await page.waitForSelector('table tbody tr');
|
||||
await page.click('table tbody tr');
|
||||
await page.waitForSelector('.node-detail');
|
||||
@@ -257,6 +259,7 @@ async function run() {
|
||||
// Test: Nodes page has WebSocket auto-update listener (#131)
|
||||
await test('Nodes page has WebSocket auto-update', async () => {
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
|
||||
await page.waitForSelector('table tbody tr');
|
||||
// The live dot in navbar indicates WS connection status
|
||||
const liveDot = await page.$('#liveDot');
|
||||
@@ -282,11 +285,12 @@ async function run() {
|
||||
// Test 3: Map page loads with markers
|
||||
await test('Map page loads with markers', async () => {
|
||||
await page.goto(`${BASE}/#/map`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
|
||||
await page.waitForSelector('.leaflet-container');
|
||||
await page.waitForSelector('.leaflet-tile-loaded');
|
||||
// Wait for markers/overlays to render (may not exist with empty DB)
|
||||
try {
|
||||
await page.waitForSelector('.leaflet-marker-icon, .leaflet-interactive, circle, .marker-cluster, .leaflet-marker-pane > *, .leaflet-overlay-pane svg path, .leaflet-overlay-pane svg circle', { timeout: 3000 });
|
||||
await page.waitForSelector('.leaflet-marker-icon, .leaflet-interactive, circle, .marker-cluster, .leaflet-marker-pane > *, .leaflet-overlay-pane svg path, .leaflet-overlay-pane svg circle', { timeout: 8000 });
|
||||
} catch (_) {
|
||||
// No markers with empty DB \u2014 assertion below handles it
|
||||
}
|
||||
@@ -362,7 +366,7 @@ async function run() {
|
||||
await page.waitForSelector('.leaflet-container');
|
||||
// Wait for markers (may not exist with empty DB)
|
||||
try {
|
||||
await page.waitForSelector('.leaflet-marker-icon, .leaflet-interactive', { timeout: 3000 });
|
||||
await page.waitForSelector('.leaflet-marker-icon, .leaflet-interactive', { timeout: 8000 });
|
||||
} catch (_) {
|
||||
// No markers with empty DB
|
||||
}
|
||||
@@ -394,6 +398,7 @@ async function run() {
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => localStorage.setItem('meshcore-time-window', '525600'));
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForSelector('[data-loaded="true"]', { timeout: 15000 });
|
||||
await page.waitForSelector('table tbody tr', { timeout: 15000 });
|
||||
const rowsBefore = await page.$$('table tbody tr');
|
||||
assert(rowsBefore.length > 0, 'No packets visible');
|
||||
|
||||
Reference in New Issue
Block a user