| // META: title=Indexed DB and Structured Serializing/Deserializing |
| // META: timeout=long |
| // META: script=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 |
| |
| // 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')); |
| await verifyFunc(value, result); |
| await promiseForTransaction(t, tx); |
| }, 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, 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 |
| [ |
| 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]) |
| ].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); |