Files
workinf_Blender_Wasm/web/tests/e2e/asset-preview-opfs-commit.spec.ts
mes123456 5a11045ca5
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance Blender 5.2 web parity through M12-03D
2026-08-17 17:30:27 -04:00

172 lines
7.3 KiB
TypeScript

import { expect, test } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
const catalog = JSON.parse(fs.readFileSync(
path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"),
"utf8",
)) as Record<string, unknown>;
const identity = JSON.parse(fs.readFileSync(
path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"),
"utf8",
)) as Record<string, unknown>;
const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png"));
test("M12-02D publishes a catalog preview only after verified OPFS persistence", async ({ page }) => {
await page.goto("/");
const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => {
const storage = await import("/src/storage/asset-preview-opfs-commit.ts");
const encoded = new Uint8Array(bytes).buffer;
const base = await storage.parseAssetCatalogManifestV2({
...sourceCatalog,
assets: (sourceCatalog.assets as Array<Record<string, unknown>>).map((asset) => ({ ...asset, preview: null })),
});
const targetAssetId = base.assets[0].assetId;
const identityInput = { ...sourceIdentity, assetId: targetAssetId } as Record<string, unknown>;
delete identityInput.identitySha256;
const boundIdentity = await storage.createAssetPreviewIdentity(identityInput as never);
const open = (name: string): Promise<IDBDatabase> => new Promise((resolve, reject) => {
const request = indexedDB.open(name, 1);
request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" });
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const complete = (transaction: IDBTransaction): Promise<void> => new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
const get = (store: IDBObjectStore, id: string): Promise<unknown> => new Promise((resolve, reject) => {
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
const seed = async (database: IDBDatabase): Promise<void> => {
const transaction = database.transaction("setting", "readwrite");
transaction.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base });
await complete(transaction);
};
const snapshot = async (database: IDBDatabase) => {
const transaction = database.transaction("setting", "readonly");
const store = transaction.objectStore("setting");
const [index, receipt] = await Promise.all([
get(store, "asset-catalog:index:v2"),
get(store, storage.ASSET_PREVIEW_CATALOG_HEAD_ID),
]);
await complete(transaction);
return { index, receipt };
};
const payloadExists = async (projectId: string): Promise<boolean> => {
try {
const root = await navigator.storage.getDirectory();
let current = root;
for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) {
current = await current.getDirectoryHandle(segment);
}
const file = await (await current.getFileHandle(boundIdentity.content.sha256)).getFile();
return file.size === encoded.byteLength;
}
catch { return false; }
};
const cleanupProject = async (projectId: string): Promise<void> => {
const root = await navigator.storage.getDirectory();
try {
const projects = await root.getDirectoryHandle("projects");
await projects.removeEntry(projectId, { recursive: true });
}
catch { /* The before-write fault creates no project directory. */ }
};
const fault = async (faultAt: "BEFORE_OPFS_WRITE" | "AFTER_OPFS_WRITE" | "AFTER_CATALOG_PUT") => {
const databaseName = `m12-02d-${faultAt.toLowerCase()}-${crypto.randomUUID()}`;
const projectId = `m12-02d-${crypto.randomUUID()}`;
let database = await open(databaseName);
await seed(database);
let code = "";
try {
await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0), { faultAt });
}
catch (error) { code = String((error as Error & { code?: string }).code ?? ""); }
database.close();
database = await open(databaseName);
const state = await snapshot(database);
const persisted = await payloadExists(projectId);
database.close();
indexedDB.deleteDatabase(databaseName);
await cleanupProject(projectId);
return { faultAt, code, state, persisted };
};
const faults = [];
for (const faultAt of ["BEFORE_OPFS_WRITE", "AFTER_OPFS_WRITE", "AFTER_CATALOG_PUT"] as const) {
faults.push(await fault(faultAt));
}
const databaseName = `m12-02d-success-${crypto.randomUUID()}`;
const projectId = `m12-02d-${crypto.randomUUID()}`;
let database = await open(databaseName);
await seed(database);
const committed = await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0));
database.close();
database = await open(databaseName);
const reopened = await storage.loadCommittedAssetPreviewCatalog(database);
const successState = await snapshot(database);
const persisted = await payloadExists(projectId);
database.close();
indexedDB.deleteDatabase(databaseName);
await cleanupProject(projectId);
return {
base,
targetAssetId,
identitySha256: boundIdentity.identitySha256,
contentSha256: boundIdentity.content.sha256,
faults,
committed: {
revision: committed.manifest.revision,
preview: committed.preview,
receipt: committed.receipt,
deduplicated: committed.deduplicated,
},
reopened: {
revision: reopened.manifest.revision,
preview: reopened.preview,
receipt: reopened.receipt,
},
successState,
persisted,
};
}, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] });
for (const failure of result.faults) {
expect(failure.code).toBe("STORAGE_TRANSACTION");
expect(failure.state.index).toEqual({ id: "asset-catalog:index:v2", value: result.base });
expect(failure.state.receipt).toBeUndefined();
}
expect(result.faults.map((failure) => failure.persisted)).toEqual([false, true, true]);
expect(result.persisted).toBe(true);
expect(result.committed.revision).toBe(8);
expect(result.committed.deduplicated).toBe(false);
expect(result.committed.preview).toEqual({
sha256: result.contentSha256,
mimeType: "image/png",
width: 8,
height: 8,
byteLength: 513,
});
expect(result.committed.receipt).toMatchObject({
task: "M12-02D",
status: "COMMITTED",
assetId: result.targetAssetId,
baseRevision: 7,
committedRevision: 8,
previewIdentitySha256: result.identitySha256,
contentSha256: result.contentSha256,
});
expect(result.reopened).toEqual({
revision: result.committed.revision,
preview: result.committed.preview,
receipt: result.committed.receipt,
});
expect(result.successState.receipt).toEqual(result.committed.receipt);
});