diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index ee2798ca..5860ede2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -5604,7 +5604,10 @@ function navigateToTab(name, options = {}) { document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active")); btn.classList.add("active"); const toolsBtn = document.getElementById("mobile-more-btn"); - if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none"); + if (toolsBtn) { + const inToolsMenu = !!document.querySelector(`#mobile-more-menu [data-navigate-tab="${name}"]`); + toolsBtn.classList.toggle("active", inToolsMenu); + } window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn); document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none"); const panel = document.getElementById(`tab-${name}`); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts index aedd73d4..6f076953 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts @@ -4618,10 +4618,17 @@ function navigateToTab(name: TabName, options: { updateHistory?: boolean; replac btn.classList.add("active"); // A destination the strip hides is reached through Tools, so mark that // button instead — otherwise the strip looks identical on all four of them. - // Derived from what is actually hidden rather than from a second copy of the - // grouping, which would drift from the one ui-core installs. + // Membership of the Tools menu is the test: it still reads from the grouping + // ui-core installs rather than a second copy of it, but unlike the tab's + // computed display it does not depend on anything being laid out. The first + // route navigation runs while the card is still behind the loading state, + // where every tab computes to display:none — which lit Tools up on every + // refresh of every page. const toolsBtn = document.getElementById("mobile-more-btn"); - if (toolsBtn) toolsBtn.classList.toggle("active", getComputedStyle(btn).display === "none"); + if (toolsBtn) { + const inToolsMenu = !!document.querySelector(`#mobile-more-menu [data-navigate-tab="${name}"]`); + toolsBtn.classList.toggle("active", inToolsMenu); + } window.trxUi?.syncSelectedTab(document.querySelector(".tab-bar-nav"), btn); document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none"); const panel = document.getElementById(`tab-${name}`); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs index 4f35c38c..0ae6a4f2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/browser-smoke.mjs @@ -124,7 +124,9 @@ const contentTypes = new Map([ ]); function assetPath(urlPath) { - if (urlPath === "/") return path.join(webDir, "index.html"); + // Every tab route has its own index handler on the server (see api/assets.rs), + // so a deep link or a refresh serves the SPA shell, not a 404. + if (!path.extname(urlPath)) return path.join(webDir, "index.html"); if (urlPath.startsWith("/vendor/")) return path.join(webDir, urlPath); const generated = path.join(generatedDir, path.basename(urlPath)); if (urlPath.endsWith(".js")) return generated; @@ -265,6 +267,24 @@ try { assert.equal(new URL(page.url()).pathname, "/"); assert.deepEqual(runtimeErrors, []); + // Refreshing or deep-linking must mark the destination, not Tools. The first + // route navigation runs while the card is still behind the loading state, so + // a test that asked whether the tab was displayed saw "none" for every tab + // and lit Tools up on every refresh of every page. + for (const [route, tab, toolsLit] of [["/map", "map", false], ["/about", "about", true]]) { + await page.goto(`http://127.0.0.1:${address.port}${route}`, { waitUntil: "domcontentloaded" }); + await page.locator(`#tab-${tab}`).waitFor({ state: "visible" }); + const marked = await page.evaluate(() => ({ + actives: [...document.querySelectorAll(".tab-bar .tab.active")].map((t) => t.dataset.tab || t.id), + tools: document.getElementById("mobile-more-btn").classList.contains("active"), + })); + assert.ok(marked.actives.includes(tab), `${route} marks ${JSON.stringify(marked.actives)}`); + assert.equal(marked.tools, toolsLit, `${route}: Tools active is ${marked.tools}`); + } + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "domcontentloaded" }); + await page.locator("#tab-main").waitFor({ state: "visible" }); + assert.deepEqual(runtimeErrors, []); + // --- Layout regressions ------------------------------------------------- // Every fault below shipped at some point while the rest of this file // passed, because nothing here looked at geometry: a header whose height