mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(dashboard): change pages without reloading the dashboard
nav.js (loaded on every signed-in page) follows links and GET forms to other dashboard pages by fetching the page and swapping in its <main>, title, page styles and page scripts; the sidebar, top bar and live WebSocket stay. History entries, back/forward (with scroll positions, also kept for a reload), deep links, #hash filters and breadcrumbs keep working. What the old page set up is taken down first: its document/window listeners, setInterval timers, Live watchers and topics, DataTables, dialogs and what it appended to <body>. Page scripts run again in order; DOMContentLoaded/load handlers they add run once they have all run. A thin bar shows while loading; a failed fetch shows an inline error with Try again / Open it normally. Falls back to a normal load for anything that isn't a signed-in dashboard page, when the account or permissions changed, and for pages with module scripts or an import map (World 3D, property view and 3D, UGC server) or data-nav="reload", and when leaving those. Unsaved-change prompts (beforeunload) are asked before swapping. Check: click around the sidebar and into accounts/characters: no white flash, the footer's live indicator stays "live", back/forward return to the same scroll position, breadcrumbs follow the path (Accounts > an account > a character). Activity Log links with #search= still filter. System Log's server picker works. Leaving Settings with a change asks first. World 3D and a property's 3D view still load (normally). Pages that poll (Server Health, Instance Load) stop polling once left (browser network tab). Test: NavRulesJs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,4 +94,7 @@ if(NODE_EXECUTABLE)
|
||||
# The sidebar's saved group state (the script at the end of templates/header.jinja2)
|
||||
add_test(NAME SidebarStateJs COMMAND ${NODE_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/sidebar-state.test.mjs"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/templates/header.jinja2")
|
||||
# Which links the dashboard follows without a reload (static/js/nav.js)
|
||||
add_test(NAME NavRulesJs COMMAND ${NODE_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/nav.test.mjs"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/static/js/nav.js")
|
||||
endif()
|
||||
|
||||
65
tests/dWebTests/nav.test.mjs
Normal file
65
tests/dWebTests/nav.test.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Which links the dashboard follows without a reload (static/js/nav.js).
|
||||
// Run by ctest: node nav.test.mjs <nav.js>
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const [navPath] = process.argv.slice(2);
|
||||
const window = {};
|
||||
vm.runInNewContext(readFileSync(navPath, 'utf8'), { window });
|
||||
const R = window.NavRules;
|
||||
let failures = 0;
|
||||
const same = (actual, expected, what) => {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
failures++;
|
||||
console.error(`${what}: ${JSON.stringify(actual)} is not ${JSON.stringify(expected)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const here = new URL('http://127.0.0.1:2006/accounts?x=1');
|
||||
const kind = (href) => R.kind(new URL(href, here), here);
|
||||
|
||||
// Dashboard pages are swapped in
|
||||
same(kind('/characters'), 'swap', 'a list page');
|
||||
same(kind('/characters/1234'), 'swap', 'a detail page');
|
||||
same(kind('/properties/5/3d'), 'swap', 'the 3D view (the fetched page then asks for a normal load)');
|
||||
same(kind('/system_log?server=world'), 'swap', 'a page with a query');
|
||||
same(kind('/activity_log#search=12'), 'swap', 'another page with a hash filter');
|
||||
same(kind('/accounts'), 'swap', 'the same page without its query');
|
||||
same(kind('/accounts?x=1'), 'swap', 'the page that is open, again');
|
||||
same(kind('/'), 'swap', 'home');
|
||||
same(kind('/status'), 'swap', 'server status');
|
||||
|
||||
// Only the fragment of this page: the browser does it
|
||||
same(kind('/accounts?x=1#tab'), 'hash', 'fragment of this page');
|
||||
same(kind('#tab'), 'hash', 'bare fragment');
|
||||
|
||||
// Normal loads
|
||||
same(kind('https://example.com/characters'), null, 'another site');
|
||||
same(kind('http://127.0.0.1:2007/characters'), null, 'another port');
|
||||
same(kind('mailto:a@b.c'), null, 'mailto');
|
||||
for (const path of ['/api/backups/1/download', '/css/dashboard.css', '/js/nav.js', '/ws', '/metrics', '/login', '/logout',
|
||||
'/register', '/forgot_password', '/reset_password?token=1', '/verify_email', '/oauth2/callback', '/status/widget', '/ugc/model.nif', '/files/log.zip']) {
|
||||
same(kind(path), null, path);
|
||||
}
|
||||
same(kind('/logins'), 'swap', 'a path that only starts like a signed-out page');
|
||||
|
||||
// Pages of one kind share what was learnt about them
|
||||
same(R.pageKind('/properties/12/3d'), '/properties/*/3d', 'a 3D view');
|
||||
same(R.pageKind('/characters/1152921504606846978'), '/characters/*', 'a character');
|
||||
same(R.pageKind('/world3d'), '/world3d', 'no numbers');
|
||||
|
||||
// Scripts: classic JavaScript runs again, data and modules don't
|
||||
for (const type of [null, '', 'text/javascript', 'application/javascript', 'TEXT/JavaScript ', 'application/x-javascript', 'text/ecmascript']) same(R.runnable(type), true, 'runs: ' + type);
|
||||
for (const type of ['application/json', 'module', 'importmap', 'text/template']) same(R.runnable(type), false, 'inert: ' + type);
|
||||
|
||||
// Child lists line up only when every key does
|
||||
same(R.sameKeys(['DIV#a', '#3', 'P'], ['DIV#a', '#3', 'P']), true, 'same keys');
|
||||
same(R.sameKeys(['DIV#a', 'P'], ['DIV#a', '#3', 'P']), false, 'one more');
|
||||
same(R.sameKeys(['DIV#a'], ['DIV#b']), false, 'other id');
|
||||
same(R.sameKeys([], []), true, 'empty');
|
||||
|
||||
if (failures) {
|
||||
console.error(`${failures} failure(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('nav rules: all passed');
|
||||
Reference in New Issue
Block a user