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 }