import type { IndexedDbMaintenanceDependencies, IndexedDbRuntimeDependencies, } from "../../src/adapters/storage/indexeddb/indexeddb-types.ts"; import { expect, test, type Page, } from "../support/browser/strict-browser-test.ts"; type BrowserItem = Readonly<{ label: string; rank: number; }>; function databaseName(_prefix: string): string { const token = crypto.randomUUID().replaceAll("-", ""); return `ca-idb-v1:a${token.slice(0, 31)}.n${token.slice(1)}.p${token.split("").reverse().join("").slice(0, 31)}`; } async function installLifecycleRuntime( page: Page, name: string, targetVersion: 1 | 2, blockedTimeoutMs = 1_000, ) { return await page.evaluate( async ({ currentDatabaseName, currentTargetVersion, currentBlockedTimeoutMs, }) => { const modulePath = "/src/adapters/storage/indexeddb/indexeddb-runtime.ts"; const { createIndexedDbRuntime } = (await import( /* @vite-ignore */ modulePath )) as typeof import("../../src/adapters/storage/indexeddb/indexeddb-runtime.ts"); type LifecycleValue = Readonly<{ value: string }>; const [authorityToken, namespaceToken, partitionToken] = currentDatabaseName.slice("ca-idb-v1:".length).split("."); const scope = { authorityToken, namespaceToken, partitionToken, accountScope: "ORIGIN_SHARED" as const, }; const storagePolicy = { owner: "platform-storage", namespace: "indexeddb-browser-lifecycle", classification: "INTERNAL" as const, authority: "SERVER" as const, accountScope: "ORIGIN_SHARED" as const, retention: { kind: "EXPLICIT_DELETE" as const }, softBudgetBytes: 1_000_000, hardBudgetBytes: 2_000_000, evictionPriority: "SYNCED_COPY" as const, logoutAction: "KEEP_ORIGIN_SHARED" as const, accountDeletionAction: "KEEP_ORIGIN_SHARED" as const, pressureAction: "RETAIN" as const, unavailableFallback: "ONLINE_ONLY" as const, }; const migrations: IndexedDbRuntimeDependencies< LifecycleValue, LifecycleValue, null >["migrations"] = [ { id: "schema-v1", fromVersion: 0, toVersion: 1, operations: [ { kind: "CREATE_STORE", name: "governance", keyPath: "bindingKey", }, { kind: "CREATE_STORE", name: "retention", keyPath: "recordKey", indexes: [ { name: "by-eligibility", keyPath: "eligibleAtEpochMs", }, ], }, { kind: "CREATE_STORE", name: "records", keyPath: "key", }, { kind: "CREATE_STORE", name: "receipts", keyPath: "idempotencyKey", indexes: [ { name: "by-expiry", keyPath: "expiresAtEpochMs", }, ], }, ], }, ...(currentTargetVersion === 2 ? [ { id: "schema-v2", fromVersion: 1, toVersion: 2, operations: [ { kind: "CREATE_INDEX" as const, store: "records", index: { name: "by-key", keyPath: "key", }, }, ], }, ] : []), ]; const config: IndexedDbRuntimeDependencies< LifecycleValue, LifecycleValue, null > = { scope, storagePolicy, schemaVersion: currentTargetVersion, recordStore: "records", governanceStore: "governance", retentionStore: "retention", retentionEligibilityIndex: "by-eligibility", lifecycleMetadataStores: [], idempotencyStore: "receipts", idempotencyExpiryIndex: "by-expiry", receiptRetentionMs: 60_000, maxIdempotencyReceipts: 1_000, migrations, codec: { currentVersion: 1, encode: (value) => ({ ok: true, value }), measureStoredBytes: (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength, decode: (version, value) => version === 1 && value !== null && typeof value === "object" && typeof ( value as Readonly<{ value?: unknown }> ).value === "string" ? { ok: true, value: value as LifecycleValue, } : { ok: false }, fingerprint: () => "0".repeat(64), }, queryPolicy: { plan: () => ({ limit: 10 }) }, blockedTimeoutMs: currentBlockedTimeoutMs, authorizeLifecycle: () => ({ authorized: true, proofToken: "browserauthorityproof_001", }), }; const runtime = createIndexedDbRuntime(config); ( globalThis as unknown as { __indexedDbLifecycleRuntime?: unknown; } ).__indexedDbLifecycleRuntime = runtime; const opened = await runtime.open(); return { opened, status: runtime.getStatus() }; }, { currentDatabaseName: name, currentTargetVersion: targetVersion, currentBlockedTimeoutMs: blockedTimeoutMs, }, ); } async function lifecycleStatus(page: Page): Promise { return await page.evaluate(() => { const runtime = ( globalThis as unknown as { __indexedDbLifecycleRuntime?: { getStatus(): unknown; }; } ).__indexedDbLifecycleRuntime; return runtime?.getStatus() ?? null; }); } async function closeLifecycleRuntime(page: Page): Promise { await page.evaluate(() => { const runtime = ( globalThis as unknown as { __indexedDbLifecycleRuntime?: { close(): void; }; } ).__indexedDbLifecycleRuntime; runtime?.close(); }); } test("runs open, CAS, read, cursor query, close, and deletion against native IndexedDB", async ({ page, }) => { await page.goto("/"); const name = databaseName("runtime-browser"); const result = await page.evaluate(async (currentDatabaseName) => { const modulePath = "/src/adapters/storage/indexeddb/indexeddb-runtime.ts"; const { createIndexedDbRuntime } = (await import( /* @vite-ignore */ modulePath )) as typeof import("../../src/adapters/storage/indexeddb/indexeddb-runtime.ts"); const observations: unknown[] = []; let epochMs = 1_000; let lifecycleAuthorizations = 0; const [authorityToken, namespaceToken, partitionToken] = currentDatabaseName.slice("ca-idb-v1:".length).split("."); const scope = { authorityToken, namespaceToken, partitionToken, accountScope: "ORIGIN_SHARED" as const, }; const storagePolicy = { owner: "platform-storage", namespace: "indexeddb-browser-runtime", classification: "INTERNAL" as const, authority: "SERVER" as const, accountScope: "ORIGIN_SHARED" as const, retention: { kind: "TTL" as const, maxAgeMs: 10 }, softBudgetBytes: 1_000_000, hardBudgetBytes: 2_000_000, evictionPriority: "SYNCED_COPY" as const, logoutAction: "KEEP_ORIGIN_SHARED" as const, accountDeletionAction: "KEEP_ORIGIN_SHARED" as const, pressureAction: "RETAIN" as const, unavailableFallback: "ONLINE_ONLY" as const, }; const config: IndexedDbRuntimeDependencies< BrowserItem, BrowserItem, Readonly<{ limit: number }> > = { scope, storagePolicy, schemaVersion: 1, recordStore: "records", governanceStore: "governance", retentionStore: "retention", retentionEligibilityIndex: "by-eligibility", lifecycleMetadataStores: [], idempotencyStore: "receipts", idempotencyExpiryIndex: "by-expiry", receiptRetentionMs: 60_000, maxIdempotencyReceipts: 1_000, migrations: [ { id: "schema-v1", fromVersion: 0, toVersion: 1, operations: [ { kind: "CREATE_STORE", name: "governance", keyPath: "bindingKey", }, { kind: "CREATE_STORE", name: "retention", keyPath: "recordKey", indexes: [ { name: "by-eligibility", keyPath: "eligibleAtEpochMs", }, ], }, { kind: "CREATE_STORE", name: "records", keyPath: "key", indexes: [ { name: "by-rank", keyPath: "payload.rank", }, ], }, { kind: "CREATE_STORE", name: "receipts", keyPath: "idempotencyKey", indexes: [ { name: "by-expiry", keyPath: "expiresAtEpochMs", }, ], }, ], }, ], codec: { currentVersion: 1, encode: (value) => ({ ok: true, value }), measureStoredBytes: (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength, decode: (version, value) => version === 1 && value !== null && typeof value === "object" && typeof (value as Partial).label === "string" && typeof (value as Partial).rank === "number" ? { ok: true, value: value as BrowserItem } : { ok: false }, fingerprint: async (value) => { const bytes = new TextEncoder().encode( JSON.stringify([value.rank, value.label]), ); const digest = await crypto.subtle.digest("SHA-256", bytes); return Array.from( new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0"), ).join(""); }, }, queryPolicy: { plan: (query) => ({ index: "by-rank", direction: "next", limit: query.limit, }), }, observe: (event) => observations.push(event), nowEpochMilliseconds: () => epochMs, nowMonotonicMilliseconds: () => 0, authorizeLifecycle: () => { lifecycleAuthorizations += 1; return { authorized: true, proofToken: "browserauthorityproof_002", }; }, }; const runtime = createIndexedDbRuntime(config); const opened = await runtime.open(); const alpha = { key: "alpha-key", value: { label: "alpha", rank: 2 }, expectedRevision: null, idempotencyKey: "alpha-create", } as const; const created = await runtime.compareAndSwap(alpha); const replayed = await runtime.compareAndSwap(alpha); const conflict = await runtime.compareAndSwap({ ...alpha, value: { label: "changed", rank: 2 }, }); const uncloneable = await runtime.compareAndSwap({ key: "uncloneable-key", value: { label: "uncloneable", rank: 3, callback: () => undefined, } as unknown as BrowserItem, expectedRevision: null, idempotencyKey: "uncloneable-create", }); const uncloneableRead = await runtime.read( "uncloneable-key", ); await runtime.compareAndSwap({ key: "beta-key", value: { label: "beta", rank: 1 }, expectedRevision: null, idempotencyKey: "beta-create", }); const read = await runtime.read("alpha-key"); const firstPage = await runtime.query({ limit: 1 }); const secondPage = await runtime.query( { limit: 1 }, firstPage.ok ? firstPage.value.nextCursor : null, ); epochMs = 1_011; const expiredRead = await runtime.read("alpha-key"); const expiredPage = await runtime.query({ limit: 10 }); const lifecycle = await runtime.enforceLifecycleBatch({ action: "RETENTION_SWEEP", maxRows: 10, maxDurationMs: 1_000, }); const auditConnection = await new Promise( (resolve, reject) => { const request = indexedDB.open(currentDatabaseName, 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }, ); const persistedAudit = await new Promise( (resolve, reject) => { const transaction = auditConnection.transaction( ["governance", "retention", "receipts"], "readonly", ); const governance = transaction .objectStore("governance") .getAll(); const retention = transaction .objectStore("retention") .getAll(); const receipts = transaction .objectStore("receipts") .getAll(); transaction.oncomplete = () => resolve( JSON.stringify([ governance.result, retention.result, receipts.result, ]), ); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }, ); auditConnection.close(); const beforeClose = runtime.getStatus(); runtime.close(); const afterClose = runtime.getStatus(); const deletion = await new Promise((resolve) => { const request = indexedDB.deleteDatabase(currentDatabaseName); let blockedTimer: ReturnType | undefined; const finish = (value: string) => { if (blockedTimer) clearTimeout(blockedTimer); resolve(value); }; request.onsuccess = () => finish("DELETED"); request.onerror = () => finish(request.error?.name ?? "UNKNOWN_ERROR"); // `blocked` is a notification, not a terminal outcome: a connection // that has already called close() may finish its last transaction and // allow this same request to succeed. request.onblocked = () => { blockedTimer ??= setTimeout(() => finish("BLOCKED"), 2_000); }; }); return { opened, created, replayed, conflict, uncloneable, uncloneableRead, read, firstPage, secondPage, expiredRead, expiredPage, lifecycle, lifecycleAuthorizations, persistedAudit, beforeClose, afterClose, deletion, observations, }; }, name); expect(result.opened).toMatchObject({ ok: true }); expect(result.created).toMatchObject({ ok: true, value: { revision: 1, replayed: false }, }); expect(result.replayed).toMatchObject({ ok: true, value: { revision: 1, replayed: true }, }); expect(result.conflict).toMatchObject({ ok: false, error: { code: "CONFLICT" }, }); expect(result.uncloneable).toMatchObject({ ok: false, error: { code: "CORRUPT_DATA" }, }); expect(result.uncloneableRead).toEqual({ ok: true, value: null, }); expect(result.read).toEqual({ ok: true, value: { value: { label: "alpha", rank: 2 }, revision: 1, }, }); expect(result.firstPage).toMatchObject({ ok: true, value: { items: [{ label: "beta", rank: 1 }] }, }); expect(result.secondPage).toEqual({ ok: true, value: { items: [{ label: "alpha", rank: 2 }], nextCursor: null, }, }); expect(result.expiredRead).toMatchObject({ ok: false, error: { code: "EXPIRED_RESOURCE" }, }); expect(result.expiredPage).toEqual({ ok: true, value: { items: [], nextCursor: null }, }); expect(result.lifecycle).toMatchObject({ ok: true, value: { state: "COMPLETE", deletedRows: 2 }, }); expect(result.lifecycleAuthorizations).toBe(1); expect(result.persistedAudit).not.toContain( "browserauthorityproof_002", ); expect(result.beforeClose).toEqual({ kind: "READY", schemaVersion: 1, }); expect(result.afterClose).toEqual({ kind: "DISPOSED" }); expect(result.deletion).toBe("DELETED"); expect(JSON.stringify(result.observations)).not.toMatch( /alpha-key|beta-key|alpha-create/, ); }); test("runs resumable codec maintenance against native IndexedDB", async ({ page, }) => { await page.goto("/"); const name = databaseName("maintenance-browser"); const result = await page.evaluate(async (currentDatabaseName) => { const runtimeModulePath = "/src/adapters/storage/indexeddb/indexeddb-runtime.ts"; const maintenanceModulePath = "/src/adapters/storage/indexeddb/indexeddb-maintenance.ts"; const { createIndexedDbRuntime } = (await import( /* @vite-ignore */ runtimeModulePath )) as typeof import("../../src/adapters/storage/indexeddb/indexeddb-runtime.ts"); const { createIndexedDbMaintenance } = (await import( /* @vite-ignore */ maintenanceModulePath )) as typeof import("../../src/adapters/storage/indexeddb/indexeddb-maintenance.ts"); const [authorityToken, namespaceToken, partitionToken] = currentDatabaseName.slice("ca-idb-v1:".length).split("."); const scope = { authorityToken, namespaceToken, partitionToken, accountScope: "ORIGIN_SHARED" as const, }; const storagePolicy = { owner: "platform-storage", namespace: "indexeddb-browser-maintenance", classification: "INTERNAL" as const, authority: "SERVER" as const, accountScope: "ORIGIN_SHARED" as const, retention: { kind: "EXPLICIT_DELETE" as const }, softBudgetBytes: 1_000_000, hardBudgetBytes: 2_000_000, evictionPriority: "SYNCED_COPY" as const, logoutAction: "KEEP_ORIGIN_SHARED" as const, accountDeletionAction: "KEEP_ORIGIN_SHARED" as const, pressureAction: "RETAIN" as const, unavailableFallback: "ONLINE_ONLY" as const, }; const runtimeConfig: IndexedDbRuntimeDependencies< Readonly<{ label: string }>, Readonly<{ label: string }>, null > = { scope, storagePolicy, schemaVersion: 1, recordStore: "records", governanceStore: "governance", retentionStore: "retention", retentionEligibilityIndex: "by-eligibility", lifecycleMetadataStores: ["maintenance"], idempotencyStore: "receipts", idempotencyExpiryIndex: "by-expiry", receiptRetentionMs: 60_000, maxIdempotencyReceipts: 1_000, migrations: [ { id: "schema-v1", fromVersion: 0, toVersion: 1, operations: [ { kind: "CREATE_STORE", name: "governance", keyPath: "bindingKey", }, { kind: "CREATE_STORE", name: "retention", keyPath: "recordKey", indexes: [ { name: "by-eligibility", keyPath: "eligibleAtEpochMs", }, ], }, { kind: "CREATE_STORE", name: "records", keyPath: "key", }, { kind: "CREATE_STORE", name: "receipts", keyPath: "idempotencyKey", indexes: [ { name: "by-expiry", keyPath: "expiresAtEpochMs", }, ], }, { kind: "CREATE_STORE", name: "maintenance", keyPath: "checkpointKey", }, ], }, ], codec: { currentVersion: 2, encode: (value) => ({ ok: true, value }), measureStoredBytes: (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength, decode: (version, value) => version === 2 && value !== null && typeof value === "object" && typeof ( value as Readonly<{ label?: unknown }> ).label === "string" ? { ok: true, value: value as Readonly<{ label: string }>, } : { ok: false }, fingerprint: () => "0".repeat(64), }, queryPolicy: { plan: () => ({ limit: 10 }) }, authorizeLifecycle: () => ({ authorized: true, proofToken: "browserauthorityproof_003", }), }; const runtime = createIndexedDbRuntime(runtimeConfig); const opened = await runtime.open(); runtime.close(); const seedConnection = await new Promise( (resolve, reject) => { const request = indexedDB.open(currentDatabaseName, 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }, ); await new Promise((resolve, reject) => { const transaction = seedConnection.transaction( ["records", "governance", "retention", "receipts"], "readwrite", ); const measuredA = new TextEncoder().encode( JSON.stringify({ legacyLabel: "alpha" }), ).byteLength + 512 + "legacy-a".length * 2; const measuredB = new TextEncoder().encode( JSON.stringify({ legacyLabel: "beta" }), ).byteLength + 512 + "legacy-b".length * 2; transaction.objectStore("records").put({ key: "legacy-a", codecVersion: 1, revision: 4, payload: { legacyLabel: "alpha" }, }); transaction.objectStore("records").put({ key: "legacy-b", codecVersion: 1, revision: 8, payload: { legacyLabel: "beta" }, }); transaction.objectStore("retention").put({ recordKey: "legacy-a", writtenAtEpochMs: 1, synchronization: "NONE", measuredBytes: measuredA, }); transaction.objectStore("retention").put({ recordKey: "legacy-b", writtenAtEpochMs: 1, synchronization: "NONE", measuredBytes: measuredB, }); transaction.objectStore("receipts").put({ idempotencyKey: "expired-browser-receipt", operation: "PUT", recordKey: "already-removed", expectedRevision: null, fingerprint: "a".repeat(64), synchronization: "NONE", revision: 1, expiresAtEpochMs: 0, }); transaction.objectStore("governance").put({ bindingKey: "dataset-budget", budgetVersion: 1, usedBytes: measuredA + measuredB, receiptCount: 1, }); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); transaction.onabort = () => reject(transaction.error); }); seedConnection.close(); const maintenanceConfig: IndexedDbMaintenanceDependencies< Readonly<{ label: string }> > = { scope, storagePolicy, schemaVersion: 1, recordStore: "records", governanceStore: "governance", retentionStore: "retention", checkpointStore: "maintenance", checkpointKey: "records-codec", idempotencyStore: "receipts", idempotencyExpiryIndex: "by-expiry", migrationPolicy: { migrationId: "records-to-v2", targetCodecVersion: 2, measureStoredBytes: (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength, isOldWriterDrainConfirmed: () => true, migrate: async ({ payload }) => { const legacyLabel = payload && typeof payload === "object" && typeof ( payload as Readonly<{ legacyLabel?: unknown }> ).legacyLabel === "string" ? ( payload as Readonly<{ legacyLabel: string }> ).legacyLabel : null; return legacyLabel === null ? { ok: false } : { ok: true, value: { label: legacyLabel } }; }, }, }; const maintenance = createIndexedDbMaintenance( maintenanceConfig, ); const firstBatch = await maintenance.migrateCodecBatch({ maxRows: 1, maxDurationMs: 10_000, }); const secondBatch = await maintenance.migrateCodecBatch({ maxRows: 1, maxDurationMs: 10_000, }); const pruned = await maintenance.pruneExpiredReceipts({ maxRows: 1, maxDurationMs: 10_000, }); const inspectConnection = await new Promise( (resolve, reject) => { const request = indexedDB.open(currentDatabaseName, 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }, ); const rawRecords = await new Promise( (resolve, reject) => { const transaction = inspectConnection.transaction( "records", "readonly", ); const request = transaction.objectStore("records").getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }, ); const receiptCount = await new Promise( (resolve, reject) => { const transaction = inspectConnection.transaction( "receipts", "readonly", ); const request = transaction.objectStore("receipts").count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }, ); inspectConnection.close(); const deletion = await new Promise((resolve) => { const request = indexedDB.deleteDatabase(currentDatabaseName); let blockedTimer: ReturnType | undefined; const finish = (value: string) => { if (blockedTimer) clearTimeout(blockedTimer); resolve(value); }; request.onsuccess = () => finish("DELETED"); request.onerror = () => finish(request.error?.name ?? "UNKNOWN_ERROR"); request.onblocked = () => { blockedTimer ??= setTimeout(() => finish("BLOCKED"), 2_000); }; }); return { opened, firstBatch, secondBatch, pruned, rawRecords, receiptCount, deletion, }; }, name); expect(result.opened).toMatchObject({ ok: true }); expect(result.firstBatch).toMatchObject({ ok: true, value: { state: "MORE", migratedRows: 1 }, }); expect(result.secondBatch).toMatchObject({ ok: true, value: { state: "COMPLETE", migratedRows: 1 }, }); expect(result.pruned).toMatchObject({ ok: true, value: { state: "COMPLETE", deletedRows: 1 }, }); expect(result.receiptCount).toBe(0); expect(result.rawRecords).toEqual([ { key: "legacy-a", codecVersion: 2, revision: 4, payload: { label: "alpha" }, }, { key: "legacy-b", codecVersion: 2, revision: 8, payload: { label: "beta" }, }, ]); expect(result.deletion).toBe("DELETED"); }); test("closes a v1 runtime on versionchange so another context can upgrade to v2", async ({ page, }) => { const secondPage = await page.context().newPage(); const name = databaseName("versionchange"); try { await Promise.all([page.goto("/"), secondPage.goto("/")]); expect(await installLifecycleRuntime(page, name, 1)).toEqual({ opened: { ok: true }, status: { kind: "READY", schemaVersion: 1 }, }); const upgrade = await secondPage.evaluate( async (currentDatabaseName) => await new Promise((resolve) => { const request = indexedDB.open(currentDatabaseName, 2); request.onsuccess = () => { request.result.close(); resolve("UPGRADED"); }; request.onerror = () => resolve(request.error?.name ?? "UNKNOWN_ERROR"); request.onblocked = () => resolve("BLOCKED"); }), name, ); expect(upgrade).toBe("UPGRADED"); expect(await lifecycleStatus(page)).toEqual({ kind: "CLOSED", reason: "VERSION_CHANGE", }); await closeLifecycleRuntime(page); const deletion = await secondPage.evaluate( async (currentDatabaseName) => await new Promise((resolve) => { const request = indexedDB.deleteDatabase( currentDatabaseName, ); request.onsuccess = () => resolve("DELETED"); request.onerror = () => resolve(request.error?.name ?? "UNKNOWN_ERROR"); request.onblocked = () => resolve("BLOCKED"); }), name, ); expect(deletion).toBe("DELETED"); } finally { await secondPage.close(); } }); test("fails a blocked v2 upgrade closed, then closes its late connection after the blocker exits", async ({ page, }) => { const blockerPage = await page.context().newPage(); const name = databaseName("blocked"); try { await Promise.all([page.goto("/"), blockerPage.goto("/")]); const blocker = await blockerPage.evaluate( async (currentDatabaseName) => await new Promise((resolve) => { const request = indexedDB.open(currentDatabaseName, 1); request.onupgradeneeded = () => { const records = request.result.createObjectStore( "records", { keyPath: "key" }, ); void records; const receipts = request.result.createObjectStore( "receipts", { keyPath: "idempotencyKey" }, ); receipts.createIndex( "by-expiry", "expiresAtEpochMs", ); }; request.onsuccess = () => { ( globalThis as unknown as { __indexedDbNativeBlocker?: IDBDatabase; } ).__indexedDbNativeBlocker = request.result; resolve("OPEN"); }; request.onerror = () => resolve(request.error?.name ?? "UNKNOWN_ERROR"); }), name, ); expect(blocker).toBe("OPEN"); const blocked = await installLifecycleRuntime( page, name, 2, 25, ); expect(blocked).toMatchObject({ opened: { ok: false, error: { code: "BLOCKED", recovery: "RELOAD_OTHER_CONTEXTS", }, }, status: { kind: "BLOCKED", currentVersion: 1, targetVersion: 2, }, }); await blockerPage.evaluate(() => { const scope = globalThis as unknown as { __indexedDbNativeBlocker?: IDBDatabase; }; scope.__indexedDbNativeBlocker?.close(); scope.__indexedDbNativeBlocker = undefined; }); await expect .poll(async () => await lifecycleStatus(page)) .toEqual({ kind: "CLOSED", reason: "NOT_OPENED", }); await closeLifecycleRuntime(page); const deletion = await blockerPage.evaluate( async (currentDatabaseName) => await new Promise((resolve) => { const request = indexedDB.deleteDatabase( currentDatabaseName, ); request.onsuccess = () => resolve("DELETED"); request.onerror = () => resolve(request.error?.name ?? "UNKNOWN_ERROR"); request.onblocked = () => resolve("BLOCKED"); }), name, ); expect(deletion).toBe("DELETED"); } finally { await blockerPage.close(); } });