feat(frontend): cache 3D assets and restore loading feedback
Generic Container CI/CD / test-build-publish (push) Successful in 1m11s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m11s

Persist versioned models and validated network metadata in IndexedDB. Show scene download progress and recoverable errors with the existing frosted material.

Connect existing skeletons to pending navigation and route boundaries while preserving the map. Previously only dynamic imports showed fallbacks, and rejected model promises prevented retries.

Verified: 32 focused unit tests; 4 navigation and scene browser checks; 7 cache browser checks including invalid-metadata recovery; isolated production build and type checks.
This commit is contained in:
2026-10-16 18:13:04 +08:00
parent c4a90be0d3
commit 4a522bf42a
23 changed files with 938 additions and 59 deletions
@@ -0,0 +1,78 @@
import { fireEvent, render, screen } from "@testing-library/react";
import {
MapNavigationContent,
NavigationFeedbackProvider,
PageNavigationContent,
useNavigationFeedback,
} from "./NavigationFeedback";
import { mapSkeletonForPath } from "./PageLoadingSkeleton";
import { MAP_SKELETON_VARIANTS } from "./mapComponentSkeletonConfig";
let mockPathname = "/network-simulation";
jest.mock("next/navigation", () => ({ usePathname: () => mockPathname }));
function Harness() {
const { reportPending } = useNavigationFeedback();
return (
<>
<button onClick={() => reportPending("first", "/scada-data-cleaning", true)}>清洗</button>
<button onClick={() => reportPending("second", "/audit-logs", true)}>审计</button>
<button onClick={() => reportPending("first", "/scada-data-cleaning", false)}>取消旧导航</button>
<button onClick={() => reportPending("second", "/audit-logs", false)}>结束导航</button>
<PageNavigationContent>
<div data-testid="map">原有地图</div>
<MapNavigationContent><input aria-label="页面草稿" defaultValue="保留输入" /></MapNavigationContent>
</PageNavigationContent>
</>
);
}
describe("page navigation feedback", () => {
beforeEach(() => { mockPathname = "/network-simulation"; });
it("shows the destination skeleton while preserving the map and a cancelled page's draft", () => {
render(<NavigationFeedbackProvider><Harness /></NavigationFeedbackProvider>);
const map = screen.getByTestId("map");
const draft = screen.getByRole("textbox", { name: "页面草稿" });
fireEvent.click(screen.getByText("清洗"));
expect(screen.getByRole("status", { name: "正在加载数据清洗" })).toBeVisible();
expect(screen.getByTestId("map")).toBe(map);
expect(map).toBeVisible();
expect(draft).not.toBeVisible();
fireEvent.click(screen.getByText("取消旧导航"));
expect(draft).toBeVisible();
expect(draft).toHaveValue("保留输入");
expect(screen.queryByRole("progressbar", { name: "正在切换页面" })).not.toBeInTheDocument();
});
it("does not dismiss a newer destination when the previous link settles", () => {
render(<NavigationFeedbackProvider><Harness /></NavigationFeedbackProvider>);
fireEvent.click(screen.getByText("清洗"));
fireEvent.click(screen.getByText("审计"));
fireEvent.click(screen.getByText("取消旧导航"));
expect(screen.getByRole("status", { name: "正在加载审计日志" })).toBeVisible();
expect(screen.getByRole("progressbar", { name: "正在切换页面" })).toBeVisible();
fireEvent.click(screen.getByText("结束导航"));
expect(screen.queryByRole("status", { name: "正在加载审计日志" })).not.toBeInTheDocument();
});
it("hands off to route/component loading boundaries as soon as history commits", () => {
const { rerender } = render(<NavigationFeedbackProvider><Harness /></NavigationFeedbackProvider>);
fireEvent.click(screen.getByText("清洗"));
expect(screen.getByRole("progressbar", { name: "正在切换页面" })).toBeVisible();
mockPathname = "/scada-data-cleaning";
rerender(<NavigationFeedbackProvider><Harness /></NavigationFeedbackProvider>);
expect(screen.queryByRole("progressbar", { name: "正在切换页面" })).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "页面草稿" })).toBeVisible();
});
it("connects every existing map skeleton to its exact public route", () => {
const paths = [
"/network-simulation", "/scada-data-cleaning", "/health-risk-analysis", "/monitoring-place-optimization",
...["burst-detection", "burst-location", "burst-simulation", "contaminant-simulation", "dma-leak-detection", "flushing-analysis"].map((name) => "/hydraulic-simulation/" + name),
];
expect(paths.map(mapSkeletonForPath).sort()).toEqual([...MAP_SKELETON_VARIANTS].sort());
expect(mapSkeletonForPath("/system-admin")).toBeUndefined();
});
});