Files
workinf_Blender_Wasm/web/tests/e2e/wbv2-material-animation-production.spec.ts
mes123456 2abe7cc220
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
Advance WBV2 domain parity through P12
2026-08-28 06:38:27 +08:00

86 lines
9.2 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 evidenceRoot = path.join(root, "tests/golden/WBV2-P12-003");
const materialFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P3-001-MATERIAL.PBR_UV.blend"));
const animationFixture = fs.readFileSync(path.join(root, "tests/files/web/generated/WBV2-P3-002-ANIMATION.TRS_ACTION.blend"));
const digest = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex");
test("WBV2-P12-003 executes material and animation mutations through compiled WASM Main", async ({ page }) => {
test.setTimeout(240_000);
await page.goto("/");
const result = await page.evaluate(async (input) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const buffer = (values: number[]): ArrayBuffer => Uint8Array.from(values).buffer;
const digestBytes = async (value: ArrayBuffer): Promise<string> => { const hash = await crypto.subtle.digest("SHA-256", value); return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join(""); };
const shape = (error: any) => ({ code: error?.code, message: error?.message });
const materialClient = new WebEngineClient({ timeoutMs: 60_000 });
const materialReopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
const animationClient = new WebEngineClient({ timeoutMs: 60_000 });
const animationReopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const materialOpened = await materialClient.openBlend(buffer(input.materialBytes));
const materialId = "material:WBV2P3001PrincipledMaterial";
const meshId = "mesh:WBV2P3001MaterialCubeMesh";
const sourceMaterial: any = materialOpened.snapshot.materials.find((candidate: any) => candidate.id === materialId);
const sourceMesh: any = materialOpened.snapshot.meshes.find((candidate: any) => candidate.id === meshId);
if (!sourceMaterial || !sourceMesh) throw new Error("material fixture is incomplete");
const createdUV = await materialClient.applyCommand({ type: "createUVMap", meshId, name: "WBV2P3001SecondaryUV", baseRevision: materialOpened.snapshot.revision } as any);
const activeUV = await materialClient.applyCommand({ type: "setActiveUVMap", meshId, name: "WBV2P3001SecondaryUV", baseRevision: createdUV.snapshot.revision } as any);
const unwrapped = await materialClient.applyCommand({ type: "unwrapUV", meshId, faceIndices: [0, 1, 2, 3, 4, 5], method: "CUBE", baseRevision: activeUV.snapshot.revision } as any);
const editedMaterial = await materialClient.applyCommand({ type: "setMaterialPrincipled", materialId, baseColor: [0.2, 0.4, 0.6, 1], roughness: 0.31, metallic: 0.72, ior: 1.5, alpha: 1, baseRevision: unwrapped.snapshot.revision } as any);
const invalidUV = await materialClient.applyCommand({ type: "setActiveUVMap", meshId, name: "missing-uv", baseRevision: editedMaterial.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const materialAfterFailure = await materialClient.snapshot();
const materialSaved = await materialClient.saveBlend();
const materialSavedSha256 = await digestBytes(materialSaved.slice(0));
const materialReopened = await materialReopenedClient.openBlend(materialSaved.slice(0));
const material = materialReopened.snapshot.materials.find((candidate: any) => candidate.id === materialId);
const materialMesh = materialReopened.snapshot.meshes.find((candidate: any) => candidate.id === meshId);
const animationOpened = await animationClient.openBlend(buffer(input.animationBytes));
const animationObject: any = animationOpened.snapshot.nodes.find((candidate: any) => candidate.id === "object:WBV2P3002AnimatedCube");
const animation: any = animationOpened.snapshot.animations.find((candidate: any) => candidate.targetId === animationObject?.id);
if (!animationObject || !animation) throw new Error("animation fixture is incomplete");
const inserted = await animationClient.applyCommand({ type: "insertObjectKeyframe", objectId: animationObject.id, frame: 25, property: "LOCATION", interpolation: "BEZIER", baseRevision: animationOpened.snapshot.revision } as any);
const interpolated = await animationClient.applyCommand({ type: "setFCurveInterpolation", animationId: animation.id, path: "location[0]", interpolation: "LINEAR", baseRevision: inserted.snapshot.revision } as any);
const staleFrame = await animationClient.applyCommand({ type: "insertObjectKeyframe", objectId: animationObject.id, frame: 30, property: "ROTATION_EULER", interpolation: "BEZIER", baseRevision: animationOpened.snapshot.revision } as any).then(() => ({ code: "NO_ERROR" })).catch(shape);
const animationAfterFailure = await animationClient.snapshot();
const animationSaved = await animationClient.saveBlend();
const animationSavedSha256 = await digestBytes(animationSaved.slice(0));
const animationReopened = await animationReopenedClient.openBlend(animationSaved.slice(0));
const reopenedAnimation: any = animationReopened.snapshot.animations.find((candidate: any) => candidate.id === animation.id);
return {
material: { sourceUnsupportedNode: sourceMaterial.nodes?.some((node: any) => node.type === "UNSUPPORTED") ?? false, baseRevision: materialOpened.snapshot.revision, finalRevision: editedMaterial.snapshot.revision, materialId, meshId, activeUVMap: materialMesh?.activeUVMap, baseColor: material?.baseColor, roughness: material?.roughness, metallic: material?.metallic, invalidUV, revisionAfterFailure: materialAfterFailure.snapshot.revision, savedByteLength: materialSaved.byteLength, sourceSha256: await digestBytes(buffer(input.materialBytes)), saveReopenExact: material?.baseColor?.join(",") === "0.20000000298023224,0.4000000059604645,0.6000000238418579,1" && materialMesh?.activeUVMap === "WBV2P3001SecondaryUV" },
animation: { baseRevision: animationOpened.snapshot.revision, insertedRevision: inserted.snapshot.revision, finalRevision: interpolated.snapshot.revision, animationId: animation.id, channelCount: animation.channels.length, locationFrames: interpolated.snapshot.animations.find((candidate: any) => candidate.id === animation.id)?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.map((keyframe: any) => keyframe.frame), interpolation: interpolated.snapshot.animations.find((candidate: any) => candidate.id === animation.id)?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation, staleFrame, revisionAfterFailure: animationAfterFailure.snapshot.revision, savedByteLength: animationSaved.byteLength, sourceSha256: await digestBytes(buffer(input.animationBytes)), reopenedFrames: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.map((keyframe: any) => keyframe.frame), reopenedInterpolation: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation, saveReopenExact: reopenedAnimation?.channels.find((channel: any) => channel.path === "location[0]")?.keyframes.at(-1)?.interpolation === "LINEAR" },
};
}
finally { materialClient.terminate(); materialReopenedClient.terminate(); animationClient.terminate(); animationReopenedClient.terminate(); }
}, { materialBytes: Array.from(materialFixture), animationBytes: Array.from(animationFixture) });
fs.mkdirSync(evidenceRoot, { recursive: true });
const output = process.env.WBV2_MATERIAL_ANIMATION_REPORT ?? "material-animation-production.json";
fs.writeFileSync(path.join(evidenceRoot, output), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P12-003", operation: "MATERIAL_ANIMATION_ENGINE_CHROMIUM", browser: "chromium", queueMutation: false, ...result }, null, 2)}\n`);
expect(result.material.sourceUnsupportedNode).toBe(true);
expect(result.material.finalRevision).toBe(result.material.baseRevision + 4);
expect(result.material.activeUVMap).toBe("WBV2P3001SecondaryUV");
expect(result.material.baseColor[0]).toBeCloseTo(0.2, 5);
expect(result.material.roughness).toBeCloseTo(0.31, 5);
expect(result.material.metallic).toBeCloseTo(0.72, 5);
expect(result.material.invalidUV.code).not.toBe("NO_ERROR");
expect(result.material.revisionAfterFailure).toBe(result.material.finalRevision);
expect(result.material.saveReopenExact).toBe(true);
expect(result.material.savedSha256).not.toBe(result.material.sourceSha256);
expect(result.animation.insertedRevision).toBe(result.animation.baseRevision + 1);
expect(result.animation.finalRevision).toBe(result.animation.baseRevision + 2);
expect(result.animation.locationFrames).toEqual([1, 10, 20, 25]);
expect(result.animation.interpolation).toBe("LINEAR");
expect(result.animation.staleFrame.code).not.toBe("NO_ERROR");
expect(result.animation.revisionAfterFailure).toBe(result.animation.finalRevision);
expect(result.animation.reopenedFrames).toEqual([1, 10, 20, 25]);
expect(result.animation.reopenedInterpolation).toBe("LINEAR");
expect(result.animation.saveReopenExact).toBe(true);
expect(result.animation.savedSha256).not.toBe(result.animation.sourceSha256);
});