Files
workinf_Blender_Wasm/web/tests/e2e/main-datablocks-reader-production.spec.ts
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

162 lines
9.1 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const root = path.resolve(import.meta.dirname, "../../..");
const fixturePaths = [
"tests/files/web/basic_scene.blend",
"tests/files/web/attribute_scene.blend",
"tests/files/web/nonmesh_scene.blend",
"tests/files/web/animation_scene.blend",
"tests/files/web/pose_constraint_scene.blend",
"tests/files/web/sequencer_scene.blend",
"tests/files/web/generated/M16-GAP-00025-datablock-Texture.blend",
"tests/files/web/generated/M16-GAP-00006-datablock-Curve.blend",
"tests/files/web/generated/M16-GAP-00010-datablock-Lattice.blend",
"tests/files/web/generated/M16-GAP-00026-datablock-Volume.blend",
"tests/files/web/generated/M16-GAP-00022-datablock-Sound.blend",
"tests/files/web/generated/M16-GAP-00001-datablock-Action.blend",
"tests/files/web/generated/M16-GAP-00002-datablock-Armature.blend",
"tests/files/web/generated/M16-GAP-00009-datablock-GreasePencil.blend",
"tests/files/web/generated/M16-GAP-00015-datablock-MetaBall.blend",
"tests/files/web/generated/M16-GAP-00018-datablock-ParticleSettings.blend",
"tests/files/web/generated/M16-GAP-00019-datablock-PointCloud.blend",
"tests/files/web/generated/M16-GAP-00023-datablock-Speaker.blend",
"tests/files/web/generated/M16-GAP-00024-datablock-Text.blend",
"tests/files/web/generated/M16-GAP-00028-datablock-WorkSpace.blend",
"tests/files/web/generated/M16-GAP-00011-datablock-Library.blend",
"tests/files/web/generated/M16-GAP-00003-datablock-Brush.blend",
];
const fixtures = fixturePaths.map((relativePath) => ({
path: relativePath,
bytes: fs.readFileSync(path.join(root, relativePath)),
}));
const digest = (bytes: Uint8Array) => crypto.createHash("sha256").update(bytes).digest("hex");
test("WBV2 source reader exposes Blender Main datablock collections without JS simulation", async ({ page }) => {
test.setTimeout(180_000);
await page.goto("/");
const result = await page.evaluate(async (inputs) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const collectionFields = [
"cameras", "collections", "materials", "meshes", "nodes", "scenes", "lights", "worlds", "images",
"textures", "volumes", "lattices", "sounds", "animations", "armatures", "nonMeshData", "greasePencils",
"speakers", "texts", "particleSettings", "libraries", "workspaces", "brushes",
] as const;
const summarize = (snapshot: any) => Object.fromEntries(collectionFields.map((field) => {
const values = Array.isArray(snapshot[field]) ? snapshot[field] : [];
return [field, {
count: values.length,
names: values.map((item: any) => item.name).filter((name: any): name is string => typeof name === "string").sort(),
types: field === "nonMeshData" ? Object.fromEntries(values.reduce((counts: Map<string, number>, item: any) => {
if (typeof item.type === "string") counts.set(item.type, (counts.get(item.type) ?? 0) + 1);
return counts;
}, new Map<string, number>())) : undefined,
}];
}));
const summarizeCollections = (snapshot: any) => (snapshot.collections ?? []).map((collection: any) => ({
id: collection.id,
name: collection.name,
objectIds: (collection.objectIds ?? []).slice().sort(),
childCollectionIds: (collection.childCollectionIds ?? []).slice().sort(),
})).sort((a: any, b: any) => a.id.localeCompare(b.id));
const receipts: any[] = [];
for (const input of inputs) {
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const opened = await client.openBlend(Uint8Array.from(input.bytes).buffer);
const summary = summarize(opened.snapshot);
const saved = await client.saveBlend();
const reopened = await reopenedClient.openBlend(saved.slice(0));
const reopenedSummary = summarize(reopened.snapshot);
receipts.push({
path: input.path,
fixtureSha256: await crypto.subtle.digest("SHA-256", Uint8Array.from(input.bytes)).then((hash) => Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("")),
openedRevision: opened.snapshot.revision,
reopenedRevision: reopened.snapshot.revision,
summary,
reopenedSummary,
collections: summarizeCollections(opened.snapshot),
reopenedCollections: summarizeCollections(reopened.snapshot),
sceneObjectIds: (opened.snapshot.nodes ?? []).map((node: any) => node.id).sort(),
reopenedSceneObjectIds: (reopened.snapshot.nodes ?? []).map((node: any) => node.id).sort(),
});
}
finally {
client.terminate();
reopenedClient.terminate();
}
}
const totals = Object.fromEntries(collectionFields.map((field) => [field, receipts.reduce((max, receipt) => Math.max(max, receipt.summary[field].count), 0)]));
const nonMeshTypes = Object.fromEntries(receipts.flatMap((receipt) => Object.entries(receipt.summary.nonMeshData.types ?? {})).reduce((counts: Map<string, number>, [type, count]) => {
counts.set(type, Math.max(counts.get(type) ?? 0, count as number));
return counts;
}, new Map<string, number>()));
return { browser: "chromium", receipts, totals, nonMeshTypes };
}, fixtures.map((fixture) => ({ path: fixture.path, bytes: Array.from(fixture.bytes) })));
fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1100-002"), { recursive: true });
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1100-002/main-datablocks-reader-production.json"), `${JSON.stringify({
schemaVersion: 1,
task: "WBV2-P1100-002",
operation: "BLEND_DATA_MAIN_READER_CHROMIUM",
browser: "chromium",
queueMutation: false,
sourceRnaStructs: {
"rna-struct:BlendDataCameras": "snapshot.cameras",
"rna-struct:BlendDataCollections": "snapshot.collections",
"rna-struct:BlendDataCurves": "snapshot.nonMeshData[type=CURVE]",
"rna-struct:BlendDataFonts": "snapshot.nonMeshData[type=FONT]",
"rna-struct:BlendDataImages": "snapshot.images",
"rna-struct:BlendDataLattices": "snapshot.lattices",
"rna-struct:BlendDataLights": "snapshot.lights",
"rna-struct:BlendDataMaterials": "snapshot.materials",
"rna-struct:BlendDataMeshes": "snapshot.meshes",
"rna-struct:BlendDataObjects": "snapshot.nodes",
"rna-struct:BlendDataScenes": "snapshot.scenes",
"rna-struct:BlendDataSounds": "snapshot.sounds",
"rna-struct:BlendDataTextures": "snapshot.textures",
"rna-struct:BlendDataVolumes": "snapshot.nonMeshData[type=VOLUME]",
"rna-struct:BlendDataWorlds": "snapshot.worlds",
"rna-struct:BlendDataNodeTrees": "snapshot.materials[].nodes + snapshot.scenes[].compositorGraph",
"rna-struct:BlendDataActions": "snapshot.animations",
"rna-struct:BlendDataArmatures": "snapshot.armatures",
"rna-struct:BlendDataBrushes": "snapshot.brushes",
"rna-struct:BlendDataGreasePencilsV3": "snapshot.greasePencils",
"rna-struct:BlendDataMetaBalls": "snapshot.nonMeshData[type=METABALL]",
"rna-struct:BlendDataPointClouds": "snapshot.nonMeshData[type=POINT_CLOUD]",
"rna-struct:BlendDataSpeakers": "snapshot.speakers",
"rna-struct:BlendDataTexts": "snapshot.texts",
"rna-struct:BlendDataParticles": "snapshot.particleSettings",
"rna-struct:BlendDataLibraries": "snapshot.libraries",
"rna-struct:BlendDataWorkSpaces": "snapshot.workspaces",
"rna-struct:BlendDataHairCurves": "snapshot.nonMeshData[type=HAIR|CURVES]",
"rna-struct:CollectionChildren": "snapshot.collections[].childCollectionIds",
"rna-struct:CollectionChild": "snapshot.collections[].childCollectionIds[]",
"rna-struct:SceneObjects": "snapshot.nodes",
},
...result,
}, null, 2)}\n`);
expect(result.browser).toBe("chromium");
expect(result.receipts).toHaveLength(fixturePaths.length);
for (const receipt of result.receipts) {
expect(receipt.fixtureSha256).toMatch(/^[a-f0-9]{64}$/);
expect(receipt.openedRevision).toBe(1);
expect(receipt.reopenedRevision).toBe(1);
expect(receipt.summary).toEqual(receipt.reopenedSummary);
expect(receipt.collections).toEqual(receipt.reopenedCollections);
expect(receipt.sceneObjectIds).toEqual(receipt.reopenedSceneObjectIds);
}
for (const field of ["cameras", "collections", "materials", "meshes", "nodes", "scenes", "lights", "worlds", "images", "textures", "lattices", "sounds", "animations", "armatures", "nonMeshData", "greasePencils", "speakers", "texts", "particleSettings", "libraries", "workspaces", "brushes"]) {
expect(result.totals[field]).toBeGreaterThan(0);
}
expect(result.nonMeshTypes.CURVE).toBeGreaterThan(0);
expect(result.nonMeshTypes.FONT).toBeGreaterThan(0);
expect(result.nonMeshTypes.VOLUME).toBeGreaterThan(0);
expect(result.receipts.some((receipt: any) => receipt.collections.some((collection: any) => collection.objectIds.length > 0 || collection.childCollectionIds.length > 0))).toBe(true);
expect(result.receipts.some((receipt: any) => receipt.sceneObjectIds.length > 0)).toBe(true);
});