diff --git a/src/App.tsx b/src/App.tsx index 4207830..08cd63a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -183,8 +183,7 @@ function AppInner() { }, []); const handleAnalyze = useCallback((hash: string | null) => { - // No reset here: observation ids are globally unique, so a stale one from another packet - // can't accidentally match — this lets a pick made inside an expanded row survive into the drawer. + // No reset: observation ids are globally unique, so a pick inside an expanded row survives into the drawer. setSearchParams((p) => { const n = new URLSearchParams(p); if (hash) { n.set("hash", hash); n.set("analyze", "1"); n.delete("path"); } diff --git a/src/features/packets/PacketList.tsx b/src/features/packets/PacketList.tsx index 58d8a60..0340b6b 100644 --- a/src/features/packets/PacketList.tsx +++ b/src/features/packets/PacketList.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { useState, useCallback, useEffect, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { usePackets } from "./usePackets"; @@ -79,23 +79,6 @@ export function PacketList({ wsManager, onAnalyze, onViewPath, selectedObservati // ?hash is the selected packet — it expands the row inline. The analyzer is a separate state (?analyze=1). const expandedHash = searchParams.get("hash"); - // A deep-linked ?hash that matches nothing once the first page has loaded is stale (bogus, or - // long-expired) — strip it so it doesn't linger forever. Only the hash present at mount is - // checked, once, so a user's own click-to-expand (always a packet already in allPackets) never - // trips this. Gated on isLoading so a slow first page can't strip a link before its packet arrives. - const [initialHash] = useState(() => searchParams.get("hash")); - const strippedInitialHashRef = useRef(false); - useEffect(() => { - if (strippedInitialHashRef.current || isLoading || !initialHash) return; - strippedInitialHashRef.current = true; - if (allPackets.some((p) => p.packetHash === initialHash)) return; - setSearchParams((p) => { - const n = new URLSearchParams(p); - if (n.get("hash") === initialHash) n.delete("hash"); - return n; - }, { replace: true }); - }, [isLoading, initialHash, allPackets, setSearchParams]); - const handleToggleExpand = useCallback((hash: string) => { const next = expandedHash === hash ? null : hash; setSearchParams((p) => { diff --git a/tests/features/packets/PacketList.test.tsx b/tests/features/packets/PacketList.test.tsx index 274367e..d704489 100644 --- a/tests/features/packets/PacketList.test.tsx +++ b/tests/features/packets/PacketList.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi, afterEach } from "vitest"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { MemoryRouter, useLocation } from "react-router-dom"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { PacketList } from "../../../src/features/packets/PacketList"; import type { WsManager } from "../../../src/api/ws-manager"; @@ -100,11 +100,6 @@ const observation = (hash: string): WsPacketObservation["data"] => ({ }, }); -function LocationProbe() { - const location = useLocation(); - return
{location.search}
; -} - function renderList(url = "/", props: Partial[0]> = {}) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const invalidate = vi.spyOn(queryClient, "invalidateQueries"); @@ -112,7 +107,7 @@ function renderList(url = "/", props: Partial[0]> const onViewPath = props.onViewPath ?? vi.fn(); const onSelectObservation = props.onSelectObservation ?? vi.fn(); - const tree = ( + render( [0]> selectedObservationId={null} onSelectObservation={onSelectObservation} /> - - + , ); - const utils = render(tree); - - // MemoryRouter only reads initialEntries on its first mount, so rerendering with the identical - // element (same position in the tree) keeps whatever location the component has navigated to. - return { onAnalyze, onViewPath, onSelectObservation, invalidate, rerender: () => utils.rerender(tree) }; + return { onAnalyze, onViewPath, onSelectObservation, invalidate }; } describe("PacketList server filter wiring", () => { @@ -280,38 +270,3 @@ describe("PacketList live observation invalidation", () => { expect(invalidate).not.toHaveBeenCalled(); }); }); - -describe("PacketList stale ?hash strip", () => { - afterEach(() => { - usePackets.mockImplementation(basePackets); - }); - - it("strips a ?hash matching no loaded packet once the first page has loaded", async () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("AA11")] })); - - renderList("/?tab=Packets&hash=BOGUS"); - - await waitFor(() => expect(screen.getByTestId("search").textContent).not.toContain("hash=")); - }); - - it("does not strip while the first page is still loading", () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: true, allPackets: [] })); - - const { rerender } = renderList("/?tab=Packets&hash=BOGUS"); - expect(screen.getByTestId("search").textContent).toContain("hash=BOGUS"); - - // the packet that matches the deep link arrives only after the first page resolves - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("BOGUS")] })); - rerender(); - - expect(screen.getByTestId("search").textContent).toContain("hash=BOGUS"); - }); - - it("does not strip a ?hash that matches a loaded packet", () => { - usePackets.mockImplementation(() => ({ ...basePackets(), isLoading: false, allPackets: [packet("AA11")] })); - - renderList("/?tab=Packets&hash=AA11"); - - expect(screen.getByTestId("search").textContent).toContain("hash=AA11"); - }); -});