| <!doctype html> | |
| <meta charset="utf8"> | |
| <meta name="timeout" content="long"> | |
| <title>IndexedDB: request result events are delivered in order</title> | |
| <link rel="help" href="https://w3c.github.io/IndexedDB/#abort-transaction"> | |
| <link rel="author" href="pwnall@chromium.org" title="Victor Costan"> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="support-promises.js"></script> | |
| <script> | |
| 'use strict'; | |
| // Should be large enough to trigger large value handling in the IndexedDB | |
| // engines that have special code paths for large values. | |
| const wrapThreshold = 128 * 1024; | |
| function populateStore(store) { | |
| store.put({id: 1, key: 'k1', value: largeValue(wrapThreshold, 1) }); | |
| store.put({id: 2, key: 'k2', value: ['small-2'] }); | |
| store.put({id: 3, key: 'k3', value: largeValue(wrapThreshold, 3) }); | |
| store.put({id: 4, key: 'k4', value: ['small-4'] }); | |
| } | |
| // Assigns cursor indexes for operations that require open cursors. | |
| // | |
| // Returns the number of open cursors required to perform all operations. | |
| function assignCursors(operations) { | |
| return cursorCount; | |
| } | |
| // Opens index cursors for operations that require open cursors. | |
| // | |
| // onsuccess is called if all cursors are opened successfully. Otherwise, | |
| // onerror will be called at least once. | |
| function openCursors(testCase, index, operations, onerror, onsuccess) { | |
| let pendingCursors = 0; | |
| for (let operation of operations) { | |
| const opcode = operation[0]; | |
| const primaryKey = operation[1]; | |
| let request; | |
| switch (opcode) { | |
| case 'continue': | |
| request = index.openCursor( | |
| IDBKeyRange.lowerBound(`k${primaryKey - 1}`)); | |
| break; | |
| case 'continue-empty': | |
| // k4 is the last key in the data set, so calling continue() will get | |
| // the cursor past the end of the store. | |
| request = index.openCursor(IDBKeyRange.lowerBound('k4')); | |
| break; | |
| default: | |
| continue; | |
| } | |
| operation[2] = request; | |
| ++pendingCursors; | |
| request.onsuccess = testCase.step_func(() => { | |
| --pendingCursors; | |
| if (!pendingCursors) | |
| onsuccess(); | |
| }); | |
| request.onerror = testCase.step_func(onerror); | |
| } | |
| if (!pendingCursors) | |
| onsuccess(); | |
| } | |
| function doOperation(testCase, store, index, operation, requestId, results) { | |
| const opcode = operation[0]; | |
| const primaryKey = operation[1]; | |
| const cursor = operation[2]; | |
| return new Promise((resolve, reject) => { | |
| let request; | |
| switch (opcode) { | |
| case 'add': // Tests returning a primary key. | |
| request = store.add( | |
| { key: `k${primaryKey}`, value: [`small-${primaryKey}`] }); | |
| break; | |
| case 'put': // Tests returning a primary key. | |
| request = store.put( | |
| { key: `k${primaryKey}`, value: [`small-${primaryKey}`] }); | |
| break; | |
| case 'put-with-id': // Tests returning success or a primary key. | |
| request = store.put( | |
| { key: `k${primaryKey}`, value: [`small-${primaryKey}`], | |
| id: primaryKey }); | |
| break; | |
| case 'get': // Tests returning a value. | |
| case 'get-empty': // Tests returning undefined. | |
| request = store.get(primaryKey); | |
| break; | |
| case 'getall': // Tests returning an array of values. | |
| request = store.getAll(); | |
| break; | |
| case 'error': // Tests returning an error. | |
| request = store.put( | |
| { key: `k${primaryKey}`, value: [`small-${primaryKey}`] }); | |
| request.onerror = testCase.step_func(event => { | |
| event.preventDefault(); | |
| results.push([requestId, request.error]); | |
| resolve(); | |
| }); | |
| request.onsuccess = testCase.step_func(() => { | |
| reject(new Error('put with duplicate primary key succeded')); | |
| }); | |
| break; | |
| case 'continue': // Tests returning a key, primary key, and value. | |
| request = cursor; | |
| cursor.result.continue(); | |
| request.onsuccess = testCase.step_func(() => { | |
| const result = request.result; | |
| results.push( | |
| [requestId, result.key, result.primaryKey, result.value]); | |
| resolve(); | |
| }); | |
| request.onerror = null; | |
| break; | |
| case 'open': // Tests returning a cursor, key, primary key, and value. | |
| request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`)); | |
| request.onsuccess = testCase.step_func(() => { | |
| const result = request.result; | |
| results.push( | |
| [requestId, result.key, result.primaryKey, result.value]); | |
| resolve(); | |
| }); | |
| break; | |
| case 'continue-empty': // Tests returning a null result. | |
| request = cursor; | |
| cursor.result.continue(); | |
| request.onsuccess = testCase.step_func(() => { | |
| results.push([requestId, request.result]); | |
| resolve(); | |
| }); | |
| request.onerror = null; | |
| break; | |
| case 'open-empty': // Tests returning a null cursor. | |
| request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`)); | |
| request.onsuccess = testCase.step_func(() => { | |
| const result = request.result; | |
| results.push([requestId, request.result]); | |
| resolve(); | |
| }); | |
| break; | |
| case 'count': // Tests returning a numeric result. | |
| request = index.count(); | |
| request.onsuccess = testCase.step_func(() => { | |
| results.push([requestId, request.result]); | |
| resolve(); | |
| }); | |
| break; | |
| }; | |
| if (!request.onsuccess) { | |
| request.onsuccess = testCase.step_func(() => { | |
| results.push([requestId, request.result]); | |
| resolve(); | |
| }); | |
| } | |
| if (!request.onerror) | |
| request.onerror = testCase.step_func(event => { | |
| event.preventDefault(); | |
| reject(request.error); | |
| }); | |
| }); | |
| } | |
| function checkOperationResult(operation, result, requestId) { | |
| const opcode = operation[0]; | |
| const primaryKey = operation[1]; | |
| const expectedValue = (primaryKey == 1 || primaryKey == 3) ? | |
| largeValue(wrapThreshold, primaryKey) : [`small-${primaryKey}`]; | |
| const requestIndex = result[0]; | |
| assert_equals( | |
| requestIndex, requestId, 'result event order should match request order'); | |
| switch (opcode) { | |
| case 'put': | |
| case 'put-with-id': | |
| case 'add': | |
| assert_equals( | |
| result[1], primaryKey, | |
| `${opcode} result should be the new object's primary key`); | |
| break; | |
| case 'get': | |
| assert_equals( | |
| result[1].id, primaryKey, | |
| 'get result should match put value (primary key)'); | |
| assert_equals( | |
| result[1].key, `k${primaryKey}`, | |
| 'get result should match put value (key)'); | |
| assert_equals( | |
| result[1].value.join(','), expectedValue.join(','), | |
| 'get result should match put value (nested value)'); | |
| break; | |
| case 'getall': | |
| assert_equals( | |
| result[1].length, primaryKey, | |
| 'getAll should return all the objects in the store'); | |
| for (let i = 0; i < primaryKey; ++i) { | |
| const object = result[1][i]; | |
| assert_equals( | |
| object.id, i + 1, | |
| `getAll result ${i + 1} should match put value (primary key)`); | |
| assert_equals( | |
| object.key, `k${i + 1}`, | |
| `get result ${i + 1} should match put value (key)`); | |
| const expectedValue = (i == 0 || i == 2) ? | |
| largeValue(wrapThreshold, i + 1) : [`small-${i + 1}`]; | |
| assert_equals( | |
| object.value.join(','), object.value.join(','), | |
| `get result ${i + 1} should match put value (nested value)`); | |
| } | |
| break; | |
| case 'get-empty': | |
| assert_equals( | |
| result[1], undefined, 'get-empty result should be undefined'); | |
| break; | |
| case 'error': | |
| assert_equals( | |
| result[1].name, 'ConstraintError', | |
| 'incorrect error from put with duplicate primary key'); | |
| break; | |
| case 'continue': | |
| case 'open': | |
| assert_equals( | |
| result[1], `k${primaryKey}`, | |
| `${opcode} key should match the key in the put value`); | |
| assert_equals( | |
| result[2], primaryKey, | |
| `${opcode} primary key should match the put value's primary key`); | |
| assert_equals( | |
| result[3].id, primaryKey, | |
| `${opcode} value should match put value (primary key)`); | |
| assert_equals( | |
| result[3].key, `k${primaryKey}`, | |
| `${opcode} value should match put value (key)`); | |
| assert_equals( | |
| result[3].value.join(','), expectedValue.join(','), | |
| `${opcode} value should match put value (nested value)`); | |
| break; | |
| case 'continue-empty': | |
| case 'open-empty': | |
| assert_equals(result[1], null, `${opcode} result should be null`); | |
| break; | |
| } | |
| } | |
| function eventsTest(label, operations) { | |
| promise_test(testCase => { | |
| return createDatabase(testCase, (database, transaction) => { | |
| const store = database.createObjectStore( | |
| 'test-store', { autoIncrement: true, keyPath: 'id' }); | |
| store.createIndex('test-index', 'key', { unique: true }); | |
| populateStore(store); | |
| }).then(database => { | |
| const transaction = database.transaction(['test-store'], 'readwrite'); | |
| const store = transaction.objectStore('test-store'); | |
| const index = store.index('test-index'); | |
| return new Promise((resolve, reject) => { | |
| openCursors(testCase, index, operations, reject, () => { | |
| const results = []; | |
| const promises = []; | |
| for (let i = 0; i < operations.length; ++i) { | |
| const promise = doOperation( | |
| testCase, store, index, operations[i], i, results); | |
| promises.push(promise); | |
| }; | |
| resolve(Promise.all(promises).then(() => results)); | |
| }); | |
| }); | |
| }).then(results => { | |
| assert_equals( | |
| results.length, operations.length, | |
| 'Promise.all should resolve after all sub-promises resolve'); | |
| for (let i = 0; i < operations.length; ++i) | |
| checkOperationResult(operations[i], results[i], i); | |
| }); | |
| }, label); | |
| } | |
| eventsTest('small values', [ | |
| ['get', 2], | |
| ['count', 4], | |
| ['continue-empty', null], | |
| ['get-empty', 5], | |
| ['add', 5], | |
| ['open', 2], | |
| ['continue', 2], | |
| ['get', 4], | |
| ['get-empty', 6], | |
| ['count', 5], | |
| ['put-with-id', 5], | |
| ['put', 6], | |
| ['error', 3], | |
| ['continue', 4], | |
| ['count', 6], | |
| ['get-empty', 7], | |
| ['open', 4], | |
| ['open-empty', 7], | |
| ['add', 7], | |
| ]); | |
| eventsTest('large values', [ | |
| ['open', 1], | |
| ['get', 1], | |
| ['getall', 4], | |
| ['get', 3], | |
| ['continue', 3], | |
| ['open', 3], | |
| ]); | |
| eventsTest('large value followed by small values', [ | |
| ['get', 1], | |
| ['getall', 4], | |
| ['open', 2], | |
| ['continue-empty', null], | |
| ['get', 2], | |
| ['get-empty', 5], | |
| ['count', 4], | |
| ['continue-empty', null], | |
| ['open-empty', 5], | |
| ['add', 5], | |
| ['error', 1], | |
| ['continue', 2], | |
| ['get-empty', 6], | |
| ['put-with-id', 5], | |
| ['put', 6], | |
| ]); | |
| eventsTest('large values mixed with small values', [ | |
| ['get', 1], | |
| ['get', 2], | |
| ['get-empty', 5], | |
| ['count', 4], | |
| ['continue-empty', null], | |
| ['open', 1], | |
| ['continue', 2], | |
| ['open-empty', 5], | |
| ['getall', 4], | |
| ['open', 2], | |
| ['continue-empty', null], | |
| ['add', 5], | |
| ['get', 3], | |
| ['count', 5], | |
| ['get-empty', 6], | |
| ['put-with-id', 5], | |
| ['getall', 5], | |
| ['continue', 3], | |
| ['open-empty', 6], | |
| ['put', 6], | |
| ['error', 1], | |
| ['continue', 2], | |
| ['open', 4], | |
| ['get-empty', 7], | |
| ['count', 6], | |
| ['continue', 3], | |
| ['add', 7], | |
| ['getall', 7], | |
| ['error', 3], | |
| ['count', 7], | |
| ]); | |
| </script> |