tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

support-get-all.js (21927B)


      1 // META: script=nested-cloning-common.js
      2 // META: script=support.js
      3 // META: script=support-promises.js
      4 
      5 'use strict';
      6 
      7 // Define constants used to populate object stores and indexes.
      8 const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
      9 const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
     10 const vowels = 'aeiou'.split('');
     11 
     12 // Setup the object store identified by `storeName` to test `getAllKeys()`,
     13 // `getAll()` and `getAllRecords()`.
     14 //  - `callback` is a function that runs after setup with the arguments: `test`,
     15 //    `connection`, and `expectedRecords`.
     16 //  - The `expectedRecords` callback argument records all of the keys and values
     17 //    added to the object store during setup.  It is an array of records where
     18 //    each element contains a `key`, `primaryKey` and `value`.  Tests can use
     19 //    `expectedRecords` to verify the actual results from a get all request.
     20 function object_store_get_all_test_setup(storeName, callback, testDescription) {
     21  const expectedRecords = [];
     22 
     23  indexeddb_test(
     24      (test, connection) => {
     25        switch (storeName) {
     26          case 'generated': {
     27            // Create an object store with auto-generated, auto-incrementing,
     28            // inline keys.
     29            const store = connection.createObjectStore(
     30                storeName, {autoIncrement: true, keyPath: 'id'});
     31            alphabet.forEach(letter => {
     32              store.put({ch: letter});
     33 
     34              const generatedKey = alphabet.indexOf(letter) + 1;
     35              expectedRecords.push({
     36                key: generatedKey,
     37                primaryKey: generatedKey,
     38                value: {ch: letter}
     39              });
     40            });
     41            return;
     42          }
     43          case 'out-of-line': {
     44            // Create an object store with out-of-line keys.
     45            const store = connection.createObjectStore(storeName);
     46            alphabet.forEach(letter => {
     47              store.put(`value-${letter}`, letter);
     48 
     49              expectedRecords.push(
     50                  {key: letter, primaryKey: letter, value: `value-${letter}`});
     51            });
     52            return;
     53          }
     54          case 'empty': {
     55            // Create an empty object store.
     56            connection.createObjectStore(storeName);
     57            return;
     58          }
     59          case 'large-values': {
     60            // Create an object store with 3 large values. `largeValue()`
     61            // generates the value using the key as the seed.  The keys start at
     62            // 0 and then increment by 1.
     63            const store = connection.createObjectStore(storeName);
     64            for (let i = 0; i < 3; i++) {
     65              const value = largeValue(/*size=*/ wrapThreshold, /*seed=*/ i);
     66              store.put(value, i);
     67 
     68              expectedRecords.push({key: i, primaryKey: i, value});
     69            }
     70            return;
     71          }
     72        }
     73      },
     74      // Bind `expectedRecords` to the `indexeddb_test` callback function.
     75      (test, connection) => {
     76        callback(test, connection, expectedRecords);
     77      },
     78      testDescription);
     79 }
     80 
     81 // Similar to `object_store_get_all_test_setup()` above, but also creates an
     82 // index named `test_idx` for each object store.
     83 function index_get_all_test_setup(storeName, callback, testDescription) {
     84  const expectedRecords = [];
     85 
     86  indexeddb_test(
     87      function(test, connection) {
     88        switch (storeName) {
     89          case 'generated': {
     90            // Create an object store with auto-incrementing, inline keys.
     91            // Create an index on the uppercase letter property `upper`.
     92            const store = connection.createObjectStore(
     93                storeName, {autoIncrement: true, keyPath: 'id'});
     94            store.createIndex('test_idx', 'upper');
     95            alphabet.forEach(function(letter) {
     96              const value = {ch: letter, upper: letter.toUpperCase()};
     97              store.put(value);
     98 
     99              const generatedKey = alphabet.indexOf(letter) + 1;
    100              expectedRecords.push(
    101                  {key: value.upper, primaryKey: generatedKey, value});
    102            });
    103            return;
    104          }
    105          case 'out-of-line': {
    106            // Create an object store with out-of-line keys.  Create an index on
    107            // the uppercase letter property `upper`.
    108            const store = connection.createObjectStore(storeName);
    109            store.createIndex('test_idx', 'upper');
    110            alphabet.forEach(function(letter) {
    111              const value = {ch: letter, upper: letter.toUpperCase()};
    112              store.put(value, letter);
    113 
    114              expectedRecords.push(
    115                  {key: value.upper, primaryKey: letter, value});
    116            });
    117            return;
    118          }
    119          case 'out-of-line-not-unique': {
    120            // Create an index on the `half` property, which is not unique, with
    121            // two possible values: `first` and `second`.
    122            const store = connection.createObjectStore(storeName);
    123            store.createIndex('test_idx', 'half');
    124            alphabet.forEach(function(letter) {
    125              let half = 'first';
    126              if (letter > 'm') {
    127                half = 'second';
    128              }
    129 
    130              const value = {ch: letter, half};
    131              store.put(value, letter);
    132 
    133              expectedRecords.push({key: half, primaryKey: letter, value});
    134            });
    135            return
    136          }
    137          case 'out-of-line-multi': {
    138            // Create a multi-entry index on `attribs`, which is an array of
    139            // strings.
    140            const store = connection.createObjectStore(storeName);
    141            store.createIndex('test_idx', 'attribs', {multiEntry: true});
    142            alphabet.forEach(function(letter) {
    143              let attrs = [];
    144              if (['a', 'e', 'i', 'o', 'u'].indexOf(letter) != -1) {
    145                attrs.push('vowel');
    146              } else {
    147                attrs.push('consonant');
    148              }
    149              if (letter == 'a') {
    150                attrs.push('first');
    151              }
    152              if (letter == 'z') {
    153                attrs.push('last');
    154              }
    155              const value = {ch: letter, attribs: attrs};
    156              store.put(value, letter);
    157 
    158              for (let attr of attrs) {
    159                expectedRecords.push({key: attr, primaryKey: letter, value});
    160              }
    161            });
    162            return;
    163          }
    164          case 'empty': {
    165            // Create an empty index.
    166            const store = connection.createObjectStore(storeName);
    167            store.createIndex('test_idx', 'upper');
    168            return;
    169          }
    170          case 'large-values': {
    171            // Create an object store and index with 3 large values and their
    172            // seed.  Use the large value's seed as the index key.
    173            const store = connection.createObjectStore('large-values');
    174            store.createIndex('test_idx', 'seed');
    175            for (let i = 0; i < 3; i++) {
    176              const seed = i;
    177              const randomValue = largeValue(/*size=*/ wrapThreshold, seed);
    178              const recordValue = {seed, randomValue};
    179              store.put(recordValue, i);
    180 
    181              expectedRecords.push(
    182                  {key: seed, primaryKey: i, value: recordValue});
    183            }
    184            return;
    185          }
    186          default: {
    187            test.assert_unreached(`Unknown storeName: ${storeName}`);
    188          }
    189        }
    190      },
    191      // Bind `expectedRecords` to the `indexeddb_test` callback function.
    192      (test, connection) => {
    193        callback(test, connection, expectedRecords);
    194      },
    195      testDescription);
    196 }
    197 
    198 // Test `getAll()`, `getAllKeys()` or `getAllRecords()` on either `storeName` or
    199 // `optionalIndexName` with the given `options`.
    200 //
    201 //  - `getAllFunctionName` is name of the function to test, which must be
    202 //     `getAll`, `getAllKeys` or `getAllRecords`.
    203 //
    204 //  - `options` is an `IDBGetAllOptions` dictionary that may contain a  `query`,
    205 //    `direction` and `count`.
    206 //
    207 // - `shouldUseDictionaryArgument` is true when testing the get all function
    208 //    overloads that takes an `IDBGetAllOptions` dictionary.  False tests the
    209 //    overloads that take two optional arguments: `query` and `count`.
    210 function get_all_test(
    211    getAllFunctionName, storeName, optionalIndexName, options,
    212    shouldUseDictionaryArgument, testDescription) {
    213  const testGetAllCallback = (test, connection, expectedRecords) => {
    214    // Create a transaction and a get all request.
    215    const transaction = connection.transaction(storeName, 'readonly');
    216    let queryTarget = transaction.objectStore(storeName);
    217    if (optionalIndexName) {
    218      queryTarget = queryTarget.index(optionalIndexName);
    219    }
    220    const request = createGetAllRequest(
    221        getAllFunctionName, queryTarget, options, shouldUseDictionaryArgument);
    222    request.onerror = test.unreached_func('The get all request must succeed');
    223 
    224    // Verify the results after the get all request completes.
    225    request.onsuccess = test.step_func(event => {
    226      const actualResults = event.target.result;
    227      const expectedResults = calculateExpectedGetAllResults(
    228          getAllFunctionName, expectedRecords, options);
    229      verifyGetAllResults(getAllFunctionName, actualResults, expectedResults);
    230      test.done();
    231    });
    232  };
    233 
    234  if (optionalIndexName) {
    235    index_get_all_test_setup(storeName, testGetAllCallback, testDescription);
    236  } else {
    237    object_store_get_all_test_setup(
    238        storeName, testGetAllCallback, testDescription);
    239  }
    240 }
    241 
    242 function object_store_get_all_keys_test(storeName, options, testDescription) {
    243  get_all_test(
    244      'getAllKeys', storeName, /*indexName=*/ undefined, options,
    245      /*shouldUseDictionaryArgument=*/ false, testDescription);
    246 }
    247 
    248 function object_store_get_all_values_test(storeName, options, testDescription) {
    249  get_all_test(
    250      'getAll', storeName, /*indexName=*/ undefined, options,
    251      /*shouldUseDictionaryArgument=*/ false, testDescription);
    252 }
    253 
    254 function object_store_get_all_values_with_options_test(
    255    storeName, options, testDescription) {
    256  get_all_test(
    257      'getAll', storeName, /*indexName=*/ undefined, options,
    258      /*shouldUseDictionaryArgument=*/ true, testDescription);
    259 }
    260 
    261 function object_store_get_all_keys_with_options_test(
    262    storeName, options, testDescription) {
    263  get_all_test(
    264      'getAllKeys', storeName, /*indexName=*/ undefined, options,
    265      /*shouldUseDictionaryArgument=*/ true, testDescription);
    266 }
    267 
    268 function object_store_get_all_records_test(
    269    storeName, options, testDescription) {
    270  get_all_test(
    271      'getAllRecords', storeName, /*indexName=*/ undefined, options,
    272      /*shouldUseDictionaryArgument=*/ true, testDescription);
    273 }
    274 
    275 function index_get_all_keys_test(storeName, options, testDescription) {
    276  get_all_test(
    277      'getAllKeys', storeName, 'test_idx', options,
    278      /*shouldUseDictionaryArgument=*/ false, testDescription);
    279 }
    280 
    281 function index_get_all_keys_with_options_test(
    282    storeName, options, testDescription) {
    283  get_all_test(
    284      'getAllKeys', storeName, 'test_idx', options,
    285      /*shouldUseDictionaryArgument=*/ true, testDescription);
    286 }
    287 
    288 function index_get_all_values_test(storeName, options, testDescription) {
    289  get_all_test(
    290      'getAll', storeName, 'test_idx', options,
    291      /*shouldUseDictionaryArgument=*/ false, testDescription);
    292 }
    293 
    294 function index_get_all_values_with_options_test(
    295    storeName, options, testDescription) {
    296  get_all_test(
    297      'getAll', storeName, 'test_idx', options,
    298      /*shouldUseDictionaryArgument=*/ true, testDescription);
    299 }
    300 
    301 function index_get_all_records_test(storeName, options, testDescription) {
    302  get_all_test(
    303      'getAllRecords', storeName, 'test_idx', options,
    304      /*shouldUseDictionaryArgument=*/ true, testDescription);
    305 }
    306 
    307 function createGetAllRequest(
    308    getAllFunctionName, queryTarget, options, shouldUseDictionaryArgument) {
    309  if (options && shouldUseDictionaryArgument) {
    310    assert_true(
    311        'getAllRecords' in queryTarget,
    312        `"${queryTarget}" must support "getAllRecords()" to use an "IDBGetAllOptions" dictionary with "${
    313            getAllFunctionName}".`);
    314    return queryTarget[getAllFunctionName](options);
    315  }
    316  // `getAll()` and `getAllKeys()` use optional arguments.  Omit the
    317  // optional arguments when undefined.
    318  if (options && options.count) {
    319    return queryTarget[getAllFunctionName](options.query, options.count);
    320  }
    321  if (options && options.query) {
    322    return queryTarget[getAllFunctionName](options.query);
    323  }
    324  return queryTarget[getAllFunctionName]();
    325 }
    326 
    327 // Returns the expected results when `getAllFunctionName` is called with
    328 // `options` to query an object store or index containing `records`.
    329 function calculateExpectedGetAllResults(getAllFunctionName, records, options) {
    330  const expectedRecords = filterWithGetAllRecordsOptions(records, options);
    331  switch (getAllFunctionName) {
    332    case 'getAll':
    333      return expectedRecords.map(({value}) => {return value});
    334    case 'getAllKeys':
    335      return expectedRecords.map(({primaryKey}) => {return primaryKey});
    336    case 'getAllRecords':
    337      return expectedRecords;
    338  }
    339  assert_unreached(`Unknown getAllFunctionName: "${getAllFunctionName}"`);
    340 }
    341 
    342 // Asserts that the array of results from `getAllFunctionName` matches the
    343 // expected results.
    344 function verifyGetAllResults(getAllFunctionName, actual, expected) {
    345  switch (getAllFunctionName) {
    346    case 'getAll':
    347      assert_idb_values_equals(actual, expected);
    348      return;
    349    case 'getAllKeys':
    350      assert_array_equals(actual, expected);
    351      return;
    352    case 'getAllRecords':
    353      assert_records_equals(actual, expected);
    354      return;
    355  }
    356  assert_unreached(`Unknown getAllFunctionName: "${getAllFunctionName}"`);
    357 }
    358 
    359 // Returns the array of `records` that satisfy `options`.  Tests may use this to
    360 // generate expected results.
    361 //  - `records` is an array of objects where each object has the properties:
    362 //    `key`, `primaryKey`, and `value`.
    363 //  - `options` is an `IDBGetAllRecordsOptions ` dictionary that may contain a
    364 //    `query`, `direction` and `count`.
    365 function filterWithGetAllRecordsOptions(records, options) {
    366  if (!options) {
    367    return records;
    368  }
    369 
    370  // Remove records that don't satisfy the query.
    371  if (options.query) {
    372    let query = options.query;
    373    if (!(query instanceof IDBKeyRange)) {
    374      // Create an IDBKeyRange for the query's key value.
    375      query = IDBKeyRange.only(query);
    376    }
    377    records = records.filter(record => query.includes(record.key));
    378  }
    379 
    380  // Remove duplicate records.
    381  if (options.direction === 'nextunique' ||
    382      options.direction === 'prevunique') {
    383    const uniqueRecords = [];
    384    records.forEach(record => {
    385      if (!uniqueRecords.some(
    386              unique => IDBKeyRange.only(unique.key).includes(record.key))) {
    387        uniqueRecords.push(record);
    388      }
    389    });
    390    records = uniqueRecords;
    391  }
    392 
    393  // Reverse the order of the records.
    394  if (options.direction === 'prev' || options.direction === 'prevunique') {
    395    records = records.slice().reverse();
    396  }
    397 
    398  // Limit the number of records.
    399  if (options.count) {
    400    records = records.slice(0, options.count);
    401  }
    402  return records;
    403 }
    404 
    405 function isArrayOrArrayBufferView(value) {
    406  return Array.isArray(value) || ArrayBuffer.isView(value);
    407 }
    408 
    409 // This function compares the string representation of the arrays because
    410 // `assert_array_equals()` is too slow for large values.
    411 function assert_large_array_equals(actual, expected, description) {
    412  const array_string = actual.join(',');
    413  const expected_string = expected.join(',');
    414  assert_equals(array_string, expected_string, description);
    415 }
    416 
    417 // Verifies two IDB values are equal.  The expected value may be a primitive, an
    418 // object, or an array.
    419 function assert_idb_value_equals(actual_value, expected_value) {
    420  if (isArrayOrArrayBufferView(expected_value)) {
    421    assert_large_array_equals(
    422        actual_value, expected_value,
    423        'The record must have the expected value');
    424  } else if (typeof expected_value === 'object') {
    425    // Verify each property of the object value.
    426    for (let property_name of Object.keys(expected_value)) {
    427      if (isArrayOrArrayBufferView(expected_value[property_name])) {
    428        // Verify the array property value.
    429        assert_large_array_equals(
    430            actual_value[property_name], expected_value[property_name],
    431            `The record must contain the array value "${
    432                JSON.stringify(
    433                    expected_value)}" with property "${property_name}"`);
    434      } else {
    435        // Verify the primitive property value.
    436        assert_equals(
    437            actual_value[property_name], expected_value[property_name],
    438            `The record must contain the value "${
    439                JSON.stringify(
    440                    expected_value)}" with property "${property_name}"`);
    441      }
    442    }
    443  } else {
    444    // Verify the primitive value.
    445    assert_equals(
    446        actual_value, expected_value,
    447        'The record must have the expected value');
    448  }
    449 }
    450 
    451 // Verifies each record from the results of `getAllRecords()`.
    452 function assert_record_equals(actual_record, expected_record) {
    453  assert_class_string(
    454      actual_record, 'IDBRecord', 'The record must be an IDBRecord');
    455  assert_idl_attribute(
    456      actual_record, 'key', 'The record must have a key attribute');
    457  assert_idl_attribute(
    458      actual_record, 'primaryKey',
    459      'The record must have a primaryKey attribute');
    460  assert_idl_attribute(
    461      actual_record, 'value', 'The record must have a value attribute');
    462 
    463  // Verify the attributes: `key`, `primaryKey` and `value`.
    464  assert_equals(
    465      actual_record.primaryKey, expected_record.primaryKey,
    466      'The record must have the expected primaryKey');
    467  assert_equals(
    468      actual_record.key, expected_record.key,
    469      'The record must have the expected key');
    470  assert_idb_value_equals(actual_record.value, expected_record.value);
    471 }
    472 
    473 // Verifies the results from `getAllRecords()`, which is an array of records:
    474 // [
    475 //   { 'key': key1, 'primaryKey': primary_key1, 'value': value1 },
    476 //   { 'key': key2, 'primaryKey': primary_key2, 'value': value2 },
    477 //   ...
    478 // ]
    479 function assert_records_equals(actual_records, expected_records) {
    480  assert_true(
    481      Array.isArray(actual_records),
    482      'The records must be an array of IDBRecords');
    483  assert_equals(
    484      actual_records.length, expected_records.length,
    485      'The records array must contain the expected number of records');
    486 
    487  for (let i = 0; i < actual_records.length; i++) {
    488    assert_record_equals(actual_records[i], expected_records[i]);
    489  }
    490 }
    491 
    492 // Verifies the results from `getAll()`, which is an array of IndexedDB record
    493 // values.
    494 function assert_idb_values_equals(actual_values, expected_values) {
    495  assert_true(Array.isArray(actual_values), 'The values must be an array');
    496  assert_equals(
    497      actual_values.length, expected_values.length,
    498      'The values array must contain the expected number of values');
    499 
    500  for (let i = 0; i < actual_values.length; i++) {
    501    assert_idb_value_equals(actual_values[i], expected_values[i]);
    502  }
    503 }
    504 
    505 // Test passing both an options dictionary  and a count to `getAll()` and
    506 // `getAllKeys()`.  The get all request must ignore the `count` argument, using
    507 //  count from the options dictionary instead.
    508 function get_all_with_options_and_count_test(
    509    getAllFunctionName, storeName, optionalIndexName, testDescription) {
    510  // Set up the object store or index to query.
    511  const setupFunction = optionalIndexName ? index_get_all_test_setup :
    512                                            object_store_get_all_test_setup;
    513 
    514  setupFunction(storeName, (test, connection, expectedRecords) => {
    515    const transaction = connection.transaction(storeName, 'readonly');
    516    let queryTarget = transaction.objectStore(storeName);
    517    if (optionalIndexName) {
    518      queryTarget = queryTarget.index(optionalIndexName);
    519    }
    520 
    521    const options = {count: 10};
    522    const request = queryTarget[getAllFunctionName](options, /*count=*/ 17);
    523 
    524    request.onerror =
    525        test.unreached_func(`"${getAllFunctionName}()" request must succeed.`);
    526 
    527    request.onsuccess = test.step_func(event => {
    528      const expectedResults = calculateExpectedGetAllResults(
    529          getAllFunctionName, expectedRecords, options);
    530 
    531      const actualResults = event.target.result;
    532      verifyGetAllResults(getAllFunctionName, actualResults, expectedResults);
    533 
    534      test.done();
    535    });
    536  }, testDescription);
    537 }
    538 
    539 // Get all operations must throw a `DataError` exception for invalid query keys.
    540 // See `get_all_test()` above for a description of the parameters.
    541 function get_all_with_invalid_keys_test(
    542    getAllFunctionName, storeName, optionalIndexName,
    543    shouldUseDictionaryArgument, testDescription) {
    544  // Set up the object store or index to query.
    545  const setupFunction = optionalIndexName ? index_get_all_test_setup :
    546                                            object_store_get_all_test_setup;
    547 
    548  setupFunction(storeName, (test, connection, expectedRecords) => {
    549    const transaction = connection.transaction(storeName, 'readonly');
    550    let queryTarget = transaction.objectStore(storeName);
    551    if (optionalIndexName) {
    552      queryTarget = queryTarget.index(optionalIndexName);
    553    }
    554 
    555    const invalidKeys = [
    556      {
    557        description: 'Date(NaN)',
    558        value: new Date(NaN),
    559      },
    560      {
    561        description: 'Array',
    562        value: [{}],
    563      },
    564      {
    565        description: 'detached TypedArray',
    566        value: createDetachedArrayBuffer(),
    567      },
    568      {
    569        description: 'detached ArrayBuffer',
    570        value: createDetachedArrayBuffer().buffer
    571      },
    572    ];
    573    invalidKeys.forEach(({description, value}) => {
    574      const argument = shouldUseDictionaryArgument ? {query: value} : value;
    575      assert_throws_dom('DataError', () => {
    576        queryTarget[getAllFunctionName](argument);
    577      }, `An invalid ${description} key must throw an exception.`);
    578    });
    579    test.done();
    580  }, testDescription);
    581 }