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 fixtures = [ "modifier_generate_scene.blend", "modifier_deform_scene.blend", "modifier_deform_curve_scene.blend", "modifier_extended_native.blend", "modifier_physics_scene.blend", "modifier_geometry_nodes_scene.blend", "rigged_shape_scene.blend", ]; const sourceModules = { "modifier-source:MOD_array": "ARRAY", "modifier-source:MOD_armature": "ARMATURE", "modifier-source:MOD_bevel": "BEVEL", "modifier-source:MOD_boolean": "BOOLEAN", "modifier-source:MOD_build": "BUILD", "modifier-source:MOD_cast": "CAST", "modifier-source:MOD_cast_web": "CAST", "modifier-source:MOD_cloth": "CLOTH", "modifier-source:MOD_collision": "COLLISION", "modifier-source:MOD_curve": "CURVE", "modifier-source:MOD_displace": "DISPLACE", "modifier-source:MOD_displace_web": "DISPLACE", "modifier-source:MOD_edgesplit": "EDGE_SPLIT", "modifier-source:MOD_hook": "HOOK", "modifier-source:MOD_lattice": "LATTICE", "modifier-source:MOD_meshdeform": "MESH_DEFORM", "modifier-source:MOD_mirror": "MIRROR", "modifier-source:MOD_nodes": "NODES", "modifier-source:MOD_nodes_web": "NODES", "modifier-source:MOD_screw": "SCREW", "modifier-source:MOD_solidify": "SOLIDIFY", "modifier-source:MOD_simpledeform": "SIMPLE_DEFORM", "modifier-source:MOD_smooth": "SMOOTH", "modifier-source:MOD_softbody": "SOFT_BODY", "modifier-source:MOD_subsurf": "SUBSURF", "modifier-source:MOD_triangulate": "TRIANGULATE", "modifier-source:MOD_surfacedeform": "SURFACE_DEFORM", "modifier-source:MOD_wave": "WAVE", "modifier-source:MOD_weld": "WELD", "modifier-source:MOD_wireframe": "WIREFRAME", }; const modifierRnaStructs = { "rna-struct:ArrayModifier": "ARRAY", "rna-struct:ArmatureModifier": "ARMATURE", "rna-struct:BevelModifier": "BEVEL", "rna-struct:BooleanModifier": "BOOLEAN", "rna-struct:BuildModifier": "BUILD", "rna-struct:CastModifier": "CAST", "rna-struct:ClothModifier": "CLOTH", "rna-struct:CollisionModifier": "COLLISION", "rna-struct:CurveModifier": "CURVE", "rna-struct:DisplaceModifier": "DISPLACE", "rna-struct:EdgeSplitModifier": "EDGE_SPLIT", "rna-struct:HookModifier": "HOOK", "rna-struct:LatticeModifier": "LATTICE", "rna-struct:MeshDeformModifier": "MESH_DEFORM", "rna-struct:MirrorModifier": "MIRROR", "rna-struct:NodesModifier": "NODES", "rna-struct:ScrewModifier": "SCREW", "rna-struct:SolidifyModifier": "SOLIDIFY", "rna-struct:SimpleDeformModifier": "SIMPLE_DEFORM", "rna-struct:SmoothModifier": "SMOOTH", "rna-struct:SoftBodyModifier": "SOFT_BODY", "rna-struct:SubsurfModifier": "SUBSURF", "rna-struct:SurfaceDeformModifier": "SURFACE_DEFORM", "rna-struct:TriangulateModifier": "TRIANGULATE", "rna-struct:WaveModifier": "WAVE", "rna-struct:WeldModifier": "WELD", "rna-struct:WireframeModifier": "WIREFRAME", }; const fixtureBytes = Object.fromEntries(fixtures.map((name) => [name, fs.readFileSync(path.join(root, "tests/files/web", name))])); const fixtureSha256 = Object.fromEntries(Object.entries(fixtureBytes).map(([name, bytes]) => [name, crypto.createHash("sha256").update(bytes).digest("hex")])); test("WBV2 modifier source modules preserve concrete Blender DNA through Chromium save/reopen", async ({ page }) => { test.setTimeout(180_000); await page.goto("/"); const result = await page.evaluate(async (input) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const canonical = (snapshot: any) => snapshot.meshes.flatMap((mesh: any) => (mesh.modifierStack ?? []).map((modifier: any) => ({ meshId: mesh.id, uuid: modifier.uuid, type: modifier.type, enabled: modifier.enabled, evaluationStatus: modifier.evaluationStatus }))); const output: any = {}; for (const [name, bytes] of Object.entries(input.fixtures as Record)) { 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 before = canonical(opened.snapshot); const target = before[0]; const edited = await client.applyCommand({ type: "setModifierVisibility", meshId: target.meshId, modifierUuid: target.uuid, showViewport: false, showRender: false, showEditMode: true, showOnCage: true } as any); const afterEdit = canonical(edited.snapshot); const saved = await client.saveBlend(); const reopened = await reopenedClient.openBlend(saved.slice(0)); const after = canonical(reopened.snapshot); output[name] = { before, afterEdit, after, exact: JSON.stringify(afterEdit) === JSON.stringify(after), savedSha256: await crypto.subtle.digest("SHA-256", saved).then((hash) => Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("")) }; } catch (error: any) { output[name] = { error: { code: error?.code, message: error?.message, detail: String(error) } }; } finally { client.terminate(); reopenedClient.terminate(); } } return output; }, { fixtures: Object.fromEntries(Object.entries(fixtureBytes).map(([name, bytes]) => [name, Array.from(bytes)])) }); fs.mkdirSync(path.join(root, "tests/golden/WBV2-P1107-000"), { recursive: true }); fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1107-000/modifier-source-roundtrip-production.json"), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P1107-000", operation: "MODIFIER_SOURCE_DNA_ROUNDTRIP_MAIN_CHROMIUM", browser: "chromium", queueMutation: false, sourceModules, modifierRnaStructs, fixtureSha256, ...result }, null, 2)}\n`); const observedTypes = new Set(Object.values(result).flatMap((entry: any) => entry?.afterEdit?.map((modifier: any) => modifier.type) ?? [])); for (const type of new Set(Object.values(sourceModules))) expect(observedTypes.has(type)).toBe(true); for (const type of new Set(Object.values(modifierRnaStructs))) expect(observedTypes.has(type)).toBe(true); for (const name of fixtures) { expect(result[name]?.error).toBeUndefined(); expect(result[name]?.exact).toBe(true); expect(result[name]?.before.length).toBeGreaterThan(0); expect(result[name]?.afterEdit).toEqual(result[name]?.after); } });