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