488 lines
18 KiB
JavaScript
488 lines
18 KiB
JavaScript
// 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]);
|
|
}
|
|
}
|