support-promises.js (14317B)
1 'use strict'; 2 3 // Returns an IndexedDB database name that is unique to the test case. 4 function databaseName(testCase) { 5 return 'db' + self.location.pathname + '-' + testCase.name; 6 } 7 8 // EventWatcher covering all the events defined on IndexedDB requests. 9 // 10 // The events cover IDBRequest and IDBOpenDBRequest. 11 function requestWatcher(testCase, request) { 12 return new EventWatcher(testCase, request, 13 ['blocked', 'error', 'success', 'upgradeneeded']); 14 } 15 16 // EventWatcher covering all the events defined on IndexedDB transactions. 17 // 18 // The events cover IDBTransaction. 19 function transactionWatcher(testCase, request) { 20 return new EventWatcher(testCase, request, ['abort', 'complete', 'error']); 21 } 22 23 // Promise that resolves with an IDBRequest's result. 24 // 25 // The promise only resolves if IDBRequest receives the "success" event. Any 26 // other event causes the promise to reject with an error. This is correct in 27 // most cases, but insufficient for indexedDB.open(), which issues 28 // "upgradeneded" events under normal operation. 29 function promiseForRequest(testCase, request) { 30 const eventWatcher = requestWatcher(testCase, request); 31 return eventWatcher.wait_for('success').then(event => event.target.result); 32 } 33 34 // Promise that resolves when an IDBTransaction completes. 35 // 36 // The promise resolves with undefined if IDBTransaction receives the "complete" 37 // event, and rejects with an error for any other event. 38 function promiseForTransaction(testCase, request) { 39 const eventWatcher = transactionWatcher(testCase, request); 40 return eventWatcher.wait_for('complete').then(() => {}); 41 } 42 43 // Migrates an IndexedDB database whose name is unique for the test case. 44 // 45 // newVersion must be greater than the database's current version. 46 // 47 // migrationCallback will be called during a versionchange transaction and will 48 // given the created database, the versionchange transaction, and the database 49 // open request. 50 // 51 // Returns a promise. If the versionchange transaction goes through, the promise 52 // resolves to an IndexedDB database that should be closed by the caller. If the 53 // versionchange transaction is aborted, the promise resolves to an error. 54 function migrateDatabase(testCase, newVersion, migrationCallback) { 55 return migrateNamedDatabase( 56 testCase, databaseName(testCase), newVersion, migrationCallback); 57 } 58 59 // Migrates an IndexedDB database. 60 // 61 // newVersion must be greater than the database's current version. 62 // 63 // migrationCallback will be called during a versionchange transaction and will 64 // given the created database, the versionchange transaction, and the database 65 // open request. 66 // 67 // Returns a promise. If the versionchange transaction goes through, the promise 68 // resolves to an IndexedDB database that should be closed by the caller. If the 69 // versionchange transaction is aborted, the promise resolves to an error. 70 function migrateNamedDatabase( 71 testCase, databaseName, newVersion, migrationCallback) { 72 // We cannot use eventWatcher.wait_for('upgradeneeded') here, because 73 // the versionchange transaction auto-commits before the Promise's then 74 // callback gets called. 75 return new Promise((resolve, reject) => { 76 const request = indexedDB.open(databaseName, newVersion); 77 request.onupgradeneeded = testCase.step_func(event => { 78 const database = event.target.result; 79 const transaction = event.target.transaction; 80 let shouldBeAborted = false; 81 let requestEventPromise = null; 82 83 // We wrap IDBTransaction.abort so we can set up the correct event 84 // listeners and expectations if the test chooses to abort the 85 // versionchange transaction. 86 const transactionAbort = transaction.abort.bind(transaction); 87 transaction.abort = () => { 88 transaction._willBeAborted(); 89 transactionAbort(); 90 } 91 transaction._willBeAborted = () => { 92 requestEventPromise = new Promise((resolve, reject) => { 93 request.onerror = event => { 94 event.preventDefault(); 95 resolve(event.target.error); 96 }; 97 request.onsuccess = () => reject(new Error( 98 'indexedDB.open should not succeed for an aborted ' + 99 'versionchange transaction')); 100 }); 101 shouldBeAborted = true; 102 } 103 104 // If migration callback returns a promise, we'll wait for it to resolve. 105 // This simplifies some tests. 106 const callbackResult = migrationCallback(database, transaction, request); 107 if (!shouldBeAborted) { 108 request.onerror = null; 109 request.onsuccess = null; 110 requestEventPromise = promiseForRequest(testCase, request); 111 } 112 113 // requestEventPromise needs to be the last promise in the chain, because 114 // we want the event that it resolves to. 115 resolve(Promise.resolve(callbackResult).then(() => requestEventPromise)); 116 }); 117 request.onerror = event => reject(event.target.error); 118 request.onsuccess = () => { 119 const database = request.result; 120 testCase.add_cleanup(() => { database.close(); }); 121 reject(new Error( 122 'indexedDB.open should not succeed without creating a ' + 123 'versionchange transaction')); 124 }; 125 }).then(databaseOrError => { 126 if (databaseOrError instanceof IDBDatabase) 127 testCase.add_cleanup(() => { databaseOrError.close(); }); 128 return databaseOrError; 129 }); 130 } 131 132 // Creates an IndexedDB database whose name is unique for the test case. 133 // 134 // setupCallback will be called during a versionchange transaction, and will be 135 // given the created database, the versionchange transaction, and the database 136 // open request. 137 // 138 // Returns a promise that resolves to an IndexedDB database. The caller should 139 // close the database. 140 function createDatabase(testCase, setupCallback) { 141 return createNamedDatabase(testCase, databaseName(testCase), setupCallback); 142 } 143 144 // Creates an IndexedDB database. 145 // 146 // setupCallback will be called during a versionchange transaction, and will be 147 // given the created database, the versionchange transaction, and the database 148 // open request. 149 // 150 // Returns a promise that resolves to an IndexedDB database. The caller should 151 // close the database. 152 function createNamedDatabase(testCase, databaseName, setupCallback) { 153 const request = indexedDB.deleteDatabase(databaseName); 154 return promiseForRequest(testCase, request).then(() => { 155 testCase.add_cleanup(() => { indexedDB.deleteDatabase(databaseName); }); 156 return migrateNamedDatabase(testCase, databaseName, 1, setupCallback) 157 }); 158 } 159 160 // Opens an IndexedDB database without performing schema changes. 161 // 162 // The given version number must match the database's current version. 163 // 164 // Returns a promise that resolves to an IndexedDB database. The caller should 165 // close the database. 166 function openDatabase(testCase, version) { 167 return openNamedDatabase(testCase, databaseName(testCase), version); 168 } 169 170 // Opens an IndexedDB database without performing schema changes. 171 // 172 // The given version number must match the database's current version. 173 // 174 // Returns a promise that resolves to an IndexedDB database. The caller should 175 // close the database. 176 function openNamedDatabase(testCase, databaseName, version) { 177 const request = indexedDB.open(databaseName, version); 178 return promiseForRequest(testCase, request).then(database => { 179 testCase.add_cleanup(() => { database.close(); }); 180 return database; 181 }); 182 } 183 184 // The data in the 'books' object store records in the first example of the 185 // IndexedDB specification. 186 const BOOKS_RECORD_DATA = [ 187 { title: 'Quarry Memories', author: 'Fred', isbn: 123456 }, 188 { title: 'Water Buffaloes', author: 'Fred', isbn: 234567 }, 189 { title: 'Bedrock Nights', author: 'Barney', isbn: 345678 }, 190 ]; 191 192 // Creates a 'books' object store whose contents closely resembles the first 193 // example in the IndexedDB specification. 194 const createBooksStore = (testCase, database) => { 195 const store = database.createObjectStore('books', 196 { keyPath: 'isbn', autoIncrement: true }); 197 store.createIndex('by_author', 'author'); 198 store.createIndex('by_title', 'title', { unique: true }); 199 for (const record of BOOKS_RECORD_DATA) 200 store.put(record); 201 return store; 202 } 203 204 // Creates a 'books' object store whose contents closely resembles the first 205 // example in the IndexedDB specification, just without autoincrementing. 206 const createBooksStoreWithoutAutoIncrement = (testCase, database) => { 207 const store = database.createObjectStore('books', 208 { keyPath: 'isbn' }); 209 store.createIndex('by_author', 'author'); 210 store.createIndex('by_title', 'title', { unique: true }); 211 for (const record of BOOKS_RECORD_DATA) 212 store.put(record); 213 return store; 214 } 215 216 // Creates a 'not_books' object store used to test renaming into existing or 217 // deleted store names. 218 function createNotBooksStore(testCase, database) { 219 const store = database.createObjectStore('not_books'); 220 store.createIndex('not_by_author', 'author'); 221 store.createIndex('not_by_title', 'title', { unique: true }); 222 return store; 223 } 224 225 // Verifies that an object store's indexes match the indexes used to create the 226 // books store in the test database's version 1. 227 // 228 // The errorMessage is used if the assertions fail. It can state that the 229 // IndexedDB implementation being tested is incorrect, or that the testing code 230 // is using it incorrectly. 231 function checkStoreIndexes (testCase, store, errorMessage) { 232 assert_array_equals( 233 store.indexNames, ['by_author', 'by_title'], errorMessage); 234 const authorIndex = store.index('by_author'); 235 const titleIndex = store.index('by_title'); 236 return Promise.all([ 237 checkAuthorIndexContents(testCase, authorIndex, errorMessage), 238 checkTitleIndexContents(testCase, titleIndex, errorMessage), 239 ]); 240 } 241 242 // Verifies that an object store's key generator is in the same state as the 243 // key generator created for the books store in the test database's version 1. 244 // 245 // The errorMessage is used if the assertions fail. It can state that the 246 // IndexedDB implementation being tested is incorrect, or that the testing code 247 // is using it incorrectly. 248 function checkStoreGenerator(testCase, store, expectedKey, errorMessage) { 249 const request = store.put( 250 { title: 'Bedrock Nights ' + expectedKey, author: 'Barney' }); 251 return promiseForRequest(testCase, request).then(result => { 252 assert_equals(result, expectedKey, errorMessage); 253 }); 254 } 255 256 // Verifies that an object store's contents matches the contents used to create 257 // the books store in the test database's version 1. 258 // 259 // The errorMessage is used if the assertions fail. It can state that the 260 // IndexedDB implementation being tested is incorrect, or that the testing code 261 // is using it incorrectly. 262 function checkStoreContents(testCase, store, errorMessage) { 263 const request = store.get(123456); 264 return promiseForRequest(testCase, request).then(result => { 265 assert_equals(result.isbn, BOOKS_RECORD_DATA[0].isbn, errorMessage); 266 assert_equals(result.author, BOOKS_RECORD_DATA[0].author, errorMessage); 267 assert_equals(result.title, BOOKS_RECORD_DATA[0].title, errorMessage); 268 }); 269 } 270 271 // Verifies that index matches the 'by_author' index used to create the 272 // by_author books store in the test database's version 1. 273 // 274 // The errorMessage is used if the assertions fail. It can state that the 275 // IndexedDB implementation being tested is incorrect, or that the testing code 276 // is using it incorrectly. 277 function checkAuthorIndexContents(testCase, index, errorMessage) { 278 const request = index.get(BOOKS_RECORD_DATA[2].author); 279 return promiseForRequest(testCase, request).then(result => { 280 assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage); 281 assert_equals(result.title, BOOKS_RECORD_DATA[2].title, errorMessage); 282 }); 283 } 284 285 // Verifies that an index matches the 'by_title' index used to create the books 286 // store in the test database's version 1. 287 // 288 // The errorMessage is used if the assertions fail. It can state that the 289 // IndexedDB implementation being tested is incorrect, or that the testing code 290 // is using it incorrectly. 291 function checkTitleIndexContents(testCase, index, errorMessage) { 292 const request = index.get(BOOKS_RECORD_DATA[2].title); 293 return promiseForRequest(testCase, request).then(result => { 294 assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage); 295 assert_equals(result.author, BOOKS_RECORD_DATA[2].author, errorMessage); 296 }); 297 } 298 299 // Returns an Uint8Array. 300 // When `seed` is non-zero, the data is pseudo-random, otherwise it is repetitive. 301 // The PRNG should be sufficient to defeat compression schemes, but it is not 302 // cryptographically strong. 303 function largeValue(size, seed) { 304 const buffer = new Uint8Array(size); 305 // Fill with a lot of the same byte. 306 if (seed == 0) { 307 buffer.fill(0x11, 0, size - 1); 308 return buffer; 309 } 310 311 // 32-bit xorshift - the seed can't be zero 312 let state = 1000 + seed; 313 314 for (let i = 0; i < size; ++i) { 315 state ^= state << 13; 316 state ^= state >> 17; 317 state ^= state << 5; 318 buffer[i] = state & 0xff; 319 } 320 321 return buffer; 322 } 323 324 async function deleteAllDatabases(testCase) { 325 const dbs_to_delete = await indexedDB.databases(); 326 for( const db_info of dbs_to_delete) { 327 let request = indexedDB.deleteDatabase(db_info.name); 328 let eventWatcher = requestWatcher(testCase, request); 329 await eventWatcher.wait_for('success'); 330 } 331 } 332 333 // Keeps the passed transaction alive indefinitely (by making requests 334 // against the named store). Returns a function that asserts that the 335 // transaction has not already completed and then ends the request loop so that 336 // the transaction may autocommit and complete. 337 function keepAlive(testCase, transaction, storeName) { 338 let completed = false; 339 transaction.addEventListener('complete', () => { completed = true; }); 340 341 let keepSpinning = true; 342 343 function spin() { 344 if (!keepSpinning) 345 return; 346 transaction.objectStore(storeName).get(0).onsuccess = spin; 347 } 348 spin(); 349 350 return testCase.step_func(() => { 351 assert_false(completed, 'Transaction completed while kept alive'); 352 keepSpinning = false; 353 }); 354 } 355 356 // Return a promise that resolves after a setTimeout finishes to break up the 357 // scope of a function's execution. 358 function timeoutPromise(ms) { 359 return new Promise(resolve => { setTimeout(resolve, ms); }); 360 }