diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index ed9308bb50..afd73a4142 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1527,10 +1527,6 @@ Returns storage state for this browser context, contains current cookies, local Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 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 927c93faa8..aff26e8942 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -9274,9 +9274,6 @@ export interface BrowserContext { * Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 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/protocol/serializers.ts b/packages/playwright-core/src/protocol/serializers.ts index 71ef31795a..e361e983b5 100644 --- a/packages/playwright-core/src/protocol/serializers.ts +++ b/packages/playwright-core/src/protocol/serializers.ts @@ -57,6 +57,10 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde } if (value.r !== undefined) return new RegExp(value.r.p, value.r.f); + if (value.ta !== undefined) { + const ctor = typedArrayKindToConstructor[value.ta.k] as any; + return new ctor(value.ta.b.buffer, value.ta.b.byteOffset, value.ta.b.length); + } if (value.a !== undefined) { const result: any[] = []; @@ -128,6 +132,10 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; + const typedArrayKind = constructorToTypedArrayKind.get(value.constructor); + if (typedArrayKind) + return { ta: { b: Buffer.from(value.buffer, value.byteOffset, value.length), k: typedArrayKind } }; + const id = visitorInfo.visited.get(value); if (id) return { ref: id }; @@ -178,3 +186,20 @@ function isError(obj: any): obj is Error { const proto = obj ? Object.getPrototypeOf(obj) : null; return obj instanceof Error || proto?.name === 'Error' || (proto && isError(proto)); } + + +type TypedArrayKind = NonNullable['k']; +const typedArrayKindToConstructor: Record = { + i8: Int8Array, + ui8: Uint8Array, + ui8c: Uint8ClampedArray, + i16: Int16Array, + ui16: Uint16Array, + i32: Int32Array, + ui32: Uint32Array, + f32: Float32Array, + f64: Float64Array, + bi64: BigInt64Array, + bui64: BigUint64Array, +}; +const constructorToTypedArrayKind: Map = new Map(Object.entries(typedArrayKindToConstructor).map(([k, v]) => [v, k as TypedArrayKind])); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7f3f064937..0484fd1c11 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -58,6 +58,10 @@ scheme.SerializedValue = tObject({ d: tOptional(tString), u: tOptional(tString), bi: tOptional(tString), + ta: tOptional(tObject({ + b: tBinary, + k: tEnum(['i8', 'ui8', 'ui8c', 'i16', 'ui16', 'i32', 'ui32', 'f32', 'f64', 'bi64', 'bui64']), + })), e: tOptional(tObject({ m: tString, n: tString, diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 945b9702ef..d419c65f24 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -16,6 +16,8 @@ import type { Builtins } from './builtins'; +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' } | @@ -27,7 +29,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 } }; type HandleOrValue = { h: number } | { fallThrough: any }; @@ -70,6 +73,48 @@ export function source(builtins: Builtins) { } } + function isTypedArray(obj: any, constructor: Function): boolean { + try { + return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`; + } catch (error) { + return false; + } + } + + const typedArrayConstructors: 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) { + /** + * Firefox does not support iterating over typed arrays, so we use `.toBase64`. + * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().' + */ + if ('toBase64' in array) + return array.toBase64(); + const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join(''); + return btoa(binary); + } + + function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { + 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: Builtins.Map = new builtins.Map()): any { if (Object.is(value, undefined)) return undefined; @@ -121,6 +166,8 @@ export function source(builtins: Builtins) { } if ('h' in value) return handles[value.h]; + if ('ta' in value) + return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); } return value; } @@ -191,6 +238,10 @@ export function source(builtins: Builtins) { return { u: value.toJSON() }; if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; + for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { + if (isTypedArray(value, 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 927c93faa8..aff26e8942 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9274,9 +9274,6 @@ export interface BrowserContext { * Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) 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/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index c6767029da..2e1c447b88 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -180,6 +180,10 @@ export type SerializedValue = { d?: string, u?: string, bi?: string, + ta?: { + b: Binary, + k: 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64', + }, e?: { m: string, n: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index ce0c3f800f..c146e995da 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -82,6 +82,25 @@ SerializedValue: u: string? # String representation of BigInt. bi: string? + # Typed array. + ta: + type: object? + properties: + b: binary + k: + type: enum + literals: + - i8 + - ui8 + - ui8c + - i16 + - ui16 + - i32 + - ui32 + - f32 + - f64 + - bi64 + - bui64 # Serialized Error object. e: type: object? diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index ec8afa8e91..31c4884e65 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -98,7 +98,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => .put({ name: 'foo', date: new Date(0), null: null }); transaction .objectStore('store2') - .put('bar', 'foo'); + .put(new TextEncoder().encode('bar'), 'foo'); transaction.addEventListener('complete', resolve); transaction.addEventListener('error', reject); }; @@ -124,16 +124,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)); })); diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index e5d6bf9dcf..a9fd6529d5 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -94,6 +94,27 @@ it('should transfer arrays as arrays, not objects', async ({ page }) => { expect(result).toBe(true); }); +it('should transfer typed arrays', async ({ page }) => { + const testCases = [ + new Int8Array([1, 2, 3]), + new Uint8Array([1, 2, 3]), + new Uint8ClampedArray([1, 2, 3]), + new Int16Array([1, 2, 3]), + new Uint16Array([1, 2, 3]), + new Int32Array([1, 2, 3]), + new Uint32Array([1, 2, 3]), + new Float32Array([1.1, 2.2, 3.3]), + new Float64Array([1.1, 2.2, 3.3]), + new BigInt64Array([1n, 2n, 3n]), + new BigUint64Array([1n, 2n, 3n]) + ]; + + for (const typedArray of testCases) { + const result = await page.evaluate(a => a, typedArray); + expect(result).toEqual(typedArray); + } +}); + it('should transfer bigint', async ({ page }) => { expect(await page.evaluate(() => 42n)).toBe(42n); expect(await page.evaluate(a => a, 17n)).toBe(17n);