diff --git a/src/features/observers/observer-navigation.ts b/src/features/observers/observer-navigation.ts
index b33fee7..75e2a4f 100644
--- a/src/features/observers/observer-navigation.ts
+++ b/src/features/observers/observer-navigation.ts
@@ -5,7 +5,7 @@ export const observerRange = (value: string | null): StatsRange => value === "24
export function observerDestination(params: URLSearchParams, id: string | null, range = observerRange(params.get("range"))) {
const next = new URLSearchParams(params);
next.set("tab", "Observers");
- for (const key of ["statsTab", "observerId", "node", "hash", "analyze", "path", "observation", "route", "routeIata", "routeRange", "routeSince", "routeUntil", "compareA", "compareB", "compareSince"]) next.delete(key);
+ for (const key of ["statsTab", "observerId", "node", "route", "routeIata", "routeRange", "routeSince", "routeUntil", "compareA", "compareB", "compareSince"]) next.delete(key);
if (id) { next.set("observer", id); next.set("range", range); }
else { next.delete("observer"); next.delete("range"); next.delete("compareWith"); next.delete("compareUntil"); }
return next;
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 0daa97b..0c8c5dc 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -309,7 +309,6 @@
"recording": "Loading observer…",
"hour": "hour",
"day": "day",
- "returnTo": "Back to {{page}}",
"directory": "All observers",
"sidebarSearch": "Search observers",
"noMatches": "No matches",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index b1dd242..a919723 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -313,7 +313,6 @@
"recording": "Chargement de l’observateur…",
"hour": "heure",
"day": "jour",
- "returnTo": "Retour à « {{page}} »",
"directory": "Tous les observateurs",
"sidebarSearch": "Rechercher les observateurs",
"noMatches": "Aucun résultat",
diff --git a/tests/App.observerInvestigation.test.tsx b/tests/App.observerInvestigation.test.tsx
index 983675d..2b1f0e3 100644
--- a/tests/App.observerInvestigation.test.tsx
+++ b/tests/App.observerInvestigation.test.tsx
@@ -51,7 +51,7 @@ const click = (name: string) => { const button = screen.getByRole("button", { na
const escape = () => fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
const browserBack = async () => { await act(async () => { window.history.back(); }); await waitFor(() => expect(window.location.search).toContain("tab=Routes")); };
-describe("observer investigation return", () => {
+describe("observer investigation windows", () => {
it("dismisses only the observer with Escape and returns keyboard focus", () => {
render(
); click("Route observer"); escape();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
@@ -77,32 +77,35 @@ describe("observer investigation return", () => {
expect(within(screen.getByRole("dialog")).getByRole("heading")).toHaveTextContent("Observer o1");
escape(); expect(within(screen.getByRole("dialog")).getByRole("heading")).toHaveTextContent("Packet aa report 8");
});
- it("returns from a changed dashboard window to the same route, filter, scroll and observer", async () => {
- render(
);
- fireEvent.change(screen.getByLabelText("Route filter"), { target: { value: "roof" } });
- screen.getByTestId("route-scroll").scrollTop = 180;
- click("Route observer"); click("Open dashboard");
+ it("opens the dashboard as a normal tab with every window left open", async () => {
+ render(
); click("Route packet"); click("Packet observer"); click("Open dashboard");
expect(await screen.findByRole("heading", { level: 1 })).toHaveTextContent("Dashboard o1 7d");
expect(window.location.search).toContain("tab=Observers&observer=o1");
- expect(screen.getByTestId("origin-url")).toHaveTextContent("route=full-route");
- fireEvent.click(within(screen.getByRole("group", { name: "Time range" })).getByRole("button", { name: "24h" }));
- click("Compare with…");
- expect(window.location.search).toContain("compareWith=");
- click("Back to Routes");
- await waitFor(() => expect(window.location.search).toContain("tab=Routes"));
- expect(screen.getByLabelText("Route filter")).toHaveValue("roof");
- expect(screen.getByTestId("route-scroll").scrollTop).toBe(180);
- await waitFor(() => expect(screen.getByRole("button", { name: "Open dashboard" })).toHaveFocus());
- escape(); expect(screen.getByRole("button", { name: "Route observer" })).toHaveFocus();
+ expect(screen.queryByRole("button", { name: "Back to Routes" })).not.toBeInTheDocument();
+ expect(within(screen.getByRole("dialog")).getByRole("heading")).toHaveTextContent("Observer o1");
+ escape(); expect(within(screen.getByRole("dialog")).getByRole("heading")).toHaveTextContent("Packet aa");
});
- it("supports browser Back/Forward without rebuilding the originating screen", async () => {
- render(
); fireEvent.change(screen.getByLabelText("Route filter"), { target: { value: "keep" } });
- click("Route observer"); click("Open dashboard");
+ it("returns to the previous tab with browser Back", async () => {
+ render(
); click("Route observer"); click("Open dashboard");
await screen.findByRole("heading", { level: 1 });
- await browserBack(); expect(screen.getByLabelText("Route filter")).toHaveValue("keep");
- await act(async () => { window.history.forward(); });
- expect(await screen.findByRole("heading", { level: 1 })).toHaveTextContent("Dashboard o1");
- await browserBack(); expect(screen.getByLabelText("Route filter")).toHaveValue("keep");
+ await browserBack(); expect(screen.getByTestId("route-origin")).toBeInTheDocument();
+ });
+ it("keeps the packet drawer and stacked windows across tabs, but not into Analytics", async () => {
+ await import("../src/features/stats/StatsOverview");
+ window.history.replaceState({}, "", "/?tab=Routes&hash=aa&analyze=1");
+ render(
);
+ expect(screen.getByRole("heading", { name: /Packet aa/ })).toBeInTheDocument();
+ click("Packet node");
+ click("Observer tab");
+ expect(await screen.findByRole("heading", { level: 2, name: /Packet aa/ })).toBeInTheDocument();
+ expect(within(screen.getByRole("dialog")).getByRole("heading")).toHaveTextContent("Node n1");
+ click("Analytics tab");
+ await screen.findByRole("button", { name: "Leaderboard observer" });
+ expect(screen.queryByRole("heading", { name: /Packet aa/ })).not.toBeInTheDocument();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ click("Route tab");
+ expect(await screen.findByRole("heading", { name: /Packet aa/ })).toBeInTheDocument();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("opens the Observers dashboard from the leaderboard", async () => {
await import("../src/features/stats/StatsOverview");
@@ -122,23 +125,10 @@ describe("observer investigation return", () => {
expect(window.location.search).toContain("tab=Observers");
expect(window.location.search).not.toContain("statsTab");
});
- it("does not invent a return destination for a reloaded/shared dashboard", async () => {
- window.history.replaceState({ usr: { beaconObserverReturnKey: "old" } }, "", "/?tab=Observers&observer=o2&range=24h");
- render(
); expect(await screen.findByRole("heading", { level: 1 })).toHaveTextContent("Dashboard o2 24h");
- expect(screen.queryByRole("button", { name: "Back to Routes" })).not.toBeInTheDocument();
- click("Back to observers"); expect(screen.queryByRole("heading", { level: 1 })).not.toBeInTheDocument();
- expect(screen.getByText("Observer directory")).toBeInTheDocument();
- });
- it("ends the retained investigation when the operator changes tabs or regions", async () => {
+ it("closes the stacked windows when the region changes", async () => {
render(
); click("Route observer"); click("Open dashboard"); await screen.findByRole("heading", { level: 1 });
click("Change region");
- await waitFor(() => expect(screen.queryByRole("button", { name: "Back to Routes" })).not.toBeInTheDocument());
- expect(screen.queryByTestId("route-origin")).not.toBeInTheDocument();
- click("Route tab"); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
- });
- it("translates the return action", async () => {
- await i18n.changeLanguage("fr"); render(
); click("Route observer"); click("Open dashboard");
- expect(await screen.findByRole("button", { name: /Retour à « Routes »/ })).toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
});
it("clears comparison params from both the observer dashboard and analytics compare tabs on tab change", () => {
window.history.replaceState({}, "", "/?tab=Observers&observer=o1&compareWith=o2&compareUntil=1700000000000&compareA=x&compareB=y&compareSince=1");
diff --git a/tests/App.packetUrlContract.test.tsx b/tests/App.packetUrlContract.test.tsx
index 9f706f8..dc3fbbd 100644
--- a/tests/App.packetUrlContract.test.tsx
+++ b/tests/App.packetUrlContract.test.tsx
@@ -141,7 +141,7 @@ afterEach(() => {
});
describe("Packets deep links", () => {
- it("returns from an observer dashboard to the same packet, report and filters", async () => {
+ it("keeps the packet drawer beside the observer dashboard and returns to the same packet, report and filters", async () => {
setMobile(false);
window.history.pushState({}, "", "/?tab=Packets&hash=AA11&analyze=1&observation=1&iata=YOW&q=missing");
render(
);
@@ -150,10 +150,10 @@ describe("Packets deep links", () => {
fireEvent.click(within(section).getByRole("button", { name: "Inspect observer" }));
fireEvent.click(await screen.findByRole("button", { name: "Open observer dashboard" }));
expect(await screen.findByRole("heading", { name: "Observer dashboard" })).toBeInTheDocument();
- expect(new URLSearchParams(window.location.search).has("observation")).toBe(false);
+ expect(new URLSearchParams(window.location.search).get("observation")).toBe("1");
+ expect(screen.getByTestId("packet-analyzer-drawer")).toBeInTheDocument();
act(() => window.history.back());
- // The originating drawer now stays mounted while hidden; wait for history, not its existence.
- await waitFor(() => expect(new URLSearchParams(window.location.search).get("hash")).toBe("AA11"));
+ await waitFor(() => expect(new URLSearchParams(window.location.search).get("tab")).toBe("Packets"));
expect(await screen.findByTestId("packet-analyzer-drawer")).toBeInTheDocument();
const params = new URLSearchParams(window.location.search);
expect(params.get("hash")).toBe("AA11"); expect(params.get("observation")).toBe("1");
diff --git a/tests/features/observers/observer-navigation.test.ts b/tests/features/observers/observer-navigation.test.ts
index 05cfa9d..bd28e75 100644
--- a/tests/features/observers/observer-navigation.test.ts
+++ b/tests/features/observers/observer-navigation.test.ts
@@ -15,9 +15,9 @@ describe("observerDestination", () => {
expect(p.has("statsTab")).toBe(false);
});
- it("drops unrelated investigation selectors and defaults the range to 7d", () => {
- const p = observerDestination(new URLSearchParams("hash=abc&analyze=1&node=n&path=p"), "b");
- expect(p.toString()).toBe("tab=Observers&observer=b&range=7d");
+ it("keeps the open packet analyzer, drops other tabs' selectors and defaults the range to 7d", () => {
+ const p = observerDestination(new URLSearchParams("hash=abc&analyze=1&observation=5&path=p&node=n&route=r"), "b");
+ expect(Object.fromEntries(p)).toEqual({ hash: "abc", analyze: "1", observation: "5", path: "p", tab: "Observers", observer: "b", range: "7d" });
});
it("drops the analytics compare params so they don't ride along into the dashboard URL", () => {