// META: script=nested-cloning-common.js // META: script=support.js // META: script=support-promises.js 'use strict'; // Define constants used to populate object stores and indexes. const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const vowels = 'aeiou'.split(''); // Setup the object store identified by `storeName` to test `getAllKeys()`, // `getAll()` and `getAllRecords()`. // - `callback` is a function that runs after setup with the arguments: `test`, // `connection`, and `expectedRecords`. // - The `expectedRecords` callback argument records all of the keys and values // added to the object store during setup. It is an array of records where // each element contains a `key`, `primaryKey` and `value`. Tests can use // `expectedRecords` to verify the actual results from a get all request. function object_store_get_all_test_setup(storeName, callback, testDescription) { const expectedRecords = []; indexeddb_test( (test, connection) => { switch (storeName) { case 'generated': { // Create an object store with auto-generated, auto-incrementing, // inline keys. const store = connection.createObjectStore( storeName, {autoIncrement: true, keyPath: 'id'}); alphabet.forEach(letter => { store.put({ch: letter}); const generatedKey = alphabet.indexOf(letter) + 1; expectedRecords.push({ key: generatedKey, primaryKey: generatedKey, value: {ch: letter} }); }); return; } case 'out-of-line': { // Create an object store with out-of-line keys. const store = connection.createObjectStore(storeName); alphabet.forEach(letter => { store.put(`value-${letter}`, letter); expectedRecords.push( {key: letter, primaryKey: letter, value: `value-${letter}`}); }); return; } case 'empty': { // Create an empty object store. connection.createObjectStore(storeName); return; } case 'large-values': { // Create an object store with 3 large values. `largeValue()` // generates the value using the key as the seed. The keys start at // 0 and then increment by 1. const store = connection.createObjectStore(storeName); for (let i = 0; i < 3; i++) { const value = largeValue(/*size=*/ wrapThreshold, /*seed=*/ i); store.put(value, i); expectedRecords.push({key: i, primaryKey: i, value}); } return; } } }, // Bind `expectedRecords` to the `indexeddb_test` callback function. (test, connection) => { callback(test, connection, expectedRecords); }, testDescription); } // Similar to `object_store_get_all_test_setup()` above, but also creates an // index named `test_idx` for each object store. function index_get_all_test_setup(storeName, callback, testDescription) { const expectedRecords = []; indexeddb_test( function(test, connection) { switch (storeName) { case 'generated': { // Create an object store with auto-incrementing, inline keys. // Create an index on the uppercase letter property `upper`. const store = connection.createObjectStore( storeName, {autoIncrement: true, keyPath: 'id'}); store.createIndex('test_idx', 'upper'); alphabet.forEach(function(letter) { const value = {ch: letter, upper: letter.toUpperCase()}; store.put(value); const generatedKey = alphabet.indexOf(letter) + 1; expectedRecords.push( {key: value.upper, primaryKey: generatedKey, value}); }); return; } case 'out-of-line': { // Create an object store with out-of-line keys. Create an index on // the uppercase letter property `upper`. const store = connection.createObjectStore(storeName); store.createIndex('test_idx', 'upper'); alphabet.forEach(function(letter) { const value = {ch: letter, upper: letter.toUpperCase()}; store.put(value, letter); expectedRecords.push( {key: value.upper, primaryKey: letter, value}); }); return; } case 'out-of-line-not-unique': { // Create an index on the `half` property, which is not unique, with // two possible values: `first` and `second`. const store = connection.createObjectStore(storeName); store.createIndex('test_idx', 'half'); alphabet.forEach(function(letter) { let half = 'first'; if (letter > 'm') { half = 'second'; } const value = {ch: letter, half}; store.put(value, letter); expectedRecords.push({key: half, primaryKey: letter, value}); }); return } case 'out-of-line-multi': { // Create a multi-entry index on `attribs`, which is an array of // strings. const store = connection.createObjectStore(storeName); store.createIndex('test_idx', 'attribs', {multiEntry: true}); alphabet.forEach(function(letter) { let attrs = []; if (['a', 'e', 'i', 'o', 'u'].indexOf(letter) != -1) { attrs.push('vowel'); } else { attrs.push('consonant'); } if (letter == 'a') { attrs.push('first'); } if (letter == 'z') { attrs.push('last'); } const value = {ch: letter, attribs: attrs}; store.put(value, letter); for (let attr of attrs) { expectedRecords.push({key: attr, primaryKey: letter, value}); } }); return; } case 'empty': { // Create an empty index. const store = connection.createObjectStore(storeName); store.createIndex('test_idx', 'upper'); return; } case 'large-values': { // Create an object store and index with 3 large values and their // seed. Use the large value's seed as the index key. const store = connection.createObjectStore('large-values'); store.createIndex('test_idx', 'seed'); for (let i = 0; i < 3; i++) { const seed = i; const randomValue = largeValue(/*size=*/ wrapThreshold, seed); const recordValue = {seed, randomValue}; store.put(recordValue, i); expectedRecords.push( {key: seed, primaryKey: i, value: recordValue}); } return; } default: { test.assert_unreached(`Unknown storeName: ${storeName}`); } } }, // Bind `expectedRecords` to the `indexeddb_test` callback function. (test, connection) => { callback(test, connection, expectedRecords); }, testDescription); } // Test `getAll()`, `getAllKeys()` or `getAllRecords()` on either `storeName` or // `optionalIndexName` with the given `options`. // - `getAllFunctionName` is name of the function to test, which must be // `getAll`, `getAllKeys` or `getAllRecords`. // - `options` is an `IDBGetAllRecordsOptions ` dictionary that may contain a // `query`, `direction` and `count`. Use `direction` to test // `getAllRecords()` only. `getAll()` and `getAllKeys()` do not support // `direction`. function get_all_test( getAllFunctionName, storeName, optionalIndexName, options, testDescription) { const testGetAllCallback = (test, connection, expectedRecords) => { // Create a transaction and a get all request. const transaction = connection.transaction(storeName, 'readonly'); let queryTarget = transaction.objectStore(storeName); if (optionalIndexName) { queryTarget = queryTarget.index(optionalIndexName); } const request = createGetAllRequest(getAllFunctionName, queryTarget, options); request.onerror = test.unreached_func('The get all request must succeed'); // Verify the results after the get all request completes. request.onsuccess = test.step_func(event => { const actualResults = event.target.result; const expectedResults = calculateExpectedGetAllResults( getAllFunctionName, expectedRecords, options); verifyGetAllResults(getAllFunctionName, actualResults, expectedResults); test.done(); }); }; if (optionalIndexName) { index_get_all_test_setup(storeName, testGetAllCallback, testDescription); } else { object_store_get_all_test_setup( storeName, testGetAllCallback, testDescription); } } function object_store_get_all_keys_test(storeName, options, testDescription) { get_all_test( 'getAllKeys', storeName, /*indexName=*/ undefined, options, testDescription); } function object_store_get_all_values_test(storeName, options, testDescription) { get_all_test( 'getAll', storeName, /*indexName=*/ undefined, options, testDescription); } function object_store_get_all_records_test( storeName, options, testDescription) { get_all_test( 'getAllRecords', storeName, /*indexName=*/ undefined, options, testDescription); } function index_get_all_keys_test(storeName, options, testDescription) { get_all_test('getAllKeys', storeName, 'test_idx', options, testDescription); } function index_get_all_values_test(storeName, options, testDescription) { get_all_test('getAll', storeName, 'test_idx', options, testDescription); } function index_get_all_records_test(storeName, options, testDescription) { get_all_test( 'getAllRecords', storeName, 'test_idx', options, testDescription); } function createGetAllRequest(getAllFunctionName, queryTarget, options) { switch (getAllFunctionName) { case 'getAll': case 'getAllKeys': // `getAll()` and `getAllKeys()` use optional arguments. Omit the // optional arguments when undefined. if (options && options.count) { return queryTarget[getAllFunctionName](options.query, options.count); } if (options && options.query) { return queryTarget[getAllFunctionName](options.query); } return queryTarget[getAllFunctionName](); case 'getAllRecords': return queryTarget.getAllRecords(options); } assert_unreached(`Unknown getAllFunctionName: "${getAllFunctionName}"`); } // Returns the expected results when `getAllFunctionName` is called with // `options` to query an object store or index containing `records`. function calculateExpectedGetAllResults(getAllFunctionName, records, options) { const expectedRecords = filterWithGetAllRecordsOptions(records, options); switch (getAllFunctionName) { case 'getAll': return expectedRecords.map(({value}) => {return value}); case 'getAllKeys': return expectedRecords.map(({primaryKey}) => {return primaryKey}); case 'getAllRecords': return expectedRecords; } assert_unreached(`Unknown getAllFunctionName: "${getAllFunctionName}"`); } // Asserts that the array of results from `getAllFunctionName` matches the // expected results. function verifyGetAllResults(getAllFunctionName, actual, expected) { switch (getAllFunctionName) { case 'getAll': assert_idb_values_equals(actual, expected); return; case 'getAllKeys': assert_array_equals(actual, expected); return; case 'getAllRecords': assert_records_equals(actual, expected); return; } assert_unreached(`Unknown getAllFunctionName: "${getAllFunctionName}"`); } // Returns the array of `records` that satisfy `options`. Tests may use this to // generate expected results. // - `records` is an array of objects where each object has the properties: // `key`, `primaryKey`, and `value`. // - `options` is an `IDBGetAllRecordsOptions ` dictionary that may contain a // `query`, `direction` and `count`. function filterWithGetAllRecordsOptions(records, options) { if (!options) { return records; } // Remove records that don't satisfy the query. if (options.query) { let query = options.query; if (!(query instanceof IDBKeyRange)) { // Create an IDBKeyRange for the query's key value. query = IDBKeyRange.only(query); } records = records.filter(record => query.includes(record.key)); } // Remove duplicate records. if (options.direction === 'nextunique' || options.direction === 'prevunique') { const uniqueRecords = []; records.forEach(record => { if (!uniqueRecords.some( unique => IDBKeyRange.only(unique.key).includes(record.key))) { uniqueRecords.push(record); } }); records = uniqueRecords; } // Reverse the order of the records. if (options.direction === 'prev' || options.direction === 'prevunique') { records = records.slice().reverse(); } // Limit the number of records. if (options.count) { records = records.slice(0, options.count); } return records; } function isArrayOrArrayBufferView(value) { return Array.isArray(value) || ArrayBuffer.isView(value); } // This function compares the string representation of the arrays because // `assert_array_equals()` is too slow for large values. function assert_large_array_equals(actual, expected, description) { const array_string = actual.join(','); const expected_string = expected.join(','); assert_equals(array_string, expected_string, description); } // Verifies a record from the results of `getAllRecords()`. function assert_record_equals(actual_record, expected_record) { assert_class_string( actual_record, 'IDBRecord', 'The record must be an IDBRecord'); assert_idl_attribute( actual_record, 'key', 'The record must have a key attribute'); assert_idl_attribute( actual_record, 'primaryKey', 'The record must have a primaryKey attribute'); assert_idl_attribute( actual_record, 'value', 'The record must have a value attribute'); // Verify the key properties. assert_equals( actual_record.primaryKey, expected_record.primaryKey, 'The record must have the expected primaryKey'); assert_equals( actual_record.key, expected_record.key, 'The record must have the expected key'); // Verify the value. assert_idb_value_equals(actual_record.value, expected_record.value); } // Verifies two IDB values are equal. The expected value may be a primitive, an // object, or an array. function assert_idb_value_equals(actual_value, expected_value) { if (isArrayOrArrayBufferView(expected_value)) { assert_large_array_equals( actual_value, expected_value, 'The record must have the expected value'); } else if (typeof expected_value === 'object') { // Verify each property of the object value. for (let property_name of Object.keys(expected_value)) { if (isArrayOrArrayBufferView(expected_value[property_name])) { // Verify the array property value. assert_large_array_equals( actual_value[property_name], expected_value[property_name], `The record must contain the array value "${ JSON.stringify( expected_value)}" with property "${property_name}"`); } else { // Verify the primitive property value. assert_equals( actual_value[property_name], expected_value[property_name], `The record must contain the value "${ JSON.stringify( expected_value)}" with property "${property_name}"`); } } } else { // Verify the primitive value. assert_equals( actual_value, expected_value, 'The record must have the expected value'); } } // Verifies each record from the results of `getAllRecords()`. function assert_record_equals(actual_record, expected_record) { assert_class_string( actual_record, 'IDBRecord', 'The record must be an IDBRecord'); assert_idl_attribute( actual_record, 'key', 'The record must have a key attribute'); assert_idl_attribute( actual_record, 'primaryKey', 'The record must have a primaryKey attribute'); assert_idl_attribute( actual_record, 'value', 'The record must have a value attribute'); // Verify the attributes: `key`, `primaryKey` and `value`. assert_equals( actual_record.primaryKey, expected_record.primaryKey, 'The record must have the expected primaryKey'); assert_equals( actual_record.key, expected_record.key, 'The record must have the expected key'); assert_idb_value_equals(actual_record.value, expected_record.value); } // Verifies the results from `getAllRecords()`, which is an array of records: // [ // { 'key': key1, 'primaryKey': primary_key1, 'value': value1 }, // { 'key': key2, 'primaryKey': primary_key2, 'value': value2 }, // ... // ] function assert_records_equals(actual_records, expected_records) { assert_true( Array.isArray(actual_records), 'The records must be an array of IDBRecords'); assert_equals( actual_records.length, expected_records.length, 'The records array must contain the expected number of records'); for (let i = 0; i < actual_records.length; i++) { assert_record_equals(actual_records[i], expected_records[i]); } } // Verifies the results from `getAll()`, which is an array of IndexedDB record // values. function assert_idb_values_equals(actual_values, expected_values) { assert_true(Array.isArray(actual_values), 'The values must be an array'); assert_equals( actual_values.length, expected_values.length, 'The values array must contain the expected number of values'); for (let i = 0; i < actual_values.length; i++) { assert_idb_value_equals(actual_values[i], expected_values[i]); } }