diff --git a/IndexedDB/idb-explicit-commit-throw.any.js b/IndexedDB/idb-explicit-commit-throw.any.js new file mode 100644 index 00000000000000..3b8d148d76c1e9 --- /dev/null +++ b/IndexedDB/idb-explicit-commit-throw.any.js @@ -0,0 +1,45 @@ +// META: script=support-promises.js + +/** + * This file contains a test that was separated out from the rest of the idb + * explict commit tests because it requires the flag 'allow_uncaught_exception', + * which prevents unintentionally thrown errors from failing tests. + * + * @author andreasbutler@google.com + */ + +setup({allow_uncaught_exception:true}); + +promise_test(async testCase => { + // Register an event listener that will prevent the intentionally thrown + // error from bubbling up to the window and failing the testharness. This + // is necessary because currently allow_uncaught_exception does not behave + // as expected for promise_test. + // + // Git issue: https://github.com/web-platform-tests/wpt/issues/14041 + self.addEventListener('error', (event) => { event.preventDefault(); }); + + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + const putRequest = objectStore.put({isbn:'one', title:'title'}); + txn.commit(); + putRequest.onsuccess = () => { + throw new Error('This error thrown after an explicit commit should not ' + + 'prevent the transaction from committing.'); + } + await promiseForTransaction(testCase, txn); + + // Ensure that despite the uncaught error after the put request, the explicit + // commit still causes the request to be committed. + const txn2 = db.transaction(['books'], 'readwrite'); + const objectStore2 = txn2.objectStore('books'); + const getRequest = objectStore2.get('one'); + await promiseForTransaction(testCase, txn2); + + assert_equals(getRequest.result.title, 'title'); +}, 'Any errors in callbacks that run after an explicit commit will not stop ' + + 'the commit from being processed.'); diff --git a/IndexedDB/idb-explicit-commit.any.js b/IndexedDB/idb-explicit-commit.any.js new file mode 100644 index 00000000000000..b2dec751b1a8f1 --- /dev/null +++ b/IndexedDB/idb-explicit-commit.any.js @@ -0,0 +1,181 @@ +// META: script=support-promises.js + +/** + * This file contains the webplatform tests for the explicit commit() method + * of the IndexedDB transaction API. + * + * @author andreasbutler@google.com + */ + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + objectStore.put({isbn: 'one', title: 'title1'}); + objectStore.put({isbn: 'two', title: 'title2'}); + objectStore.put({isbn: 'three', title: 'title3'}); + txn.commit(); + await promiseForTransaction(testCase, txn); + + const txn2 = db.transaction(['books'], 'readonly'); + const objectStore2 = txn2.objectStore('books'); + const getRequestitle1 = objectStore2.get('one'); + const getRequestitle2 = objectStore2.get('two'); + const getRequestitle3 = objectStore2.get('three'); + txn2.commit(); + await promiseForTransaction(testCase, txn2); + assert_array_equals( + [getRequestitle1.result.title, + getRequestitle2.result.title, + getRequestitle3.result.title], + ['title1', 'title2', 'title3'], + 'All three retrieved titles should match those that were put.'); + db.close(); +}, 'Explicitly committed data can be read back out.'); + + +promise_test(async testCase => { + let db = await createDatabase(testCase, () => {}); + assert_equals(1, db.version, 'A database should be created as version 1'); + db.close(); + + // Upgrade the versionDB database and explicitly commit its versionchange + // transaction. + db = await migrateDatabase(testCase, 2, async (db, txn) => { + txn.commit(); + }); + assert_equals(2, db.version, + 'The database version should have been incremented regardless of ' + + 'whether the versionchange transaction was explicitly or implicitly ' + + 'committed.'); + db.close(); +}, 'commit() on a version change transaction does not cause errors.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + txn.commit(); + assert_throws('TransactionInactiveError', + () => { objectStore.put({isbn: 'one', title: 'title1'}); }, + 'After commit is called, the transaction should be inactive.'); + db.close(); +}, 'A committed transaction becomes inactive immediately.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + const putRequest = objectStore.put({isbn: 'one', title: 'title1'}); + putRequest.onsuccess = testCase.step_func(() => { + assert_throws('TransactionInactiveError', + () => { objectStore.put({isbn:'two', title:'title2'}); }, + 'The transaction should not be active in the callback of a request after ' + + 'commit() is called.'); + }); + txn.commit(); + await promiseForTransaction(testCase, txn); + db.close(); +}, 'A committed transaction is inactive in future request callbacks.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + txn.commit(); + + assert_throws('TransactionInactiveError', + () => { objectStore.put({isbn:'one', title:'title1'}); }, + 'After commit is called, the transaction should be inactive.'); + + const txn2 = db.transaction(['books'], 'readonly'); + const objectStore2 = txn2.objectStore('books'); + const getRequest = objectStore2.get('one'); + await promiseForTransaction(testCase, txn2); + assert_equals(getRequest.result, undefined); + + db.close(); +}, 'Puts issued after commit are not fulfilled.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + txn.abort(); + assert_throws('InvalidStateError', + () => { txn.commit(); }, + 'The transaction should have been aborted.'); + db.close(); +}, 'Calling commit on an aborted transaction throws.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + txn.commit(); + assert_throws('InvalidStateError', + () => { txn.commit(); }, + 'The transaction should have already committed.'); + db.close(); +}, 'Calling commit on a committed transaction throws.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + const putRequest = objectStore.put({isbn:'one', title:'title1'}); + txn.commit(); + assert_throws('InvalidStateError', + () => { txn.abort(); }, + 'The transaction should already have committed.'); + const txn2 = db.transaction(['books'], 'readwrite'); + const objectStore2 = txn2.objectStore('books'); + const getRequest = objectStore2.get('one'); + await promiseForTransaction(testCase, txn2); + assert_equals( + getRequest.result.title, + 'title1', + 'Explicitly committed data should be gettable.'); + db.close(); +}, 'Calling abort on a committed transaction throws and does not prevent ' + + 'persisting the data.'); + + +promise_test(async testCase => { + const db = await createDatabase(testCase, async db => { + await createBooksStore(testCase, db); + }); + const txn = db.transaction(['books'], 'readwrite'); + const objectStore = txn.objectStore('books'); + const releaseTxnFunction = keepAlive(testCase, txn, 'books'); + + // Break up the scope of execution to force the transaction into an inactive + // state. + await timeoutPromise(0); + + assert_throws('InvalidStateError', + () => { txn.commit(); }, + 'The transaction should be inactive so calling commit should throw.'); + releaseTxnFunction(); + db.close(); +}, 'Calling txn.commit() when txn is inactive should throw.'); diff --git a/IndexedDB/support-promises.js b/IndexedDB/support-promises.js index 433af5092701d6..8195be341e1886 100644 --- a/IndexedDB/support-promises.js +++ b/IndexedDB/support-promises.js @@ -312,3 +312,32 @@ async function deleteAllDatabases(testCase) { await eventWatcher.wait_for('success'); } } + +// Keeps the passed transaction alive indefinitely (by making requests +// against the named store). Returns a function that asserts that the +// transaction has not already completed and then ends the request loop so that +// the transaction may autocommit and complete. +function keepAlive(testCase, transaction, storeName) { + let completed = false; + transaction.addEventListener('complete', () => { completed = true; }); + + let keepSpinning = true; + + function spin() { + if (!keepSpinning) + return; + transaction.objectStore(storeName).get(0).onsuccess = spin; + } + spin(); + + return testCase.step_func(() => { + assert_false(completed, 'Transaction completed while kept alive'); + keepSpinning = false; + }); +} + +// Return a promise that resolves after a setTimeout finishes to break up the +// scope of a function's execution. +function timeoutPromise(ms) { + return new Promise(resolve => { setTimeout(resolve, ms); }); +} diff --git a/IndexedDB/support.js b/IndexedDB/support.js index 5edbdacbcc7d0c..d3b2bb7d1212dd 100644 --- a/IndexedDB/support.js +++ b/IndexedDB/support.js @@ -1,6 +1,3 @@ -var databaseName = "database"; -var databaseVersion = 1; - /* Delete created databases * * Go through each finished test, see if it has an associated database. Close @@ -170,18 +167,18 @@ function is_transaction_active(tx, store_name) { } } -// Keep the passed transaction alive indefinitely (by making requests -// against the named store). Returns a function to to let the -// transaction finish, and asserts that the transaction is not yet -// finished. +// Keeps the passed transaction alive indefinitely (by making requests +// against the named store). Returns a function that asserts that the +// transaction has not already completed and then ends the request loop so that +// the transaction may autocommit and complete. function keep_alive(tx, store_name) { let completed = false; tx.addEventListener('complete', () => { completed = true; }); - let pin = true; + let keepSpinning = true; function spin() { - if (!pin) + if (!keepSpinning) return; tx.objectStore(store_name).get(0).onsuccess = spin; } @@ -189,6 +186,6 @@ function keep_alive(tx, store_name) { return () => { assert_false(completed, 'Transaction completed while kept alive'); - pin = false; + keepSpinning = false; }; }