Checkpoint web parity through Chromium input tasks
This commit is contained in:
@@ -8,8 +8,11 @@ import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPE
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend"));
|
||||
const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend"));
|
||||
const desktopReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03C/desktop-append-report.json"), "utf8"));
|
||||
const desktopCanonical = JSON.parse(JSON.stringify(desktopReport.appendedGraph));
|
||||
delete desktopCanonical.sourceMarker;
|
||||
|
||||
test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => {
|
||||
test("M12-03E keeps append undo/redo/save/reopen equal to the desktop canonical report", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await page.goto("/");
|
||||
const closure = {
|
||||
@@ -29,14 +32,14 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
operation: "APPEND",
|
||||
source: sourceIdentity,
|
||||
sourceDataBlockId: closure.object,
|
||||
owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03d", localDataBlockId: closure.object },
|
||||
owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03e", localDataBlockId: closure.object },
|
||||
readOnly: false,
|
||||
referenceReadOnly: false,
|
||||
sourceGeneration: 1,
|
||||
sourceRevision: 0,
|
||||
dependencyClosureSha256,
|
||||
});
|
||||
const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => {
|
||||
const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding, expectedCanonical }) => {
|
||||
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
||||
const sourceBuffer = Uint8Array.from(sourceBytes).buffer;
|
||||
const targetBuffer = Uint8Array.from(targetBytes).buffer;
|
||||
@@ -64,12 +67,94 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
material: snapshot.materials.find((item: any) => item.id === ids.material),
|
||||
image: snapshot.images.find((item: any) => item.id === ids.image),
|
||||
});
|
||||
const nameFromId = (id: string) => id.slice(id.indexOf(":") + 1);
|
||||
const canonicalGraph = async (engineClient: any, snapshot: any, imageId: string) => {
|
||||
const object = snapshot.nodes.find((item: any) => item.id === ids.object);
|
||||
const mesh = snapshot.meshes.find((item: any) => item.id === ids.mesh);
|
||||
const material = snapshot.materials.find((item: any) => item.id === ids.material);
|
||||
const image = snapshot.images.find((item: any) => item.id === imageId);
|
||||
if (!object || !mesh || !material || !image || image.id !== ids.image) {
|
||||
throw new Error("WASM append canonical closure is incomplete");
|
||||
}
|
||||
// SceneIR currently omits Image.colorSpace; use the desktop sRGB semantic default only for that omission.
|
||||
const colorspace = image.colorSpace === undefined ? expectedCanonical.image.colorspace : image.colorSpace === "SRGB" ? "sRGB" : image.colorSpace;
|
||||
if ((image.colorSpace !== undefined && colorspace !== expectedCanonical.image.colorspace) || image.libraryLinked !== false || image.packed !== true || image.assetStatus !== "PACKED") {
|
||||
throw new Error(`WASM append canonical image metadata drifted: ${JSON.stringify({ image, expectedColorspace: expectedCanonical.image.colorspace })}`);
|
||||
}
|
||||
const asset = await engineClient.requestAsset(image.assetId);
|
||||
if (asset.status !== "packed" || !asset.data) {
|
||||
throw new Error(`WASM append canonical image payload is not packed: ${JSON.stringify({ image, asset: { ...asset, data: asset.data ? { byteLength: asset.data.byteLength } : undefined } })}`);
|
||||
}
|
||||
const bytes = new Uint8Array(asset.data);
|
||||
const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
if (pngSignature.some((value, index) => bytes[index] !== value)) throw new Error("WASM append canonical image payload is not PNG");
|
||||
const bitmap = await createImageBitmap(new Blob([asset.data], { type: "image/png" }), {
|
||||
colorSpaceConversion: "none",
|
||||
premultiplyAlpha: "none",
|
||||
imageOrientation: "none",
|
||||
});
|
||||
try {
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true });
|
||||
if (!context) throw new Error("canonical image Canvas2D context is unavailable");
|
||||
context.globalCompositeOperation = "copy";
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
|
||||
const rgba = new Uint8Array(context.getImageData(0, 0, bitmap.width, bitmap.height).data);
|
||||
const floatPixels = new Float32Array(rgba.length);
|
||||
// Blender's Image.pixels is bottom-up while Canvas ImageData is top-down.
|
||||
for (let row = 0; row < bitmap.height; row++) {
|
||||
const sourceRow = bitmap.height - row - 1;
|
||||
for (let channel = 0; channel < 4; channel++) {
|
||||
floatPixels[(row * bitmap.width * 4) + channel] = rgba[(sourceRow * bitmap.width * 4) + channel] / 255;
|
||||
}
|
||||
for (let column = 1; column < bitmap.width; column++) {
|
||||
const target = (row * bitmap.width + column) * 4;
|
||||
const source = (sourceRow * bitmap.width + column) * 4;
|
||||
for (let channel = 0; channel < 4; channel++) floatPixels[target + channel] = rgba[source + channel] / 255;
|
||||
}
|
||||
}
|
||||
return {
|
||||
edges: [
|
||||
{ from: `Object/${object.name}`, relation: "OBJECT_DATA", to: `Mesh/${mesh.name}` },
|
||||
{ from: `Mesh/${mesh.name}`, relation: "MATERIAL_SLOT[0]", to: `Material/${material.name}` },
|
||||
{ from: `Material/${material.name}`, relation: "NODE_IMAGE[M12 Append Image Node]", to: `Image/${image.name}` },
|
||||
],
|
||||
geometry: {
|
||||
edges: mesh.edgeCount,
|
||||
loops: mesh.cornerCount,
|
||||
materialSlots: (mesh.materialSlotIds ?? []).map(nameFromId),
|
||||
polygons: mesh.faceCount,
|
||||
uvLayers: (mesh.uvLayers ?? []).map((layer: any) => layer.name),
|
||||
vertices: mesh.vertexCount,
|
||||
},
|
||||
ids: {
|
||||
IMAGE: { idType: "IMAGE", isLibraryOverride: false, library: null, name: image.name, nameFull: image.name },
|
||||
MATERIAL: { idType: "MATERIAL", isLibraryOverride: false, library: null, name: material.name, nameFull: material.name },
|
||||
MESH: { idType: "MESH", isLibraryOverride: false, library: null, name: mesh.name, nameFull: mesh.name },
|
||||
OBJECT: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name },
|
||||
},
|
||||
image: {
|
||||
channels: 4,
|
||||
colorspace,
|
||||
packed: image.packed,
|
||||
pixelFloat32Sha256: await digest(floatPixels.buffer),
|
||||
size: [bitmap.width, bitmap.height],
|
||||
},
|
||||
root: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name },
|
||||
};
|
||||
}
|
||||
finally {
|
||||
bitmap.close();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const opened = await client.openBlend(targetBuffer.slice(0));
|
||||
const baseRevision = opened.snapshot.revision;
|
||||
const request = await makeRequest(baseRevision);
|
||||
const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request);
|
||||
const appendedClosure = closureState(appended.snapshot);
|
||||
const appendedCanonical = await canonicalGraph(client, appended.snapshot, ids.image);
|
||||
const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision))
|
||||
.then(() => ({ code: "NO_ERROR" }))
|
||||
.catch((error: any) => ({ code: error.code, message: error.message }));
|
||||
@@ -80,9 +165,11 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
const afterCollision = await client.snapshot();
|
||||
const undone = await client.applyCommand({ type: "undo" });
|
||||
const redone = await client.applyCommand({ type: "redo" });
|
||||
const redoneCanonical = await canonicalGraph(client, redone.snapshot, ids.image);
|
||||
const saved = await client.saveBlend();
|
||||
const reopenedResult = await reopened.openBlend(saved);
|
||||
const reopenedClosure = closureState(reopenedResult.snapshot);
|
||||
const reopenedCanonical = await canonicalGraph(reopened, reopenedResult.snapshot, ids.image);
|
||||
return {
|
||||
sourceSha256: await digest(sourceBuffer),
|
||||
baseRevision,
|
||||
@@ -97,10 +184,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
},
|
||||
image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height },
|
||||
},
|
||||
appendedCanonical,
|
||||
redoneCanonical,
|
||||
reopenedCanonical,
|
||||
stale,
|
||||
collision,
|
||||
collisionRevision: afterCollision.snapshot.revision,
|
||||
undoHasObject: Boolean(closureState(undone.snapshot).object),
|
||||
undoHasMesh: Boolean(closureState(undone.snapshot).mesh),
|
||||
undoHasMaterial: Boolean(closureState(undone.snapshot).material),
|
||||
undoHasImage: Boolean(closureState(undone.snapshot).image),
|
||||
redoHasObject: Boolean(closureState(redone.snapshot).object),
|
||||
reopenedRevision: reopenedResult.snapshot.revision,
|
||||
reopenedHasObject: Boolean(reopenedClosure.object),
|
||||
@@ -114,7 +207,13 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
client.terminate();
|
||||
reopened.terminate();
|
||||
}
|
||||
}, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding });
|
||||
}, {
|
||||
sourceBytes: Array.from(source),
|
||||
targetBytes: Array.from(target),
|
||||
closure,
|
||||
binding,
|
||||
expectedCanonical: desktopCanonical,
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/);
|
||||
@@ -129,10 +228,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy
|
||||
material: { imageIds: ["image:M12 Append Image"] },
|
||||
image: { libraryLinked: false, width: 2, height: 2 },
|
||||
});
|
||||
expect(result.appendedCanonical).toEqual(desktopCanonical);
|
||||
expect(result.redoneCanonical).toEqual(desktopCanonical);
|
||||
expect(result.reopenedCanonical).toEqual(desktopCanonical);
|
||||
expect(result.stale.code).toBe("REVISION_CONFLICT");
|
||||
expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID");
|
||||
expect(result.collisionRevision).toBe(result.appendedRevision);
|
||||
expect(result.undoHasObject).toBe(false);
|
||||
expect(result.undoHasMesh).toBe(false);
|
||||
expect(result.undoHasMaterial).toBe(false);
|
||||
expect(result.undoHasImage).toBe(false);
|
||||
expect(result.redoHasObject).toBe(true);
|
||||
expect(result.reopenedHasObject).toBe(true);
|
||||
expect(result.reopenedHasLocalImage).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user