summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/IndexedDB/resources/support-promises.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/IndexedDB/resources/support-promises.js')
-rw-r--r--testing/web-platform/tests/IndexedDB/resources/support-promises.js355
1 files changed, 355 insertions, 0 deletions
diff --git a/testing/web-platform/tests/IndexedDB/resources/support-promises.js b/testing/web-platform/tests/IndexedDB/resources/support-promises.js
new file mode 100644
index 0000000000..9128bfe151
--- /dev/null
+++ b/testing/web-platform/tests/IndexedDB/resources/support-promises.js
@@ -0,0 +1,355 @@
+'use strict';
+
+// Returns an IndexedDB database name that is unique to the test case.
+function databaseName(testCase) {
+ return 'db' + self.location.pathname + '-' + testCase.name;
+}
+
+// EventWatcher covering all the events defined on IndexedDB requests.
+//
+// The events cover IDBRequest and IDBOpenDBRequest.
+function requestWatcher(testCase, request) {
+ return new EventWatcher(testCase, request,
+ ['blocked', 'error', 'success', 'upgradeneeded']);
+}
+
+// EventWatcher covering all the events defined on IndexedDB transactions.
+//
+// The events cover IDBTransaction.
+function transactionWatcher(testCase, request) {
+ return new EventWatcher(testCase, request, ['abort', 'complete', 'error']);
+}
+
+// Promise that resolves with an IDBRequest's result.
+//
+// The promise only resolves if IDBRequest receives the "success" event. Any
+// other event causes the promise to reject with an error. This is correct in
+// most cases, but insufficient for indexedDB.open(), which issues
+// "upgradeneded" events under normal operation.
+function promiseForRequest(testCase, request) {
+ const eventWatcher = requestWatcher(testCase, request);
+ return eventWatcher.wait_for('success').then(event => event.target.result);
+}
+
+// Promise that resolves when an IDBTransaction completes.
+//
+// The promise resolves with undefined if IDBTransaction receives the "complete"
+// event, and rejects with an error for any other event.
+function promiseForTransaction(testCase, request) {
+ const eventWatcher = transactionWatcher(testCase, request);
+ return eventWatcher.wait_for('complete').then(() => {});
+}
+
+// Migrates an IndexedDB database whose name is unique for the test case.
+//
+// newVersion must be greater than the database's current version.
+//
+// migrationCallback will be called during a versionchange transaction and will
+// given the created database, the versionchange transaction, and the database
+// open request.
+//
+// Returns a promise. If the versionchange transaction goes through, the promise
+// resolves to an IndexedDB database that should be closed by the caller. If the
+// versionchange transaction is aborted, the promise resolves to an error.
+function migrateDatabase(testCase, newVersion, migrationCallback) {
+ return migrateNamedDatabase(
+ testCase, databaseName(testCase), newVersion, migrationCallback);
+}
+
+// Migrates an IndexedDB database.
+//
+// newVersion must be greater than the database's current version.
+//
+// migrationCallback will be called during a versionchange transaction and will
+// given the created database, the versionchange transaction, and the database
+// open request.
+//
+// Returns a promise. If the versionchange transaction goes through, the promise
+// resolves to an IndexedDB database that should be closed by the caller. If the
+// versionchange transaction is aborted, the promise resolves to an error.
+function migrateNamedDatabase(
+ testCase, databaseName, newVersion, migrationCallback) {
+ // We cannot use eventWatcher.wait_for('upgradeneeded') here, because
+ // the versionchange transaction auto-commits before the Promise's then
+ // callback gets called.
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(databaseName, newVersion);
+ request.onupgradeneeded = testCase.step_func(event => {
+ const database = event.target.result;
+ const transaction = event.target.transaction;
+ let shouldBeAborted = false;
+ let requestEventPromise = null;
+
+ // We wrap IDBTransaction.abort so we can set up the correct event
+ // listeners and expectations if the test chooses to abort the
+ // versionchange transaction.
+ const transactionAbort = transaction.abort.bind(transaction);
+ transaction.abort = () => {
+ transaction._willBeAborted();
+ transactionAbort();
+ }
+ transaction._willBeAborted = () => {
+ requestEventPromise = new Promise((resolve, reject) => {
+ request.onerror = event => {
+ event.preventDefault();
+ resolve(event.target.error);
+ };
+ request.onsuccess = () => reject(new Error(
+ 'indexedDB.open should not succeed for an aborted ' +
+ 'versionchange transaction'));
+ });
+ shouldBeAborted = true;
+ }
+
+ // If migration callback returns a promise, we'll wait for it to resolve.
+ // This simplifies some tests.
+ const callbackResult = migrationCallback(database, transaction, request);
+ if (!shouldBeAborted) {
+ request.onerror = null;
+ request.onsuccess = null;
+ requestEventPromise = promiseForRequest(testCase, request);
+ }
+
+ // requestEventPromise needs to be the last promise in the chain, because
+ // we want the event that it resolves to.
+ resolve(Promise.resolve(callbackResult).then(() => requestEventPromise));
+ });
+ request.onerror = event => reject(event.target.error);
+ request.onsuccess = () => {
+ const database = request.result;
+ testCase.add_cleanup(() => { database.close(); });
+ reject(new Error(
+ 'indexedDB.open should not succeed without creating a ' +
+ 'versionchange transaction'));
+ };
+ }).then(databaseOrError => {
+ if (databaseOrError instanceof IDBDatabase)
+ testCase.add_cleanup(() => { databaseOrError.close(); });
+ return databaseOrError;
+ });
+}
+
+// Creates an IndexedDB database whose name is unique for the test case.
+//
+// setupCallback will be called during a versionchange transaction, and will be
+// given the created database, the versionchange transaction, and the database
+// open request.
+//
+// Returns a promise that resolves to an IndexedDB database. The caller should
+// close the database.
+function createDatabase(testCase, setupCallback) {
+ return createNamedDatabase(testCase, databaseName(testCase), setupCallback);
+}
+
+// Creates an IndexedDB database.
+//
+// setupCallback will be called during a versionchange transaction, and will be
+// given the created database, the versionchange transaction, and the database
+// open request.
+//
+// Returns a promise that resolves to an IndexedDB database. The caller should
+// close the database.
+function createNamedDatabase(testCase, databaseName, setupCallback) {
+ const request = indexedDB.deleteDatabase(databaseName);
+ return promiseForRequest(testCase, request).then(() => {
+ testCase.add_cleanup(() => { indexedDB.deleteDatabase(databaseName); });
+ return migrateNamedDatabase(testCase, databaseName, 1, setupCallback)
+ });
+}
+
+// Opens an IndexedDB database without performing schema changes.
+//
+// The given version number must match the database's current version.
+//
+// Returns a promise that resolves to an IndexedDB database. The caller should
+// close the database.
+function openDatabase(testCase, version) {
+ return openNamedDatabase(testCase, databaseName(testCase), version);
+}
+
+// Opens an IndexedDB database without performing schema changes.
+//
+// The given version number must match the database's current version.
+//
+// Returns a promise that resolves to an IndexedDB database. The caller should
+// close the database.
+function openNamedDatabase(testCase, databaseName, version) {
+ const request = indexedDB.open(databaseName, version);
+ return promiseForRequest(testCase, request).then(database => {
+ testCase.add_cleanup(() => { database.close(); });
+ return database;
+ });
+}
+
+// The data in the 'books' object store records in the first example of the
+// IndexedDB specification.
+const BOOKS_RECORD_DATA = [
+ { title: 'Quarry Memories', author: 'Fred', isbn: 123456 },
+ { title: 'Water Buffaloes', author: 'Fred', isbn: 234567 },
+ { title: 'Bedrock Nights', author: 'Barney', isbn: 345678 },
+];
+
+// Creates a 'books' object store whose contents closely resembles the first
+// example in the IndexedDB specification.
+const createBooksStore = (testCase, database) => {
+ const store = database.createObjectStore('books',
+ { keyPath: 'isbn', autoIncrement: true });
+ store.createIndex('by_author', 'author');
+ store.createIndex('by_title', 'title', { unique: true });
+ for (const record of BOOKS_RECORD_DATA)
+ store.put(record);
+ return store;
+}
+
+// Creates a 'books' object store whose contents closely resembles the first
+// example in the IndexedDB specification, just without autoincrementing.
+const createBooksStoreWithoutAutoIncrement = (testCase, database) => {
+ const store = database.createObjectStore('books',
+ { keyPath: 'isbn' });
+ store.createIndex('by_author', 'author');
+ store.createIndex('by_title', 'title', { unique: true });
+ for (const record of BOOKS_RECORD_DATA)
+ store.put(record);
+ return store;
+}
+
+// Creates a 'not_books' object store used to test renaming into existing or
+// deleted store names.
+function createNotBooksStore(testCase, database) {
+ const store = database.createObjectStore('not_books');
+ store.createIndex('not_by_author', 'author');
+ store.createIndex('not_by_title', 'title', { unique: true });
+ return store;
+}
+
+// Verifies that an object store's indexes match the indexes used to create the
+// books store in the test database's version 1.
+//
+// The errorMessage is used if the assertions fail. It can state that the
+// IndexedDB implementation being tested is incorrect, or that the testing code
+// is using it incorrectly.
+function checkStoreIndexes (testCase, store, errorMessage) {
+ assert_array_equals(
+ store.indexNames, ['by_author', 'by_title'], errorMessage);
+ const authorIndex = store.index('by_author');
+ const titleIndex = store.index('by_title');
+ return Promise.all([
+ checkAuthorIndexContents(testCase, authorIndex, errorMessage),
+ checkTitleIndexContents(testCase, titleIndex, errorMessage),
+ ]);
+}
+
+// Verifies that an object store's key generator is in the same state as the
+// key generator created for the books store in the test database's version 1.
+//
+// The errorMessage is used if the assertions fail. It can state that the
+// IndexedDB implementation being tested is incorrect, or that the testing code
+// is using it incorrectly.
+function checkStoreGenerator(testCase, store, expectedKey, errorMessage) {
+ const request = store.put(
+ { title: 'Bedrock Nights ' + expectedKey, author: 'Barney' });
+ return promiseForRequest(testCase, request).then(result => {
+ assert_equals(result, expectedKey, errorMessage);
+ });
+}
+
+// Verifies that an object store's contents matches the contents used to create
+// the books store in the test database's version 1.
+//
+// The errorMessage is used if the assertions fail. It can state that the
+// IndexedDB implementation being tested is incorrect, or that the testing code
+// is using it incorrectly.
+function checkStoreContents(testCase, store, errorMessage) {
+ const request = store.get(123456);
+ return promiseForRequest(testCase, request).then(result => {
+ assert_equals(result.isbn, BOOKS_RECORD_DATA[0].isbn, errorMessage);
+ assert_equals(result.author, BOOKS_RECORD_DATA[0].author, errorMessage);
+ assert_equals(result.title, BOOKS_RECORD_DATA[0].title, errorMessage);
+ });
+}
+
+// Verifies that index matches the 'by_author' index used to create the
+// by_author books store in the test database's version 1.
+//
+// The errorMessage is used if the assertions fail. It can state that the
+// IndexedDB implementation being tested is incorrect, or that the testing code
+// is using it incorrectly.
+function checkAuthorIndexContents(testCase, index, errorMessage) {
+ const request = index.get(BOOKS_RECORD_DATA[2].author);
+ return promiseForRequest(testCase, request).then(result => {
+ assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
+ assert_equals(result.title, BOOKS_RECORD_DATA[2].title, errorMessage);
+ });
+}
+
+// Verifies that an index matches the 'by_title' index used to create the books
+// store in the test database's version 1.
+//
+// The errorMessage is used if the assertions fail. It can state that the
+// IndexedDB implementation being tested is incorrect, or that the testing code
+// is using it incorrectly.
+function checkTitleIndexContents(testCase, index, errorMessage) {
+ const request = index.get(BOOKS_RECORD_DATA[2].title);
+ return promiseForRequest(testCase, request).then(result => {
+ assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
+ assert_equals(result.author, BOOKS_RECORD_DATA[2].author, errorMessage);
+ });
+}
+
+// Returns an Uint8Array with pseudorandom data.
+//
+// The PRNG should be sufficient to defeat compression schemes, but it is not
+// cryptographically strong.
+function largeValue(size, seed) {
+ const buffer = new Uint8Array(size);
+
+ // 32-bit xorshift - the seed can't be zero
+ let state = 1000 + seed;
+
+ for (let i = 0; i < size; ++i) {
+ state ^= state << 13;
+ state ^= state >> 17;
+ state ^= state << 5;
+ buffer[i] = state & 0xff;
+ }
+
+ return buffer;
+}
+
+async function deleteAllDatabases(testCase) {
+ const dbs_to_delete = await indexedDB.databases();
+ for( const db_info of dbs_to_delete) {
+ let request = indexedDB.deleteDatabase(db_info.name);
+ let eventWatcher = requestWatcher(testCase, request);
+ 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); });
+}