Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion public/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,30 @@
}

// Re-render when distance unit or theme changes
_themeRefreshHandler = function () { renderTab(_currentTab); };
_themeRefreshHandler = function () {
// #1925: never full-rebuild the neighbor-graph tab on theme-refresh.
// Every page load fires one theme-refresh ~300ms after
// /api/config/theme resolves (app.js dispatches 'theme-changed', then
// debounces 300ms). renderTab() here replaced el.innerHTML, which reset
// the role checkboxes to their defaults and rebuilt _ngState from the
// full graph, discarding any filtering the user had applied in the
// meantime. Restarting the renderer keeps the current filter state and
// still picks up the new theme: node colors are read live per frame
// from window.ROLE_COLORS, role swatches use CSS tokens, and the one
// cached value (_labelColor) is re-read on restart. Measured: this also
// stops the old path leaking one rAF render loop and one canvas
// listener set per theme-refresh (a 10-event burst ran the force
// simulation at ~20x speed; it now stays flat).
//
// Not covered, pre-existing: _ngState is only assigned after the graph
// fetch resolves, while the checkboxes exist from the synchronous
// innerHTML before it. A theme-refresh landing inside that fetch window
// still falls through to renderTab() and still discards a filter
// applied during the load. Closing that needs a "load in flight"
// sentinel, which is deliberately left out of this port.
if (_currentTab === 'neighbor-graph' && _ngState) { startGraphRenderer(); return; }
renderTab(_currentTab);
};
window.addEventListener('theme-refresh', _themeRefreshHandler);

loadAnalytics();
Expand Down
28 changes: 28 additions & 0 deletions test-issue-1758-ng-filter-rerenders-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,34 @@ function buildFixture() {
if (reFiltered.canvasDisplay === 'none') fail('second filter-down: expected #ngCanvas visible again');
console.log(' ✓ second filter-down: render restored across repeated cycles');

// --- #1925: a theme-refresh must not discard the applied filter.
// Every page load fires one 'theme-refresh' about 300ms after
// /api/config/theme resolves. It used to call renderTab(), which replaced
// el.innerHTML (resetting the role checkboxes to their defaults) and
// rebuilt _ngState from the full graph, putting #ngSkipMsg back. On an
// idle machine the test finished ~90ms before that landed; under CI load
// it landed mid-test, which is both failure modes of #1925.
// Dispatching the event directly makes that deterministic, so this asserts
// the cause rather than waiting for the race to show up.
await page.evaluate(() => window.dispatchEvent(new CustomEvent('theme-refresh')));
await page.waitForTimeout(500);
const afterTheme = await page.evaluate(() => {
const canvas = document.getElementById('ngCanvas');
const cb = document.querySelector('#ngRoleChecks input[data-role="companion"]');
return {
hasSkip: !!document.getElementById('ngSkipMsg'),
canvasDisplay: canvas ? getComputedStyle(canvas).display : '(no canvas)',
companionChecked: cb ? cb.checked : '(no checkbox)',
};
});
if (afterTheme.companionChecked !== false) {
fail('theme-refresh reset the role filter (companion re-checked): '
+ JSON.stringify(afterTheme));
}
if (afterTheme.hasSkip) fail('theme-refresh brought #ngSkipMsg back: ' + JSON.stringify(afterTheme));
if (afterTheme.canvasDisplay === 'none') fail('theme-refresh hid #ngCanvas again');
console.log(' ✓ theme-refresh preserves the applied filter (#1925)');

await browser.close();

console.log('\nPASS: #1758 neighbor-graph filter re-render lifecycle holds');
Expand Down
Loading