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; const identity = JSON.parse(fs.readFileSync( path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8", )) as Record; const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); test("M12-02E deduplicates preview content without merging asset metadata", 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>).map((asset) => ({ ...asset, preview: null })), }); const identities = []; for (const asset of base.assets) { const input = { ...sourceIdentity, assetId: asset.assetId } as Record; delete input.identitySha256; identities.push(await storage.createAssetPreviewIdentity(input as never)); } const databaseName = `m12-02e-${crypto.randomUUID()}`; const projectId = `m12-02e-${crypto.randomUUID()}`; const open = (): Promise => new Promise((resolve, reject) => { const request = indexedDB.open(databaseName, 1); request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }); let database = await open(); const seed = database.transaction("setting", "readwrite"); seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); await complete(seed); const first = await storage.commitAssetPreviewToOPFS(database, projectId, 7, identities[0], encoded.slice(0)); const second = await storage.commitAssetPreviewToOPFS(database, projectId, 8, identities[1], encoded.slice(0)); database.close(); database = await open(); const reopened = await storage.loadCommittedAssetPreviewCatalog(database); const root = await navigator.storage.getDirectory(); let contentDirectory = root; for (const segment of ["projects", projectId, "assets", "sha256", identities[0].content.sha256.slice(0, 2)]) { contentDirectory = await contentDirectory.getDirectoryHandle(segment); } const files: string[] = []; for await (const [name, handle] of (contentDirectory as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }).entries()) { if (handle.kind === "file") files.push(name); } files.sort(); const stored = await (await contentDirectory.getFileHandle(identities[0].content.sha256)).getFile(); const storedBytes = await stored.arrayBuffer(); const storedSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", storedBytes)), (byte) => byte.toString(16).padStart(2, "0")).join(""); const withoutPreview = (asset: Record) => { const copy = { ...asset }; delete copy.preview; return copy; }; const resultValue = { first: { revision: first.manifest.revision, deduplicated: first.deduplicated, identity: identities[0].identitySha256 }, second: { revision: second.manifest.revision, deduplicated: second.deduplicated, identity: identities[1].identitySha256 }, contentSha256: identities[0].content.sha256, files, storedBytes: stored.size, storedSha256, baseMetadata: base.assets.map((asset) => withoutPreview(asset as unknown as Record)), committedMetadata: second.manifest.assets.map((asset) => withoutPreview(asset as unknown as Record)), previews: second.manifest.assets.map((asset) => asset.preview), reopened: { revision: reopened.manifest.revision, receiptAssetId: reopened.receipt.assetId, preview: reopened.preview, }, assetIds: base.assets.map((asset) => asset.assetId), }; database.close(); indexedDB.deleteDatabase(databaseName); const projects = await root.getDirectoryHandle("projects"); await projects.removeEntry(projectId, { recursive: true }); return resultValue; }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); expect(result.first).toMatchObject({ revision: 8, deduplicated: false }); expect(result.second).toMatchObject({ revision: 9, deduplicated: true }); expect(result.first.identity).not.toBe(result.second.identity); expect(result.files).toEqual([result.contentSha256]); expect(result.storedBytes).toBe(513); expect(result.storedSha256).toBe(result.contentSha256); expect(result.committedMetadata).toEqual(result.baseMetadata); expect(result.committedMetadata[0]).not.toEqual(result.committedMetadata[1]); expect(result.previews).toEqual([ { sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 }, { sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 }, ]); expect(result.reopened).toEqual({ revision: 9, receiptAssetId: result.assetIds[1], preview: result.previews[1], }); });