import { fireEvent, render, screen, waitFor, within, } from "@testing-library/react"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import AnalysisReport, { clearAnalysisReportDiameterCache, matchesValveAnalysis, } from "./AnalysisReport"; import { SchemeRecord, ValveIsolationResult } from "./types"; import { isAllowedAccidentPipe, normalizeValveIsolationResult, } from "./valveIsolationScope"; jest.mock("@/utils/mapQueryService", () => ({ queryFeaturesByIds: jest.fn(), })); const scheme: SchemeRecord = { id: "17", schemeName: "burst-report-demo", type: "burst_analysis", username: "operator", create_time: "2026-07-30T08:00:00+08:00", startTime: "2026-07-30T09:00:00+08:00", schemeDetail: { burst_ID: ["P-1", "P-2"], burst_size: [120, 80], modify_total_duration: 5400, modify_fixed_pump_pattern: null, modify_valve_opening: null, modify_variable_pump_pattern: null, }, }; const valveResult: ValveIsolationResult = { accident_elements: ["P-1"], affected_nodes: ["J-1", "J-2"], must_close_valves: ["V-1"], optional_valves: ["V-2"], isolatable: true, }; const feature = (id: string, diameter: number) => ({ getProperties: () => ({ id, diameter }), }); describe("AnalysisReport", () => { beforeEach(() => { (queryFeaturesByIds as jest.Mock).mockImplementation( async (ids: string[], layerName: string) => layerName === "pipes" ? ids.map((id) => id === "P-1" ? feature(id, 315) : feature(id, 800), ) : [], ); }); afterEach(() => { clearAnalysisReportDiameterCache(); jest.clearAllMocks(); document.body.classList.remove("burst-analysis-report-printing"); }); it("guides the user to choose a scheme when no report is active", () => { render( , ); expect(screen.getByText("尚未选择分析方案")).toBeInTheDocument(); expect( screen.getByText( "请在“方案查询”中打开一个方案,再查看分析报告。", ), ).toBeInTheDocument(); }); it("renders the selected scheme, pipe data, and matching valve result", async () => { render( , ); const preview = within( screen.getByTestId("burst-analysis-report-preview"), ); expect( preview.getByRole("heading", { name: "爆管分析报告" }), ).toBeInTheDocument(); expect(preview.getByText("burst-report-demo")).toBeInTheDocument(); expect(preview.getByText("可隔离")).toBeInTheDocument(); expect(preview.getByText("V-1")).toBeInTheDocument(); expect(preview.getByText("V-3")).toBeInTheDocument(); await waitFor(() => { expect(preview.getByText("315 mm")).toBeInTheDocument(); expect(preview.getByText("800 mm")).toBeInTheDocument(); }); expect(queryFeaturesByIds).toHaveBeenCalledWith( ["P-1", "P-2"], "pipes", ); }); it("does not mix an unrelated valve analysis into the report", async () => { render( , ); const preview = within( screen.getByTestId("burst-analysis-report-preview"), ); expect( preview.getByText(/尚未对本方案执行匹配的关阀分析/), ).toBeInTheDocument(); expect(preview.queryByText("V-1")).not.toBeInTheDocument(); expect( matchesValveAnalysis(scheme, { ...valveResult, accident_elements: ["P-99"], }), ).toBe(false); await waitFor(() => expect(preview.getByText("315 mm")).toBeInTheDocument(), ); }); it("shows only the affected count for a non-isolatable result", async () => { const legacyAffectedNodes = Array.from( { length: 100 }, (_, index) => `legacy-node-${index}`, ); const nonIsolatableResult = { ...valveResult, affected_nodes: legacyAffectedNodes, affected_node_count: 85747, must_close_valves: [], isolatable: false, }; render( , ); const preview = within( screen.getByTestId("burst-analysis-report-preview"), ); expect(preview.getByText("85747 个")).toBeInTheDocument(); expect( preview.getByText("不可隔离,未生成受影响节点清单。"), ).toBeInTheDocument(); expect(preview.queryByText("legacy-node-0")).not.toBeInTheDocument(); expect(preview.queryByText("legacy-node-99")).not.toBeInTheDocument(); await waitFor(() => expect(preview.getByText("315 mm")).toBeInTheDocument(), ); }); it("limits scheme-bound valve analysis to the scheme accident pipes", () => { expect(isAllowedAccidentPipe("P-1", ["P-1", "P-2"])).toBe(true); expect(isAllowedAccidentPipe("P-99", ["P-1", "P-2"])).toBe(false); expect(isAllowedAccidentPipe("P-99", undefined)).toBe(true); }); it("normalizes legacy valve results without changing isolatable lists", () => { expect( normalizeValveIsolationResult({ ...valveResult, affected_nodes: ["J-1", "J-2", "J-3"], must_close_valves: [], isolatable: false, }), ).toMatchObject({ affected_nodes: [], affected_node_count: 3, isolatable: false, }); expect(normalizeValveIsolationResult(valveResult)).toMatchObject({ affected_nodes: ["J-1", "J-2"], affected_node_count: 2, isolatable: true, }); }); it("prints only after assigning the report print state", async () => { const originalTitle = document.title; const print = jest .spyOn(window, "print") .mockImplementation(() => undefined); render( , ); const preview = within( screen.getByTestId("burst-analysis-report-preview"), ); await waitFor(() => expect(preview.getByText("315 mm")).toBeInTheDocument(), ); expect( document.querySelector(".burst-analysis-report-print-root"), ).not.toBeInTheDocument(); fireEvent.click( screen.getByRole("button", { name: "打印/保存 PDF" }), ); expect(print).toHaveBeenCalledTimes(1); expect( document.querySelector(".burst-analysis-report-print-root"), ).toBeInTheDocument(); expect(document.body).toHaveClass("burst-analysis-report-printing"); expect(document.title).toBe("爆管分析报告-burst-report-demo"); fireEvent(window, new Event("afterprint")); expect(document.body).not.toHaveClass( "burst-analysis-report-printing", ); expect(document.title).toBe(originalTitle); print.mockRestore(); }); it("reuses pipe diameters after the report is remounted", async () => { const props = { scheme, valveResult: null, disabledValves: [], generatedAt: new Date("2026-07-30T10:00:00+08:00"), }; const firstRender = render(); await waitFor(() => expect( within(screen.getByTestId("burst-analysis-report-preview")).getByText( "800 mm", ), ).toBeInTheDocument(), ); expect(queryFeaturesByIds).toHaveBeenCalledTimes(1); firstRender.unmount(); render(); expect( within(screen.getByTestId("burst-analysis-report-preview")).getByText( "800 mm", ), ).toBeInTheDocument(); expect(queryFeaturesByIds).toHaveBeenCalledTimes(1); }); });