feat(frontend): cache 3D assets and restore loading feedback
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:
@@ -0,0 +1,96 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
import { mockBackend } from "./support/mockBackend";
|
||||
|
||||
test.setTimeout(90_000);
|
||||
|
||||
async function prepare(page: Page) {
|
||||
await mockBackend(page);
|
||||
await page.route("**/api/v1/admin/**", (route) => route.fulfill({
|
||||
json: route.request().url().endsWith("/users/me")
|
||||
? { id: "playwright-user", is_system_admin: true, system_role: "system_admin" }
|
||||
: [],
|
||||
}));
|
||||
}
|
||||
|
||||
test("菜单点击后立即反馈,等待页面响应时显示路由骨架", async ({ page }) => {
|
||||
await prepare(page);
|
||||
await page.goto("/system-admin");
|
||||
await expect(page.getByRole("link", { name: "审计日志", exact: true })).toBeVisible();
|
||||
await page.locator("[data-page-scroll-content]").evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||
let release: () => void = () => {};
|
||||
const gate = new Promise<void>((resolve) => { release = resolve; });
|
||||
let waiting = false;
|
||||
await page.route("**/audit-logs?*", async (route) => {
|
||||
if (route.request().headers().rsc !== "1") return route.fallback();
|
||||
waiting = true;
|
||||
await gate;
|
||||
await route.continue();
|
||||
});
|
||||
try {
|
||||
await page.getByRole("link", { name: "审计日志", exact: true }).click();
|
||||
await expect.poll(() => waiting).toBe(true);
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toBeVisible({ timeout: 1500 });
|
||||
await expect(page.getByRole("status", { name: "正在加载审计日志", exact: true })).toBeVisible();
|
||||
} finally { release(); }
|
||||
await expect(page.getByRole("heading", { name: "审计日志", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("地图页面切换复用原地图,并显示目标面板的既有骨架", async ({ page }) => {
|
||||
await prepare(page);
|
||||
await page.route("**/ows?**", (route) => route.fulfill({ json: { type: "FeatureCollection", features: [] } }));
|
||||
await page.route("**/api/v1/scada-devices", (route) => route.fulfill({ json: [] }));
|
||||
await page.goto("/network-simulation");
|
||||
const map = page.locator(".ol-viewport").first();
|
||||
await expect(map).toBeVisible({ timeout: 60_000 });
|
||||
await map.evaluate((element) => element.setAttribute("data-navigation-map", "original"));
|
||||
let release: () => void = () => {};
|
||||
const gate = new Promise<void>((resolve) => { release = resolve; });
|
||||
await page.route("**/scada-data-cleaning?*", async (route) => {
|
||||
if (route.request().headers().rsc !== "1") return route.fallback();
|
||||
await gate;
|
||||
await route.continue();
|
||||
});
|
||||
try {
|
||||
await page.getByRole("link", { name: "SCADA 数据清洗", exact: true }).click();
|
||||
await expect(page.getByRole("status", { name: "正在加载数据清洗", exact: true })).toBeVisible();
|
||||
await expect(page.locator('[data-navigation-map="original"]')).toBeVisible();
|
||||
await expect(page.locator("[data-map-page-content]")).toHaveCSS("display", "none");
|
||||
await page.screenshot({ path: "test-results/navigation-map-skeleton.png" });
|
||||
} finally { release(); }
|
||||
await expect(page).toHaveURL(/\/scada-data-cleaning$/);
|
||||
await expect(page.locator("[data-map-page-content]")).toHaveCSS("display", "contents");
|
||||
await expect(page.locator('[data-navigation-map="original"]')).toBeVisible();
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toHaveCount(0);
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/network-simulation$/);
|
||||
await expect(page.locator('[data-navigation-map="original"]')).toBeVisible();
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("移动端收起菜单后仍显示页面切换反馈,窄屏无溢出", async ({ page }) => {
|
||||
await prepare(page);
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.goto("/system-admin");
|
||||
await page.getByRole("button", { name: "open drawer", exact: true }).click();
|
||||
let release: () => void = () => {};
|
||||
const gate = new Promise<void>((resolve) => { release = resolve; });
|
||||
await page.route("**/audit-logs?*", async (route) => {
|
||||
if (route.request().headers().rsc !== "1") return route.fallback();
|
||||
await gate;
|
||||
await route.continue();
|
||||
});
|
||||
try {
|
||||
await page.getByRole("link", { name: "审计日志", exact: true }).click();
|
||||
for (const width of [375, 320]) {
|
||||
await page.setViewportSize({ width, height: 812 });
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("status", { name: "正在加载审计日志", exact: true })).toBeVisible();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
await page.screenshot({ path: `test-results/navigation-mobile-${width}.png` });
|
||||
}
|
||||
} finally { release(); }
|
||||
await expect(page.getByRole("heading", { name: "审计日志", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("progressbar", { name: "正在切换页面", exact: true })).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user