Files
workinf_Blender_Wasm/web/tests/e2e/shader-main-command-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

207 lines
10 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 fixturePath = path.join(root, "tests/files/web/m12_glb_main_v1/pbr.blend");
const fixture = fs.readFileSync(fixturePath);
const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex");
const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+W7j8WQAAAABJRU5ErkJggg==";
test("WBV2-P1113-000 writes Shader Graph through native Blender Main cmd json", async ({ page }) => {
test.setTimeout(120_000);
await page.goto("/");
const result = await page.evaluate(async ({ bytes, pngBase64: inputPngBase64 }) => {
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
const digest = async (value: ArrayBuffer) => {
const hash = await crypto.subtle.digest("SHA-256", value);
return Array.from(new Uint8Array(hash), (item) => item.toString(16).padStart(2, "0")).join("");
};
const summarize = (snapshot: any, materialId: string) => {
const material = snapshot.materials.find((candidate: any) => candidate.id === materialId);
if (!material) throw new Error(`material not found: ${materialId}`);
const nodeNames = new Map((material.nodes ?? []).map((node: any) => [node.id, node.name]));
return {
id: material.id,
nodes: (material.nodes ?? []).map((node: any) => ({ type: node.type, name: node.name })).sort((a: any, b: any) => `${a.type}:${a.name}`.localeCompare(`${b.type}:${b.name}`)),
links: (material.links ?? []).map((link: any) => ({
fromNode: nodeNames.get(link.fromNodeId) ?? link.fromNodeId,
fromSocketId: link.fromSocketId,
toNode: nodeNames.get(link.toNodeId) ?? link.toNodeId,
toSocketId: link.toSocketId,
})).sort((a: any, b: any) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
};
};
const client = new WebEngineClient({ timeoutMs: 60_000 });
const reopenedClient = new WebEngineClient({ timeoutMs: 60_000 });
try {
const opened = await client.openBlend(Uint8Array.from(bytes).buffer);
const material = opened.snapshot.materials[0];
if (!material) throw new Error("fixture has no material");
await client.callJson("applyCommand", {
command: { type: "importImage", name: "WBV2 Shader Normal", mimeType: "image/png", width: 1, height: 1, base64: inputPngBase64 },
});
const importedSnapshot = await client.snapshot();
const importedImage = importedSnapshot.snapshot.images.find((image: any) => image.name === "WBV2 Shader Normal");
if (!importedImage) throw new Error("native Blender Main image import did not return an image");
const graph = {
schemaVersion: 1,
id: "shader:wbv2-p1113-native",
materialId: material.id,
outputNodeId: "output",
nodes: [
{
id: "rgb",
type: "RGB",
name: "WBV2 RGB",
sockets: [{ id: "color", name: "Color", direction: "OUTPUT", dataType: "COLOR", defaultValue: [0.17, 0.27, 0.37, 1] }],
},
{
id: "valueA",
type: "VALUE",
name: "WBV2 Value A",
sockets: [{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE", defaultValue: 0.2 }],
},
{
id: "valueB",
type: "VALUE",
name: "WBV2 Value B",
sockets: [{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE", defaultValue: 0.22 }],
},
{
id: "math",
type: "MATH",
name: "WBV2 Add",
properties: { operation: "ADD" },
sockets: [
{ id: "a", name: "Value", direction: "INPUT", dataType: "VALUE", defaultValue: 0 },
{ id: "b", name: "Value_001", direction: "INPUT", dataType: "VALUE", defaultValue: 0 },
{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE" },
],
},
{
id: "image",
type: "IMAGE_TEXTURE",
name: "WBV2 Image Texture",
imageId: importedImage.id,
sockets: [
{ id: "color", name: "Color", direction: "OUTPUT", dataType: "COLOR" },
],
},
{
id: "normal",
type: "NORMAL_MAP",
name: "WBV2 Normal Map",
sockets: [
{ id: "color", name: "Color", direction: "INPUT", dataType: "COLOR" },
{ id: "normal", name: "Normal", direction: "OUTPUT", dataType: "VECTOR" },
],
},
{
id: "principled",
type: "PRINCIPLED",
name: "WBV2 Principled",
sockets: [
{ id: "base", name: "Base Color", direction: "INPUT", dataType: "COLOR", defaultValue: [0.17, 0.27, 0.37, 1] },
{ id: "roughness", name: "Roughness", direction: "INPUT", dataType: "VALUE", defaultValue: 0.42 },
{ id: "normal", name: "Normal", direction: "INPUT", dataType: "VECTOR" },
{ id: "bsdf", name: "BSDF", direction: "OUTPUT", dataType: "SHADER" },
],
},
{
id: "output",
type: "MATERIAL_OUTPUT",
name: "WBV2 Output",
sockets: [{ id: "surface", name: "Surface", direction: "INPUT", dataType: "SHADER" }],
},
],
links: [
{ fromNodeId: "rgb", fromSocketId: "color", toNodeId: "principled", toSocketId: "base" },
{ fromNodeId: "valueA", fromSocketId: "value", toNodeId: "math", toSocketId: "a" },
{ fromNodeId: "valueB", fromSocketId: "value", toNodeId: "math", toSocketId: "b" },
{ fromNodeId: "math", fromSocketId: "value", toNodeId: "principled", toSocketId: "roughness" },
{ fromNodeId: "image", fromSocketId: "color", toNodeId: "normal", toSocketId: "color" },
{ fromNodeId: "normal", fromSocketId: "normal", toNodeId: "principled", toSocketId: "normal" },
{ fromNodeId: "principled", fromSocketId: "bsdf", toNodeId: "output", toSocketId: "surface" },
],
};
const baseRevision = importedSnapshot.snapshot.revision;
const applied = await client.callJson<{ status: string }>("applyCommand", {
command: { type: "setShaderGraph", materialId: material.id, graph },
});
const after = await client.snapshot();
const afterGraph = summarize(after.snapshot, material.id);
const undo = await client.callJson<{ status: string }>("undo");
const undoSnapshot = await client.snapshot();
const redo = await client.callJson<{ status: string }>("redo");
const redoSnapshot = await client.snapshot();
const malformed = await client.callJson("applyCommand", {
command: { type: "setShaderGraph", materialId: material.id, graph: { ...graph, nodes: [{ ...graph.nodes[0], type: "UNSUPPORTED" }] } },
}).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message }));
const afterMalformed = await client.snapshot();
const saved = await client.saveBlend();
const reopened = await reopenedClient.openBlend(saved.slice(0));
return {
browser: "chromium",
fixtureSha256: await digest(Uint8Array.from(bytes).buffer),
materialId: material.id,
baseRevision,
appliedStatus: applied.result.status,
afterGraph,
undoStatus: undo.result.status,
undoRevision: undoSnapshot.snapshot.revision,
redoStatus: redo.result.status,
redoRevision: redoSnapshot.snapshot.revision,
malformed,
malformedRevisionPreserved: afterMalformed.snapshot.revision === redoSnapshot.snapshot.revision,
reopenedGraph: summarize(reopened.snapshot, material.id),
reopenedRevision: reopened.snapshot.revision,
};
}
catch (error: any) {
return { error: { code: error?.code, message: error?.message, detail: String(error) } };
}
finally {
client.terminate();
reopenedClient.terminate();
}
}, { bytes: Array.from(fixture), pngBase64 });
fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1113-000"), { recursive: true });
fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1113-000/shader-main-command-production.json"), `${JSON.stringify({
schemaVersion: 1,
task: "WBV2-P1113-000",
operation: "SHADER_GRAPH_MAIN_CMD_JSON_CHROMIUM",
browser: "chromium",
queueMutation: false,
sourceCommand: "web_engine_blend_main_set_shader_graph",
sourcePath: "blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
sourceNodes: {
"node:ShaderNodeRGB": "material.nodes[type=RGB]",
"node:ShaderNodeValue": "material.nodes[type=VALUE]",
"node:ShaderNodeMath": "material.nodes[type=MATH]",
"node:ShaderNodeTexImage": "material.nodes[type=IMAGE_TEXTURE]",
"node:ShaderNodeNormalMap": "material.nodes[type=NORMAL_MAP]",
"node:ShaderNodeBsdfPrincipled": "material.nodes[type=PRINCIPLED]",
"node:ShaderNodeOutputMaterial": "material.nodes[type=OUTPUT]",
},
jsonAbi: { command: "applyCommand", stateful: true, nativeOnly: true },
...result,
}, null, 2)}\n`);
expect(result.browser).toBe("chromium");
expect(result.fixtureSha256).toBe(fixtureSha256);
expect(result.appliedStatus).toBe("applied");
expect(result.afterGraph.nodes.map((node: any) => node.type)).toEqual(expect.arrayContaining(["RGB", "VALUE", "MATH", "PRINCIPLED", "OUTPUT"]));
expect(result.afterGraph.nodes.map((node: any) => node.type)).toEqual(expect.arrayContaining(["RGB", "VALUE", "MATH", "IMAGE_TEXTURE", "NORMAL_MAP", "PRINCIPLED", "OUTPUT"]));
expect(result.afterGraph.links).toHaveLength(7);
expect(result.undoStatus).toBe("undo");
expect(result.redoStatus).toBe("redo");
expect(result.undoRevision).toBe(result.baseRevision + 2);
expect(result.redoRevision).toBe(result.baseRevision + 3);
expect(result.malformed.code).not.toBe("NO_ERROR");
expect(result.malformedRevisionPreserved).toBe(true);
expect(result.reopenedGraph).toEqual(result.afterGraph);
expect(result.reopenedRevision).toBe(1);
});