From 58cc2c3ce942e4df331e45cca7076880af594593 Mon Sep 17 00:00:00 2001 From: drakezhang Date: Tue, 22 Sep 2026 12:25:20 +0800 Subject: [PATCH] fix(debug): preserve results when reselecting active tabs --- .../src/entrypoints/debug/App.test.tsx | 29 +++- .../src/entrypoints/debug/analysis.test.tsx | 127 ++++++++++++++++++ .../src/entrypoints/debug/analysis.tsx | 1 + .../src/entrypoints/debug/evidence.tsx | 1 + 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 apps/extension/src/entrypoints/debug/analysis.test.tsx diff --git a/apps/extension/src/entrypoints/debug/App.test.tsx b/apps/extension/src/entrypoints/debug/App.test.tsx index 7cebf7f..a9a1807 100644 --- a/apps/extension/src/entrypoints/debug/App.test.tsx +++ b/apps/extension/src/entrypoints/debug/App.test.tsx @@ -10,7 +10,7 @@ import { } from "@/debug/client"; import type { DebugOperation, DebugRequest, DebugRun } from "@/debug/types"; import { DebugApp } from "./App"; -import { RequestList } from "./evidence"; +import { RequestDetail, RequestList } from "./evidence"; import { useRequests } from "./use-requests"; vi.mock("@/debug/client", () => ({ @@ -293,6 +293,33 @@ describe("website evidence workspace", () => { }); }); +describe("request detail navigation", () => { + it.each([ + { part: "response", label: "响应正文", content: '{"ok":false}' }, + { part: "headers", label: "Headers", content: "[redacted]" }, + ] as const)("keeps loaded $part evidence when its selected tab is clicked again", async ({ + part, + label, + content, + }) => { + render( + {}} + />, + ); + await screen.findByText(content); + const reads = vi.mocked(recordingRequest).mock.calls.length; + fireEvent.click(screen.getByRole("button", { name: label })); + expect(screen.getByText(content)).toBeTruthy(); + expect(screen.queryByText("正在读取…")).toBeNull(); + expect(recordingRequest).toHaveBeenCalledTimes(reads); + }); +}); + describe("incremental request list", () => { it("shows the newest request immediately without changing operation-list order", () => { const entries = Array.from({ length: 150 }, (_, i) => ({ diff --git a/apps/extension/src/entrypoints/debug/analysis.test.tsx b/apps/extension/src/entrypoints/debug/analysis.test.tsx new file mode 100644 index 0000000..36a7999 --- /dev/null +++ b/apps/extension/src/entrypoints/debug/analysis.test.tsx @@ -0,0 +1,127 @@ +import { i18n } from "@browser-skill/i18n"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { recordingRequest } from "@/debug/client"; +import type { DebugResult } from "@/debug/types"; +import { AnalysisPanel } from "./analysis"; + +vi.mock("@/debug/client", () => ({ recordingRequest: vi.fn() })); +afterEach(cleanup); + +const modes = [ + { action: "aggregate", label: "接口聚合", other: "疑似重复", otherAction: "duplicates" }, + { action: "duplicates", label: "疑似重复", other: "接口聚合", otherAction: "aggregate" }, +] as const; + +function result(action: string, offset: number): DebugResult { + const url = `https://site.test/${action}/${offset}`; + return { + session_id: "s1", + next_offset: offset === 0 ? 20 : undefined, + aggregates: [ + { + id: url, + method: "GET", + endpoint: url, + count: 2, + failed: 0, + http_errors: 0, + pending: 0, + interrupted: 0, + statuses: { "200": 2 }, + slow: 0, + timing_samples: 2, + transfer_bytes: 100, + transfer_samples: 2, + cached: 0, + service_worker: 0, + controlled: 0, + replayed: 0, + request_ids: ["d1:n1", "d1:n2"], + refs_truncated: false, + }, + ], + duplicates: [ + { + id: url, + method: "GET", + url, + count: 2, + extra_requests: 1, + started_at: 100, + ended_at: 150, + overlap_count: 1, + possible_retry: false, + request_ids: ["d1:n1", "d1:n2"], + operation_ids: [], + refs_truncated: false, + }, + ], + }; +} + +beforeEach(async () => { + await i18n.changeLanguage("zh-CN"); + vi.clearAllMocks(); + vi.mocked(recordingRequest).mockImplementation(async ({ action, offset = 0 }) => + result(action, offset), + ); +}); + +describe("analysis of a stopped recording", () => { + it.each(modes)("keeps $action results when reselecting the current mode", async (mode) => { + render( {}} />); + await screen.findByRole("heading", { name: "GET https://site.test/aggregate/0" }); + fireEvent.click(screen.getByRole("button", { name: mode.label })); + const heading = `GET https://site.test/${mode.action}/0`; + await screen.findByRole("heading", { name: heading }); + const reads = vi.mocked(recordingRequest).mock.calls.length; + + fireEvent.click(screen.getByRole("button", { name: mode.label })); + + expect(screen.getByRole("heading", { name: heading })).toBeTruthy(); + expect(screen.queryByText("正在读取…")).toBeNull(); + expect(recordingRequest).toHaveBeenCalledTimes(reads); + + fireEvent.click(screen.getByRole("button", { name: mode.other })); + await screen.findByRole("heading", { + name: `GET https://site.test/${mode.otherAction}/0`, + }); + expect(recordingRequest).toHaveBeenCalledTimes(reads + 1); + expect(recordingRequest).toHaveBeenLastCalledWith({ + session_id: "s1", + run_id: "d1", + action: mode.otherAction, + offset: 0, + limit: 20, + }); + }); + + it.each(modes)("keeps the current $action page when reselecting its mode", async (mode) => { + render( {}} />); + await screen.findByRole("heading", { name: "GET https://site.test/aggregate/0" }); + fireEvent.click(screen.getByRole("button", { name: mode.label })); + await screen.findByRole("heading", { name: `GET https://site.test/${mode.action}/0` }); + fireEvent.click(screen.getByRole("button", { name: "下一页" })); + const heading = `GET https://site.test/${mode.action}/20`; + await screen.findByRole("heading", { name: heading }); + const reads = vi.mocked(recordingRequest).mock.calls.length; + + fireEvent.click(screen.getByRole("button", { name: mode.label })); + + expect(screen.getByRole("heading", { name: heading })).toBeTruthy(); + expect(recordingRequest).toHaveBeenCalledTimes(reads); + expect((screen.getByRole("button", { name: "上一页" }) as HTMLButtonElement).disabled).toBe( + false, + ); + + fireEvent.click(screen.getByRole("button", { name: mode.other })); + await screen.findByRole("heading", { + name: `GET https://site.test/${mode.otherAction}/0`, + }); + expect(recordingRequest).toHaveBeenCalledTimes(reads + 1); + expect((screen.getByRole("button", { name: "上一页" }) as HTMLButtonElement).disabled).toBe( + true, + ); + }); +}); diff --git a/apps/extension/src/entrypoints/debug/analysis.tsx b/apps/extension/src/entrypoints/debug/analysis.tsx index 625fc52..a3b5cc9 100644 --- a/apps/extension/src/entrypoints/debug/analysis.tsx +++ b/apps/extension/src/entrypoints/debug/analysis.tsx @@ -81,6 +81,7 @@ export function AnalysisPanel({ size="sm" variant={mode === value ? "default" : "outline"} onClick={() => { + if (mode === value) return; setMode(value); setOffsets([0]); setData(undefined); diff --git a/apps/extension/src/entrypoints/debug/evidence.tsx b/apps/extension/src/entrypoints/debug/evidence.tsx index 3f0de05..46ed8ef 100644 --- a/apps/extension/src/entrypoints/debug/evidence.tsx +++ b/apps/extension/src/entrypoints/debug/evidence.tsx @@ -458,6 +458,7 @@ export function RequestDetail({ type="button" aria-pressed={part === value} onClick={() => { + if (part === value) return; setPart(value); setOffset(0); setData(undefined);