Advance Blender 5.2 web parity through M12-03D
This commit is contained in:
171
web/tests/e2e/asset-preview-opfs-commit.spec.ts
Normal file
171
web/tests/e2e/asset-preview-opfs-commit.spec.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user