diff --git a/agent/main/test/basic.test.ts b/agent/main/test/basic.test.ts index ece0e5c1e..f6d090dcc 100644 --- a/agent/main/test/basic.test.ts +++ b/agent/main/test/basic.test.ts @@ -1,5 +1,7 @@ import { BrowserUtils, Helpers, TestLogger } from '@ulixee/unblocked-agent-testing/index'; import TypeSerializer, { stringifiedTypeSerializerClass } from '@ulixee/commons/lib/TypeSerializer'; +import getTestObject from '@ulixee/commons/test/helpers/getTestObject'; + import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEvent'; import { Browser, BrowserContext } from '../index'; @@ -13,24 +15,7 @@ describe('basic tests', () => { }); beforeAll(async () => { - testObject = { - name: 'original', - map: new Map([ - ['1', 1], - ['2', 2], - ]), - set: new Set([1, 2, 3, 4]), - regex: /test13234/gi, - date: new Date('2021-03-17T15:41:06.513Z'), - buffer: Buffer.from('This is a test buffer'), - error: new CanceledPromiseError('This is canceled'), - }; - - testObject.nestedObject = { ...testObject, name: 'nested' }; - testObject.nestedArray = [ - { ...testObject, name: 'item1' }, - { ...testObject, name: 'item2' }, - ]; + testObject = getTestObject(); browser = new Browser(BrowserUtils.browserEngineOptions); Helpers.onClose(() => browser.close(), true); diff --git a/commons/lib/TypeSerializer.ts b/commons/lib/TypeSerializer.ts index 841a65cae..5b33bfc73 100644 --- a/commons/lib/TypeSerializer.ts +++ b/commons/lib/TypeSerializer.ts @@ -171,30 +171,23 @@ export default class TypeSerializer { } return result; } + } - if (ArrayBuffer.isView(value)) { - const binary = Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) - .map(byte => String.fromCharCode(byte)) - .join(''); - return { - __type: Types.ArrayBuffer64, - value: globalThis.btoa(binary), - args: { - arrayType: value[Symbol.toStringTag], - byteOffset: value.byteOffset, - byteLength: value.byteLength, - }, - }; - } - if (value instanceof ArrayBuffer) { - const binary = Array.from(new Uint8Array(value)) - .map(byte => String.fromCharCode(byte)) - .join(''); - return { - __type: Types.ArrayBuffer64, - value: globalThis.btoa(binary), - }; - } + if (ArrayBuffer.isView(value)) { + const buffer = new Uint8Array(value.buffer); + return { + __type: Types.ArrayBuffer64, + value: this.Uint8ArrayToBase64String(buffer), + arrayType: value[Symbol.toStringTag], + }; + } + + if (value instanceof ArrayBuffer) { + const buffer = new Uint8Array(value); + return { + __type: Types.ArrayBuffer64, + value: this.Uint8ArrayToBase64String(buffer), + }; } if (type === 'object' && 'toJSON' in value) { @@ -204,6 +197,32 @@ export default class TypeSerializer { return value; } + private static Uint8ArrayToBase64String(value: Uint8Array): string { + const str = Array.from(value) + .map(byte => String.fromCharCode(byte)) + .join(''); + + // base64 is only here for backwards compatability, this migth be removed + // in the future. Also 'binary' is needed to support communication between + // chrome and nodejs: https://stackoverflow.com/questions/23097928/node-js-throws-btoa-is-not-defined-error + if (this.isNodejs) { + return Buffer.from(str, 'binary').toString('base64'); + } + return globalThis.btoa(str); + } + + private static base64StringToUint8Array(value: string): Uint8Array { + const str = this.isNodejs + ? Buffer.from(value, 'base64').toString('binary') + : globalThis.atob(value); + + const uint8Array = new Uint8Array(new ArrayBuffer(str.length)); + for (let i = 0; i < str.length; i++) { + uint8Array[i] = str.charCodeAt(i); + } + return uint8Array; + } + private static reviver(stackMarker: string, key: string, entry: any): any { if (!entry || !entry.__type) return entry; @@ -216,20 +235,15 @@ export default class TypeSerializer { if (type === Types.NegativeInfinity) return Number.NEGATIVE_INFINITY; if (type === Types.DateIso) return new Date(value); if (type === Types.Buffer64 || type === Types.ArrayBuffer64) { - if (this.isNodejs) { - return Buffer.from(value, 'base64'); - } - - const decoded = globalThis.atob(value); - const uint8Array = new Uint8Array(new ArrayBuffer(decoded.length)); - for (let i = 0; i < decoded.length; i++) { - uint8Array[i] = decoded.charCodeAt(i); + const buffer = this.base64StringToUint8Array(value); + if (!entry.arrayType) { + if (this.isNodejs) return Buffer.from(buffer); + // Chrome doesnt have buffer type in this case just dont parse it + // and leave as is. If needed you can still manually handle this + // case but its impossible for use here to know exactly what the goal is. + return entry; } - if (!entry.args) return uint8Array; - - const { arrayType, byteOffset, byteLength } = entry.args; - - return new globalThis[arrayType](uint8Array.buffer, byteOffset, byteLength); + return new globalThis[entry.arrayType](buffer.buffer); } if (type === Types.RegExp) return new RegExp(value[0], value[1]); if (type === Types.Map) { diff --git a/commons/test/TypeSerializer.test.ts b/commons/test/TypeSerializer.test.ts index c16253c6f..277ebefd3 100644 --- a/commons/test/TypeSerializer.test.ts +++ b/commons/test/TypeSerializer.test.ts @@ -1,26 +1,9 @@ import TypeSerializer from '../lib/TypeSerializer'; -import { CanceledPromiseError } from '../interfaces/IPendingWaitEvent'; +import getTestObject from './helpers/getTestObject'; let testObject: any; beforeAll(() => { - testObject = { - name: 'original', - map: new Map([ - ['1', 1], - ['2', 2], - ]), - set: new Set([1, 2, 3, 4]), - regex: /test13234/gi, - date: new Date('2021-03-17T15:41:06.513Z'), - buffer: Buffer.from('This is a test buffer'), - error: new CanceledPromiseError('This is canceled'), - }; - - testObject.nestedObject = { ...testObject, name: 'nested' }; - testObject.nestedArray = [ - { ...testObject, name: 'item1' }, - { ...testObject, name: 'item2' }, - ]; + testObject = getTestObject(); }); test('it should be able to serialize a complex object in nodejs', () => { diff --git a/commons/test/helpers/getTestObject.ts b/commons/test/helpers/getTestObject.ts new file mode 100644 index 000000000..2b5e52fa4 --- /dev/null +++ b/commons/test/helpers/getTestObject.ts @@ -0,0 +1,28 @@ +import { CanceledPromiseError } from '../../interfaces/IPendingWaitEvent'; + +export default function getTestObject() { + const testObject: any = { + name: 'original', + map: new Map([ + ['1', 1], + ['2', 2], + ]), + set: new Set([1, 2, 3, 4]), + regex: /test13234/gi, + date: new Date('2021-03-17T15:41:06.513Z'), + buffer: Buffer.from('This is a test buffer'), + error: new CanceledPromiseError('This is canceled'), + uintArray: Uint32Array.from([4, 5]), + intArray: Int32Array.from([4, 7]), + floatArray: Float32Array.from([4.1]), + float64Array: Float64Array.from([4.1, 5.3]), + }; + + testObject.nestedObject = { ...testObject, name: 'nested' }; + testObject.nestedArray = [ + { ...testObject, name: 'item1' }, + { ...testObject, name: 'item2' }, + ]; + + return testObject; +}