| // META: title=Indexed DB and Structured Serializing/Deserializing | |
| // META: timeout=long | |
| // META: script=resources/support-promises.js | |
| // META: script=/common/subset-tests.js | |
| // META: variant=?1-20 | |
| // META: variant=?21-40 | |
| // META: variant=?41-60 | |
| // META: variant=?61-80 | |
| // META: variant=?81-100 | |
| // META: variant=?101-last | |
| 'use strict'; | |
| // Tests Indexed DB coverage of HTML's Safe "passing of structured data" | |
| // https://html.spec.whatwg.org/multipage/structured-data.html | |
| function describe(value) { | |
| let type, str; | |
| if (typeof value === 'object' && value) { | |
| type = Object.getPrototypeOf(value).constructor.name; | |
| // Handle Number(-0), etc. | |
| str = Object.is(value.valueOf(), -0) ? '-0' : String(value); | |
| } else { | |
| type = typeof value; | |
| // Handle primitive -0. | |
| str = Object.is(value, -0) ? '-0' : String(value); | |
| } | |
| return `${type}: ${str}`; | |
| } | |
| function cloneTest(value, verifyFunc) { | |
| subsetTest(promise_test, async t => { | |
| const db = await createDatabase(t, db => { | |
| const store = db.createObjectStore('store'); | |
| // This index is not used, but evaluating key path on each put() | |
| // call will exercise (de)serialization. | |
| store.createIndex('index', 'dummyKeyPath'); | |
| }); | |
| t.add_cleanup(() => { | |
| if (db) { | |
| db.close(); | |
| indexedDB.deleteDatabase(db.name); | |
| } | |
| }); | |
| const tx = db.transaction('store', 'readwrite'); | |
| const store = tx.objectStore('store'); | |
| await promiseForRequest(t, store.put(value, 'key')); | |
| const result = await promiseForRequest(t, store.get('key')); | |
| // Because the async verifyFunc may await async values that are independent | |
| // of the transaction lifetime (ex: blob.text()), we must only await it | |
| // after adding listeners to the transaction. | |
| await promiseForTransaction(t, tx); | |
| await verifyFunc(value, result); | |
| }, describe(value)); | |
| } | |
| // Specialization of cloneTest() for objects, with common asserts. | |
| function cloneObjectTest(value, verifyFunc) { | |
| cloneTest(value, async (orig, clone) => { | |
| assert_not_equals(orig, clone); | |
| assert_equals(typeof clone, 'object'); | |
| assert_equals(Object.getPrototypeOf(orig), Object.getPrototypeOf(clone)); | |
| await verifyFunc(orig, clone); | |
| }); | |
| } | |
| function cloneFailureTest(value) { | |
| subsetTest(promise_test, async t => { | |
| const db = await createDatabase(t, db => { | |
| db.createObjectStore('store'); | |
| }); | |
| t.add_cleanup(() => { | |
| if (db) { | |
| db.close(); | |
| indexedDB.deleteDatabase(db.name); | |
| } | |
| }); | |
| const tx = db.transaction('store', 'readwrite'); | |
| const store = tx.objectStore('store'); | |
| assert_throws_dom('DataCloneError', () => store.put(value, 'key')); | |
| }, 'Not serializable: ' + describe(value)); | |
| } | |
| // | |
| // ECMAScript types | |
| // | |
| // Primitive values: Undefined, Null, Boolean, Number, BigInt, String | |
| const booleans = [false, true]; | |
| const numbers = [ | |
| NaN, | |
| -Infinity, | |
| -Number.MAX_VALUE, | |
| -0xffffffff, | |
| -0x80000000, | |
| -0x7fffffff, | |
| -1, | |
| -Number.MIN_VALUE, | |
| -0, | |
| 0, | |
| 1, | |
| Number.MIN_VALUE, | |
| 0x7fffffff, | |
| 0x80000000, | |
| 0xffffffff, | |
| Number.MAX_VALUE, | |
| Infinity, | |
| ]; | |
| const bigints = [ | |
| -12345678901234567890n, | |
| -1n, | |
| 0n, | |
| 1n, | |
| 12345678901234567890n, | |
| ]; | |
| const strings = [ | |
| '', | |
| 'this is a sample string', | |
| 'null(\0)', | |
| ]; | |
| [undefined, null].concat(booleans, numbers, bigints, strings) | |
| .forEach(value => cloneTest(value, (orig, clone) => { | |
| assert_equals(orig, clone); | |
| })); | |
| // "Primitive" Objects (Boolean, Number, BigInt, String) | |
| [].concat(booleans, numbers, bigints, strings) | |
| .forEach(value => cloneObjectTest(Object(value), (orig, clone) => { | |
| assert_equals(orig.valueOf(), clone.valueOf()); | |
| })); | |
| // Dates | |
| [ | |
| new Date(-1e13), | |
| new Date(-1e12), | |
| new Date(-1e9), | |
| new Date(-1e6), | |
| new Date(-1e3), | |
| new Date(0), | |
| new Date(1e3), | |
| new Date(1e6), | |
| new Date(1e9), | |
| new Date(1e12), | |
| new Date(1e13) | |
| ].forEach(value => cloneTest(value, (orig, clone) => { | |
| assert_not_equals(orig, clone); | |
| assert_equals(typeof clone, 'object'); | |
| assert_equals(Object.getPrototypeOf(orig), Object.getPrototypeOf(clone)); | |
| assert_equals(orig.valueOf(), clone.valueOf()); | |
| })); | |
| // Regular Expressions | |
| [ | |
| new RegExp(), | |
| /abc/, | |
| /abc/g, | |
| /abc/i, | |
| /abc/gi, | |
| /abc/m, | |
| /abc/mg, | |
| /abc/mi, | |
| /abc/mgi, | |
| /abc/gimsuy, | |
| ].forEach(value => cloneObjectTest(value, (orig, clone) => { | |
| assert_equals(orig.toString(), clone.toString()); | |
| })); | |
| // ArrayBuffer | |
| cloneObjectTest(new Uint8Array([0, 1, 254, 255]).buffer, (orig, clone) => { | |
| assert_array_equals(new Uint8Array(orig), new Uint8Array(clone)); | |
| }); | |
| // TODO SharedArrayBuffer | |
| // Array Buffer Views | |
| let byteArrays = [ | |
| new Uint8Array([]), | |
| new Uint8Array([0, 1, 254, 255]), | |
| new Uint16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), | |
| new Uint32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), | |
| new Int8Array([0, 1, 254, 255]), | |
| new Int16Array([0x0000, 0x0001, 0xFFFE, 0xFFFF]), | |
| new Int32Array([0x00000000, 0x00000001, 0xFFFFFFFE, 0xFFFFFFFF]), | |
| new Uint8ClampedArray([0, 1, 254, 255]), | |
| new Float32Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN]), | |
| new Float64Array([-Infinity, -Number.MAX_VALUE, -Number.MIN_VALUE, 0, | |
| Number.MIN_VALUE, Number.MAX_VALUE, Infinity, NaN]) | |
| ] | |
| if (typeof Float16Array !== 'undefined') { | |
| byteArrays.push( | |
| new Float16Array([-Infinity, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, Infinity, NaN])); | |
| } | |
| byteArrays.forEach(value => cloneObjectTest(value, (orig, clone) => { | |
| assert_array_equals(orig, clone); | |
| })); | |
| // Map | |
| cloneObjectTest(new Map([[1,2],[3,4]]), (orig, clone) => { | |
| assert_array_equals([...orig.keys()], [...clone.keys()]); | |
| assert_array_equals([...orig.values()], [...clone.values()]); | |
| }); | |
| // Set | |
| cloneObjectTest(new Set([1,2,3,4]), (orig, clone) => { | |
| assert_array_equals([...orig.values()], [...clone.values()]); | |
| }); | |
| // Error | |
| [ | |
| new Error(), | |
| new Error('abc', 'def'), | |
| new EvalError(), | |
| new EvalError('ghi', 'jkl'), | |
| new RangeError(), | |
| new RangeError('ghi', 'jkl'), | |
| new ReferenceError(), | |
| new ReferenceError('ghi', 'jkl'), | |
| new SyntaxError(), | |
| new SyntaxError('ghi', 'jkl'), | |
| new TypeError(), | |
| new TypeError('ghi', 'jkl'), | |
| new URIError(), | |
| new URIError('ghi', 'jkl'), | |
| ].forEach(value => cloneObjectTest(value, (orig, clone) => { | |
| assert_equals(orig.name, clone.name); | |
| assert_equals(orig.message, clone.message); | |
| })); | |
| // Arrays | |
| [ | |
| [], | |
| [1,2,3], | |
| Object.assign( | |
| ['foo', 'bar'], | |
| {10: true, 11: false, 20: 123, 21: 456, 30: null}), | |
| Object.assign( | |
| ['foo', 'bar'], | |
| {a: true, b: false, foo: 123, bar: 456, '': null}), | |
| ].forEach(value => cloneObjectTest(value, (orig, clone) => { | |
| assert_array_equals(orig, clone); | |
| assert_array_equals(Object.keys(orig), Object.keys(clone)); | |
| Object.keys(orig).forEach(key => { | |
| assert_equals(orig[key], clone[key], `Property ${key}`); | |
| }); | |
| })); | |
| // Objects | |
| cloneObjectTest({foo: true, bar: false}, (orig, clone) => { | |
| assert_array_equals(Object.keys(orig), Object.keys(clone)); | |
| Object.keys(orig).forEach(key => { | |
| assert_equals(orig[key], clone[key], `Property ${key}`); | |
| }); | |
| }); | |
| // | |
| // [Serializable] Platform objects | |
| // | |
| // TODO: Test these additional interfaces: | |
| // * DOMQuad | |
| // * DOMException | |
| // * RTCCertificate | |
| // Geometry types | |
| [ | |
| new DOMMatrix(), | |
| new DOMMatrixReadOnly(), | |
| new DOMPoint(), | |
| new DOMPointReadOnly(), | |
| new DOMRect, | |
| new DOMRectReadOnly(), | |
| ].forEach(value => cloneObjectTest(value, (orig, clone) => { | |
| Object.keys(Object.getPrototypeOf(orig)).forEach(key => { | |
| assert_equals(orig[key], clone[key], `Property ${key}`); | |
| }); | |
| })); | |
| // ImageData | |
| const image_data = new ImageData(8, 8); | |
| for (let i = 0; i < 256; ++i) { | |
| image_data.data[i] = i; | |
| } | |
| cloneObjectTest(image_data, (orig, clone) => { | |
| assert_equals(orig.width, clone.width); | |
| assert_equals(orig.height, clone.height); | |
| assert_array_equals(orig.data, clone.data); | |
| }); | |
| // Blob | |
| cloneObjectTest( | |
| new Blob(['This is a test.'], {type: 'a/b'}), | |
| async (orig, clone) => { | |
| assert_equals(orig.size, clone.size); | |
| assert_equals(orig.type, clone.type); | |
| assert_equals(await orig.text(), await clone.text()); | |
| }); | |
| // File | |
| cloneObjectTest( | |
| new File(['This is a test.'], 'foo.txt', {type: 'c/d'}), | |
| async (orig, clone) => { | |
| assert_equals(orig.size, clone.size); | |
| assert_equals(orig.type, clone.type); | |
| assert_equals(orig.name, clone.name); | |
| assert_equals(orig.lastModified, clone.lastModified); | |
| assert_equals(await orig.text(), await clone.text()); | |
| }); | |
| // FileList - exposed in Workers, but not constructable. | |
| if ('document' in self) { | |
| // TODO: Test with populated list. | |
| cloneObjectTest( | |
| Object.assign(document.createElement('input'), | |
| {type: 'file', multiple: true}).files, | |
| async (orig, clone) => { | |
| assert_equals(orig.length, clone.length); | |
| }); | |
| } | |
| // | |
| // Non-serializable types | |
| // | |
| [ | |
| // ECMAScript types | |
| function() {}, | |
| Symbol('desc'), | |
| // Non-[Serializable] platform objects | |
| self, | |
| new Event(''), | |
| new MessageChannel() | |
| ].forEach(cloneFailureTest); |