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>
207 lines
10 KiB
TypeScript
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);
|
|
});
|