diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 91e43e22dd..fbcde01a45 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1530,10 +1530,6 @@ Returns storage state for this browser context, contains current cookies, local Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. -:::note -IndexedDBs with typed arrays are currently not supported. -::: - ## property: BrowserContext.tracing * since: v1.12 - type: <[Tracing]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 26115f729a..2f41dee5e8 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -9273,9 +9273,6 @@ export interface BrowserContext { /** * Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store * authentication tokens, like Firebase Authentication, enable this. - * - * **NOTE** IndexedDBs with typed arrays are currently not supported. - * */ indexedDB?: boolean; diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 6df7988caf..3c758cbe92 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64'; + export type SerializedValue = undefined | boolean | number | string | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | @@ -25,7 +27,8 @@ export type SerializedValue = { a: SerializedValue[], id: number } | { o: { k: string, v: SerializedValue }[], id: number } | { ref: number } | - { h: number }; + { h: number } | + { ta: { b: string, k: TypedArrayKind } }; export type HandleOrValue = { h: number } | { fallThrough: any }; @@ -68,6 +71,42 @@ export function source() { } } + const typedArrayCtors: Record = { + i8: Int8Array, + ui8: Uint8Array, + ui8c: Uint8ClampedArray, + i16: Int16Array, + ui16: Uint16Array, + i32: Int32Array, + ui32: Uint32Array, + // TODO: add Float16Array once it's in baseline + f32: Float32Array, + f64: Float64Array, + bi64: BigInt64Array, + bui64: BigUint64Array, + }; + + function typedArrayToBase64(array: any) { + if (globalThis.Buffer) + return Buffer.from(array).toString('base64'); + + const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); + return btoa(binary); + } + + function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { + if (globalThis.Buffer) { + const buf = Buffer.from(base64, 'base64'); + return new TypedArrayConstructor(buf.buffer, buf.byteOffset, buf.byteLength / buf.BYTES_PER_ELEMENT); + } + + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return new TypedArrayConstructor(bytes.buffer); + } + function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new Map()): any { if (Object.is(value, undefined)) return undefined; @@ -119,6 +158,8 @@ export function source() { } if ('h' in value) return handles[value.h]; + if ('ta' in value) + return base64ToTypedArray(value.ta.b, typedArrayCtors[value.ta.k]); } return value; } @@ -186,6 +227,10 @@ export function source() { return { u: value.toJSON() }; if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; + for (const [k, ctor] of Object.entries(typedArrayCtors) as [TypedArrayKind, Function][]) { + if (value instanceof ctor) + return { ta: { b: typedArrayToBase64(value), k } }; + } const id = visitorInfo.visited.get(value); if (id) diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 26115f729a..2f41dee5e8 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9273,9 +9273,6 @@ export interface BrowserContext { /** * Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store * authentication tokens, like Firebase Authentication, enable this. - * - * **NOTE** IndexedDBs with typed arrays are currently not supported. - * */ indexedDB?: boolean; diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index e22c66f23c..56f1540a3a 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -100,7 +100,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => .put({ name: 'foo', date: new Date(0) }); transaction .objectStore('store2') - .put('bar', 'foo'); + .put(new TextEncoder().encode('bar'), 'foo'); transaction.addEventListener('complete', resolve); transaction.addEventListener('error', reject); }; @@ -126,16 +126,18 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => expect(cookie).toEqual('username=John Doe'); const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => { const openRequest = indexedDB.open('db', 42); - openRequest.addEventListener('success', () => { + openRequest.addEventListener('success', async () => { const db = openRequest.result; const transaction = db.transaction(['store', 'store2'], 'readonly'); const request1 = transaction.objectStore('store').get('foo'); const request2 = transaction.objectStore('store2').get('foo'); - Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { + const [result1, result2] = await Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { request.addEventListener('success', () => resolve(request.result)); request.addEventListener('error', () => reject(request.error)); - }))).then(resolve, reject); + }))); + + resolve([result1, new TextDecoder().decode(result2 as any)]); }); openRequest.addEventListener('error', () => reject(openRequest.error)); }));