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, item: any) => { if (typeof item.type === "string") counts.set(item.type, (counts.get(item.type) ?? 0) + 1); return counts; }, new Map())) : 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, [type, count]) => { counts.set(type, Math.max(counts.get(type) ?? 0, count as number)); return counts; }, new Map())); 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); });