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.
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const sceneRoot = join(
|
|
process.cwd(),
|
|
"public",
|
|
"three-dimensional",
|
|
"zjb",
|
|
"v29",
|
|
);
|
|
|
|
describe("ZJB three-dimensional runtime assets", () => {
|
|
it("ships every model referenced by the production manifest", () => {
|
|
const manifest = JSON.parse(
|
|
readFileSync(join(sceneRoot, "manifest.json"), "utf8"),
|
|
) as {
|
|
packageId: string;
|
|
renderRevision: number;
|
|
assets: Array<{ file: string; sha256: string }>;
|
|
networkContext: string[];
|
|
networkModel: { file: string; metadata: string; sha256: string; metadataSha256: string };
|
|
sourceCatalogFile?: string;
|
|
bindingsFile?: string;
|
|
};
|
|
|
|
expect(manifest.packageId).toBe("zjb-web-v29");
|
|
expect(manifest.renderRevision).toBe(29);
|
|
expect(manifest.assets).toHaveLength(24);
|
|
expect(manifest.networkContext).toEqual([
|
|
"detail_roof",
|
|
"detail_facade",
|
|
"detail_interior",
|
|
"detail_ceiling",
|
|
"map_platforms",
|
|
"map_railway",
|
|
"map_roads",
|
|
"map_pump_shell",
|
|
]);
|
|
manifest.assets.forEach((asset) => {
|
|
const assetPath = join(sceneRoot, asset.file);
|
|
expect(existsSync(assetPath)).toBe(true);
|
|
expect(createHash("sha256").update(readFileSync(assetPath)).digest("hex")).toBe(
|
|
asset.sha256,
|
|
);
|
|
});
|
|
expect(existsSync(join(sceneRoot, manifest.networkModel.file))).toBe(true);
|
|
expect(existsSync(join(sceneRoot, manifest.networkModel.metadata))).toBe(true);
|
|
expect(createHash("sha256").update(
|
|
readFileSync(join(sceneRoot, manifest.networkModel.metadata)),
|
|
).digest("hex")).toBe(manifest.networkModel.metadataSha256);
|
|
expect(manifest.sourceCatalogFile).toBeUndefined();
|
|
expect(manifest.bindingsFile).toBeUndefined();
|
|
|
|
const networkModel = readFileSync(join(sceneRoot, manifest.networkModel.file));
|
|
expect(createHash("sha256").update(networkModel).digest("hex")).toBe(
|
|
manifest.networkModel.sha256,
|
|
);
|
|
});
|
|
|
|
it("includes the offline runtime modules and third-party licenses", () => {
|
|
[
|
|
"preview.html",
|
|
"preview.mjs",
|
|
"appearance.mjs",
|
|
"asset-inspector.mjs",
|
|
"camera-navigation.mjs",
|
|
"network-style.mjs",
|
|
"integration.mjs",
|
|
"render-effects.mjs",
|
|
"resourceCache.mjs",
|
|
"loadingProgress.mjs",
|
|
"vendor/meshopt_decoder.module.js",
|
|
"vendor/meshoptimizer-LICENSE.md",
|
|
"vendor/three/LICENSE",
|
|
"vendor/three/three.module.js",
|
|
"vendor/three/addons/loaders/GLTFLoader.js",
|
|
].forEach((relativePath) => {
|
|
expect(existsSync(join(sceneRoot, relativePath))).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("keeps the embedded viewer on the versioned same-origin host protocol", () => {
|
|
const preview = readFileSync(join(sceneRoot, "preview.mjs"), "utf8");
|
|
const html = readFileSync(join(sceneRoot, "preview.html"), "utf8");
|
|
const inspector = readFileSync(
|
|
join(sceneRoot, "asset-inspector.mjs"),
|
|
"utf8",
|
|
);
|
|
const networkStyle = readFileSync(
|
|
join(sceneRoot, "network-style.mjs"),
|
|
"utf8",
|
|
);
|
|
|
|
expect(preview).toContain("tjwater:zjb-scene");
|
|
expect(preview).toContain("HOST_VERSION=2");
|
|
expect(preview).toContain("event.origin===window.location.origin");
|
|
expect(preview).toContain("event.source===window.parent");
|
|
expect(preview).toContain("postHost('ready'");
|
|
expect(preview).toContain("data.type==='results'");
|
|
expect(preview).toContain("data.type==='clear-results'");
|
|
expect(preview).toContain("data.type!=='command'");
|
|
expect(preview).toContain("postHost('selection-changed'");
|
|
expect(preview).toContain("next==='network'?manifest.networkContext:manifest.mapDefault");
|
|
expect(preview).not.toContain("window.zjbNetwork");
|
|
expect(preview).not.toContain("resultsFile");
|
|
expect(html).not.toContain("绑定运行结果");
|
|
expect(html).not.toContain("resultsFile");
|
|
expect(inspector).not.toContain("document.getElementById");
|
|
expect(networkStyle).toContain("ScadaSensorMarkers");
|
|
expect(networkStyle).toContain("source==='scada'");
|
|
});
|
|
});
|