// Copyright 2012 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.dbTest'); goog.setTestOnly('goog.dbTest'); goog.require('goog.Disposable'); goog.require('goog.array'); goog.require('goog.async.Deferred'); goog.require('goog.async.DeferredList'); goog.require('goog.db'); goog.require('goog.db.Cursor'); goog.require('goog.db.Error'); goog.require('goog.db.IndexedDb'); goog.require('goog.db.KeyRange'); goog.require('goog.db.Transaction'); goog.require('goog.events'); goog.require('goog.object'); goog.require('goog.testing.AsyncTestCase'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.asserts'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent.product'); goog.require('goog.userAgent.product.isVersion'); var idbSupported = goog.userAgent.product.CHROME && goog.userAgent.product.isVersion('22'); var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); asyncTestCase.stepTimeout = 5000; var dbName; var dbBaseName = 'testDb'; var globalDb = null; var propertyReplacer; // On Chrome 24+, the database reports its default version as 1 as opposed to // the empty string (as per the new spec). var baseVersion = goog.userAgent.product.isVersion('24') ? 1 : ''; var dbVersion = 1; function unblockDatabase(db) { // If a test goes wrong, the database connection may not be closed reliably. // This listens for a version change event (e.g. from deleting it in // preparation for the next test) and closes the existing connection when one // is received. goog.events.listen( db, goog.db.IndexedDb.EventType.VERSION_CHANGE, function(ev) { db.close(); }); } function openDatabase() { return goog.db.openDatabase(dbName).addCallback(unblockDatabase); } function incrementVersion(db, onUpgradeNeeded) { if (db.isOpen()) { db.close(); } var onBlocked = function(ev) { fail('Upgrade to version ' + dbVersion + ' is blocked.'); }; return goog.db.openDatabase(dbName, ++dbVersion, onUpgradeNeeded, onBlocked). addCallback(unblockDatabase).addCallback(function(db) { assertEquals(dbVersion, db.getVersion()); }); } function addStore(db) { return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('store'); }); } function addStoreWithIndex(db) { return incrementVersion(db, function(ev, db, tx) { var store = db.createObjectStore('store', {keyPath: 'key'}); store.createIndex('index', 'value'); }); } function populateStore(values, keys, db) { var putTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var store = putTx.objectStore('store'); for (var i = 0; i < values.length; ++i) { store.put(values[i], keys[i]); } return putTx.wait(); } function populateStoreWithObjects(values, keys, db) { var putTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var store = putTx.objectStore('store'); goog.array.forEach(values, function(value, index) { store.put({'key': keys[index], 'value': value}); }); return putTx.wait(); } function assertStoreValues(values, db) { var assertStoreTx = db.createTransaction(['store']); assertStoreTx.objectStore('store').getAll().addCallback(function(results) { assertSameElements(values, results); closeAndContinue(db); }); } function assertStoreObjectValues(values, db) { var assertStoreTx = db.createTransaction(['store']); assertStoreTx.objectStore('store').getAll().addCallback(function(results) { var retrievedValues = goog.array.map(results, function(result) { return result['value']; }); assertSameElements(values, retrievedValues); closeAndContinue(db); }); } function assertStoreValuesAndCursorsDisposed(values, cursors, db) { var assertStoreTx = db.createTransaction(['store']); assertStoreTx.objectStore('store').getAll().addCallback(function(results) { assertSameElements(values, results); assertTrue(cursors.length > 0); goog.array.forEach(cursors, function(elem, index, array) { console.log(elem); assertTrue('array[' + index + '] (' + elem + ') is not disposed', goog.Disposable.isDisposed(elem)); }); closeAndContinue(db); }); } function assertStoreDoesntExist(db) { try { db.createTransaction(['store']); fail('Create transaction with a non-existent store should have failed.'); } catch (e) { // expected assertEquals(e.getName(), goog.db.Error.ErrorName.NOT_FOUND_ERR); closeAndContinue(db); } } function failOnError(err) { fail(err.message); } function failOnErrorEvent(ev) { fail(ev.target.message); } function closeAndContinue(db) { db.close(); asyncTestCase.continueTesting(); } function setUpPage() { propertyReplacer = new goog.testing.PropertyReplacer(); } function setUp() { if (!idbSupported) { return; } // Always use a clean database by generating a new database name. dbName = dbBaseName + Date.now().toString(); globalDb = openDatabase(); } function tearDown() { propertyReplacer.reset(); } function testDatabaseOpened() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('check database is open'); assertNotNull(globalDb); globalDb.branch().addCallback(function(db) { assertTrue(db.isOpen()); closeAndContinue(db); }).addErrback(failOnError); } function testOpenWithNewVersion() { if (!idbSupported) { return; } var upgradeNeeded = false; asyncTestCase.waitForAsync('open with new version'); globalDb.branch().addCallback(function(db) { assertEquals(baseVersion, db.getVersion()); db.close(); return incrementVersion(db, function(ev, db, tx) { upgradeNeeded = true; }); }).addCallback(function(db) { assertTrue(upgradeNeeded); closeAndContinue(db); }).addErrback(failOnError); } function testManipulateObjectStores() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('manipulate object stores'); globalDb.branch().addCallback(function(db) { assertEquals(baseVersion, db.getVersion()); db.close(); return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('basicStore'); db.createObjectStore('keyPathStore', {keyPath: 'keyGoesHere'}); db.createObjectStore('autoIncrementStore', {autoIncrement: true}); }); }).addCallback(function(db) { var storeNames = db.getObjectStoreNames(); assertEquals(3, storeNames.length); assertTrue(storeNames.contains('basicStore')); assertTrue(storeNames.contains('keyPathStore')); assertTrue(storeNames.contains('autoIncrementStore')); return incrementVersion(db, function(ev, db, tx) { db.deleteObjectStore('basicStore'); }); }).addCallback(function(db) { var storeNames = db.getObjectStoreNames(); assertEquals(2, storeNames.length); assertFalse(storeNames.contains('basicStore')); assertTrue(storeNames.contains('keyPathStore')); assertTrue(storeNames.contains('autoIncrementStore')); closeAndContinue(db); }).addErrback(failOnError); } function testBadObjectStoreManipulation() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('bad object store manipulation'); var expectedCode = goog.db.Error.ErrorName.INVALID_STATE_ERR; globalDb.branch().addCallback(function(db) { try { db.createObjectStore('diediedie'); fail('Create object store outside transaction should have failed.'); } catch (err) { // expected assertEquals(expectedCode, err.getName()); } }).addCallback(addStore).addCallback(function(db) { try { db.deleteObjectStore('store'); fail('Delete object store outside transaction should have failed.'); } catch (err) { // expected assertEquals(expectedCode, err.getName()); } }).addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { try { db.deleteObjectStore('diediedie'); fail('Delete non-existent store should have failed.'); } catch (err) { // expected assertEquals(goog.db.Error.ErrorName.NOT_FOUND_ERR, err.getName()); } }); }).addCallback(closeAndContinue).addErrback(failOnError); } function testGetNonExistentObjectStore() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('get non-existent object store'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var tx = db.createTransaction(['store']); try { tx.objectStore('diediedie'); fail('getting non-existent object store should have failed'); } catch (err) { assertEquals(goog.db.Error.ErrorName.NOT_FOUND_ERR, err.getName()); } closeAndContinue(db); }).addErrback(failOnError); } function testCreateTransaction() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('create transactions'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var tx = db.createTransaction(['store']); assertEquals( 'mode not READ_ONLY', goog.db.Transaction.TransactionMode.READ_ONLY, tx.getMode()); tx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); assertEquals( 'mode not READ_WRITE', goog.db.Transaction.TransactionMode.READ_WRITE, tx.getMode()); closeAndContinue(db); }).addErrback(failOnError); } function testPutRecord() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('putting record'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var rw = goog.db.Transaction.TransactionMode.READ_WRITE; var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var ERROR = goog.db.Transaction.EventTypes.ERROR; function checkForOverwrittenValue() { var checkOverwriteTx = db.createTransaction(['store']); checkOverwriteTx.objectStore('store').get('putKey').addCallback( function(result) { // this is guaranteed to run before the COMPLETE event fires on // the transaction assertEquals('overwritten', result.key); assertEquals('value2', result.value); }); goog.events.listen(checkOverwriteTx, ERROR, failOnErrorEvent); goog.events.listen(checkOverwriteTx, COMPLETE, function() { closeAndContinue(db); }); } function overwriteValue() { var overwriteTx = db.createTransaction(['store'], rw); overwriteTx.objectStore('store').put( {key: 'overwritten', value: 'value2'}, 'putKey'); goog.events.listen(overwriteTx, ERROR, failOnErrorEvent); goog.events.listen(overwriteTx, COMPLETE, checkForOverwrittenValue); } function checkForInitialValue() { var checkResultsTx = db.createTransaction(['store']); checkResultsTx.objectStore('store').get('putKey').addCallback( function(result) { assertEquals('initial', result.key); assertEquals('value1', result.value); }); goog.events.listen(checkResultsTx, ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, overwriteValue); } var initialPutTx = db.createTransaction(['store'], rw); initialPutTx.objectStore('store').put( {key: 'initial', value: 'value1'}, 'putKey'); goog.events.listen(initialPutTx, ERROR, failOnErrorEvent); goog.events.listen(initialPutTx, COMPLETE, checkForInitialValue); }).addErrback(failOnError); } function testAddRecord() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding record'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var rw = goog.db.Transaction.TransactionMode.READ_WRITE; var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var ERROR = goog.db.Transaction.EventTypes.ERROR; var initialAddTx = db.createTransaction(['store'], rw); initialAddTx.objectStore('store').add( {key: 'hi', value: 'something'}, 'stuff'); goog.events.listen(initialAddTx, ERROR, failOnErrorEvent); goog.events.listen(initialAddTx, COMPLETE, function() { var successfulAddTx = db.createTransaction(['store']); successfulAddTx.objectStore('store').get('stuff').addCallback( function(result) { assertEquals('hi', result.key); assertEquals('something', result.value); }); goog.events.listen(successfulAddTx, ERROR, failOnErrorEvent); goog.events.listen(successfulAddTx, COMPLETE, function() { var addOverwriteTx = db.createTransaction(['store'], rw); addOverwriteTx.objectStore('store').add( {key: 'bye', value: 'nothing'}, 'stuff').addErrback(function(err) { // expected assertEquals( goog.db.Error.ErrorName.CONSTRAINT_ERR, err.getName()); }); goog.events.listen(addOverwriteTx, COMPLETE, function() { fail('adding existing record should not have succeeded'); }); goog.events.listen(addOverwriteTx, ERROR, function(ev) { // expected assertEquals( goog.db.Error.ErrorName.CONSTRAINT_ERR, ev.target.getName()); closeAndContinue(db); }); }); }); }).addErrback(failOnError); } function testPutRecordKeyPathStore() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding record, key path store'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('keyStore', {keyPath: 'key'}); }); }).addCallback(function(db) { var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var ERROR = goog.db.Transaction.EventTypes.ERROR; var putTx = db.createTransaction( ['keyStore'], goog.db.Transaction.TransactionMode.READ_WRITE); putTx.objectStore('keyStore').put({key: 'hi', value: 'something'}); goog.events.listen(putTx, ERROR, failOnErrorEvent); goog.events.listen(putTx, COMPLETE, function() { var checkResultsTx = db.createTransaction(['keyStore']); checkResultsTx.objectStore('keyStore').get('hi').addCallback( function(result) { assertNotUndefined(result); assertEquals('hi', result.key); assertEquals('something', result.value); }); goog.events.listen(checkResultsTx, ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, function() { closeAndContinue(db); }); }); }).addErrback(failOnError); } function testPutBadRecordKeyPathStore() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding bad record, key path store'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('keyStore', {keyPath: 'key'}); }); }).addCallback(function(db) { var badTx = db.createTransaction( ['keyStore'], goog.db.Transaction.TransactionMode.READ_WRITE); badTx.objectStore('keyStore').put( {key: 'diedie', value: 'anything'}, 'badKey').addCallback(function() { fail('inserting with explicit key should have failed'); }).addErrback(function(err) { // expected assertEquals(goog.db.Error.ErrorName.DATA_ERR, err.getName()); closeAndContinue(db); }); }).addErrback(failOnError); } function testPutRecordAutoIncrementStore() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding record, auto increment store'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('aiStore', {autoIncrement: true}); }); }).addCallback(function(db) { var tx = db.createTransaction( ['aiStore'], goog.db.Transaction.TransactionMode.READ_WRITE); tx.objectStore('aiStore').put('1'); tx.objectStore('aiStore').put('2'); tx.objectStore('aiStore').put('3'); goog.events.listen( tx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen(tx, goog.db.Transaction.EventTypes.COMPLETE, function() { var tx = db.createTransaction(['aiStore']); tx.objectStore('aiStore').getAll().addCallback(function(results) { assertEquals(3, results.length); // only checking to see if the results are included because the keys // are not specified assertNotEquals(-1, results.indexOf('1')); assertNotEquals(-1, results.indexOf('2')); assertNotEquals(-1, results.indexOf('3')); closeAndContinue(db); }); }); }).addErrback(failOnError); } function testPutRecordKeyPathAndAutoIncrementStore() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding record, key path + auto increment store'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { db.createObjectStore('hybridStore', { keyPath: 'key', autoIncrement: true }); }); }).addCallback(function(db) { var tx = db.createTransaction( ['hybridStore'], goog.db.Transaction.TransactionMode.READ_WRITE); tx.objectStore('hybridStore').put({value: 'whatever'}); goog.events.listen( tx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen(tx, goog.db.Transaction.EventTypes.COMPLETE, function() { var tx = db.createTransaction(['hybridStore']); tx.objectStore('hybridStore').getAll().addCallback(function(results) { assertEquals(1, results.length); assertEquals('whatever', results[0].value); assertNotUndefined(results[0].key); closeAndContinue(db); }); }); }).addErrback(failOnError); } function testPutIllegalRecords() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding illegal records'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var tx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); tx.objectStore('store').put('death', null).addCallback(function() { fail('putting with null key should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.DATA_ERR, err.getName()); }); tx.objectStore('store').put('death', NaN).addCallback(function() { fail('putting with NaN key should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.DATA_ERR, err.getName()); }); tx.objectStore('store').put('death', undefined).addCallback(function() { fail('putting with undefined key should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.DATA_ERR, err.getName()); }); closeAndContinue(db); }).addErrback(failOnError); } function testPutIllegalRecordsWithIndex() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding illegal records'); globalDb.branch().addCallback(addStoreWithIndex).addCallback(function(db) { var tx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); tx.objectStore('store').put({key: 'diediedie', value: null}). addErrback(function(err) { assertEquals(goog.db.Error.ErrorCode.DATA_ERR, err.code); }); tx.objectStore('store').put({key: 'dietodeath', value: NaN}). addErrback(function(err) { assertEquals(goog.db.Error.ErrorCode.DATA_ERR, err.code); }); tx.objectStore('store').put({key: 'dietodeath', value: undefined}). addErrback(function(err) { assertEquals(goog.db.Error.ErrorCode.DATA_ERR, err.code); }); closeAndContinue(db); }).addErrback(failOnError); } function testDeleteRecord() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('deleting record'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var rw = goog.db.Transaction.TransactionMode.READ_WRITE; var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var ERROR = goog.db.Transaction.EventTypes.ERROR; var putTx = db.createTransaction(['store'], rw); putTx.objectStore('store').put({key: 'hi', value: 'something'}, 'stuff'); goog.events.listen(putTx, ERROR, failOnErrorEvent); goog.events.listen(putTx, COMPLETE, function() { var deleteTx = db.createTransaction(['store'], rw); deleteTx.objectStore('store').remove('stuff'); goog.events.listen(deleteTx, ERROR, failOnErrorEvent); goog.events.listen(deleteTx, COMPLETE, function() { var checkResultsTx = db.createTransaction(['store']); checkResultsTx.objectStore('store').get('stuff').addCallback( function(result) { assertUndefined(result); }); goog.events.listen(checkResultsTx, ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, function() { closeAndContinue(db); }); }); }); }).addErrback(failOnError); } function testGetAll() { if (!idbSupported) { return; } var values = ['1', '2', '3']; var keys = ['a', 'b', 'c']; var addData = goog.partial(populateStore, values, keys); var checkStore = goog.partial(assertStoreValues, values); asyncTestCase.waitForAsync('getting all'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(checkStore); } function testGetAllFreesCursor() { if (!idbSupported) { return; } var values = ['1', '2', '3']; var keys = ['a', 'b', 'c']; var addData = goog.partial(populateStore, values, keys); var origCursor = goog.db.Cursor; var cursors = []; /** @constructor */ var testCursor = function() { origCursor.call(this); cursors.push(this); }; goog.object.extend(testCursor, origCursor); // We don't use goog.inherits here because we are going to be overwriting // goog.db.Cursor and we don't want a new "base" method as // goog.db.Cursor.base(this, 'constructor') would be a call to // testCursor.base(this, 'constructor') which would be goog.db.Cursor and be // an infinite loop. testCursor.prototype = origCursor.prototype; propertyReplacer.replace(goog.db, 'Cursor', testCursor); var checkStoreAndCursorDisposed = goog.partial(assertStoreValuesAndCursorsDisposed, values, cursors); asyncTestCase.waitForAsync('getting all'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(checkStoreAndCursorDisposed); } function testObjectStoreCursorGet() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStore, values, keys); // Open the cursor over range ['b', 'c'], move in backwards direction. var openCursorAndCheck = function(db) { var cursorTx = db.createTransaction(['store']); var store = cursorTx.objectStore('store'); var values = []; var keys = []; var whenTxComplete = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { whenTxComplete.callback(); }); var whenCursorComplete = new goog.async.Deferred(); var cursor = store.openCursor( goog.db.KeyRange.bound('b', 'c'), goog.db.Cursor.Direction.PREV); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { values.push(cursor.getValue()); keys.push(cursor.getKey()); cursor.next(); }); goog.events.listenOnce(cursor, [ goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR ], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { whenCursorComplete.callback(db); } else { whenCursorComplete.errback(); } }); var whenAllComplete = goog.async.DeferredList.gatherResults([ whenCursorComplete, whenTxComplete ]); whenAllComplete.addCallback(function(results) { var db = results[0]; assertArrayEquals(['3', '2'], values); assertArrayEquals(['c', 'b'], keys); closeAndContinue(db); }); }; asyncTestCase.waitForAsync('getting range'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(openCursorAndCheck); } function testObjectStoreCursorReplace() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStore, values, keys); // Store should contain ['1', '2', '5', '4'] after replacement. var checkStore = goog.partial(assertStoreValues, ['1', '2', '5', '4']); // Use a bounded cursor for ('b', 'c'] to update value '3' -> '5'. var openCursorAndReplace = function(db) { var d = new goog.async.Deferred(); var cursorTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var whenTxComplete = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { whenTxComplete.callback(); }); var store = cursorTx.objectStore('store'); var whenCursorComplete = new goog.async.Deferred(); var cursor = store.openCursor(goog.db.KeyRange.bound('b', 'c', true)); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { assertEquals('3', cursor.getValue()); cursor.update('5').addCallback(function() { cursor.next(); }).addErrback(failOnError); }); goog.events.listenOnce(cursor, [ goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR ], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { whenCursorComplete.callback(db); } else { whenCursorComplete.errback(); } }); goog.async.DeferredList.gatherResults([ whenCursorComplete, whenTxComplete ]).addCallbacks(function(results) { d.callback(results[0]); }, failOnError); return d; }; // Setup and execute test case. asyncTestCase.waitForAsync('replacing value by cursor'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(openCursorAndReplace). addCallback(checkStore); } function testObjectStoreCursorRemove() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStore, values, keys); // Store should contain ['1', '2'] after removing elements. var checkStore = goog.partial(assertStoreValues, ['1', '2']); // Use a bounded cursor for ('b', ...) to remove '3', '4'. var openCursorAndRemove = function(db) { var d = new goog.async.Deferred(); var cursorTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var whenTxComplete = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { whenTxComplete.callback(); }); var store = cursorTx.objectStore('store'); var whenCursorComplete = new goog.async.Deferred(); var cursor = store.openCursor(goog.db.KeyRange.lowerBound('b', true)); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { cursor.remove('5').addCallback(function() { cursor.next(); }).addErrback(failOnError); }); goog.events.listenOnce(cursor, [ goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR ], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { whenCursorComplete.callback(db); } else { whenCursorComplete.errback(); } }); goog.async.DeferredList.gatherResults([ whenCursorComplete, whenTxComplete ]).addCallbacks(function(results) { d.callback(results[0]); }, failOnError); return d; }; // Setup and execute test case. asyncTestCase.waitForAsync('removing value by cursor'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(openCursorAndRemove). addCallback(checkStore); } function testClear() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('clearing'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var rw = goog.db.Transaction.TransactionMode.READ_WRITE; var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var ERROR = goog.db.Transaction.EventTypes.ERROR; var putTx = db.createTransaction(['store'], rw); putTx.objectStore('store').put('1', 'a'); putTx.objectStore('store').put('2', 'b'); putTx.objectStore('store').put('3', 'c'); goog.events.listen(putTx, ERROR, failOnErrorEvent); goog.events.listen(putTx, COMPLETE, function() { var clearTx = db.createTransaction(['store'], rw); clearTx.objectStore('store').clear(); goog.events.listen(clearTx, ERROR, failOnErrorEvent); goog.events.listen(clearTx, COMPLETE, function() { var checkResultsTx = db.createTransaction(['store']); checkResultsTx.objectStore('store').getAll().addCallback( function(results) { assertEquals(0, results.length); }).addErrback(failOnError); goog.events.listen(checkResultsTx, ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, function() { closeAndContinue(db); }); }); }); }).addErrback(failOnError); } function testAbortTransaction() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('abort transaction'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var abortTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); abortTx.objectStore('store').put('data', 'stuff').addCallback(function() { abortTx.abort(); }); goog.events.listen( abortTx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen( abortTx, goog.db.Transaction.EventTypes.COMPLETE, function() { fail('transaction shouldn\'t have completed after being aborted'); }); goog.events.listen( abortTx, goog.db.Transaction.EventTypes.ABORT, function() { var checkResultsTx = db.createTransaction(['store']); checkResultsTx.objectStore('store').get('stuff').addCallback( function(result) { assertUndefined(result); }); goog.events.listen( checkResultsTx, goog.db.Transaction.EventTypes.COMPLETE, function() { closeAndContinue(db); }); }); }).addErrback(failOnError); } function testInactiveTransaction() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('inactive transaction'); globalDb.branch().addCallback(addStoreWithIndex).addCallback(function(db) { var tx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var store = tx.objectStore('store'); var index = store.getIndex('index'); store.put({key: 'something', value: 'anything'}); goog.events.listen(tx, goog.db.Transaction.EventTypes.COMPLETE, function() { var expectedCode = goog.db.Error.ErrorName.TRANSACTION_INACTIVE_ERR; store.put({ key: 'another', value: 'thing' }).addCallback(function() { fail('putting with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); store.add({ key: 'another', value: 'thing' }).addCallback(function() { fail('adding with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); store.remove('something').addCallback(function() { fail('deleting with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); store.get('something').addCallback(function() { fail('getting with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); store.getAll().addCallback(function() { fail('getting all with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); store.clear().addCallback(function() { fail('clearing all with inactive transaction should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); index.get('anything'). addCallback(function() { fail('getting from index with inactive transaction should have ' + 'failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); index.getKey('anything'). addCallback(function() { fail('getting key from index with inactive transaction ' + 'should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); index.getAll('anything'). addCallback(function() { fail('getting all from index with inactive transaction ' + 'should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); index.getAllKeys('anything'). addCallback(function() { fail('getting all keys from index with inactive transaction ' + 'should have failed'); }).addErrback(function(err) { assertEquals(expectedCode, err.getName()); }); closeAndContinue(db); }); }).addErrback(failOnError); } function testWrongTransactionMode() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('wrong transaction mode'); globalDb.branch().addCallback(addStore).addCallback(function(db) { var tx = db.createTransaction(['store']); assertEquals(goog.db.Transaction.TransactionMode.READ_ONLY, tx.getMode()); tx.objectStore('store').put('KABOOM!', 'anything').addCallback(function() { fail('putting should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.READ_ONLY_ERR, err.getName()); }); tx.objectStore('store').add('EXPLODE!', 'die').addCallback(function() { fail('adding should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.READ_ONLY_ERR, err.getName()); }); tx.objectStore('store').remove('no key', 'nothing').addCallback(function() { fail('deleting should have failed'); }).addErrback(function(err) { assertEquals(goog.db.Error.ErrorName.READ_ONLY_ERR, err.getName()); }); closeAndContinue(db); }).addErrback(failOnError); } function testManipulateIndexes() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('index manipulation'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { var store = db.createObjectStore('store'); store.createIndex('index', 'attr1'); store.createIndex('uniqueIndex', 'attr2', {unique: true}); store.createIndex('multirowIndex', 'attr3', {multirow: true}); }); }).addCallback(function(db) { var tx = db.createTransaction(['store']); var store = tx.objectStore('store'); var index = store.getIndex('index'); var uniqueIndex = store.getIndex('uniqueIndex'); var multirowIndex = store.getIndex('multirowIndex'); try { var dies = store.getIndex('diediedie'); fail('getting non-existent index should have failed'); } catch (err) { assertEquals(goog.db.Error.ErrorName.NOT_FOUND_ERR, err.getName()); } return tx.wait(); }).addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { var store = tx.objectStore('store'); store.deleteIndex('index'); try { store.deleteIndex('diediedie'); fail('deleting non-existent index should have failed'); } catch (err) { assertEquals(goog.db.Error.ErrorName.NOT_FOUND_ERR, err.getName()); } }); }).addCallback(function(db) { var tx = db.createTransaction(['store']); var store = tx.objectStore('store'); try { var index = store.getIndex('index'); fail('getting deleted index should have failed'); } catch (err) { assertEquals(goog.db.Error.ErrorName.NOT_FOUND_ERR, err.getName()); } var uniqueIndex = store.getIndex('uniqueIndex'); var multirowIndex = store.getIndex('multirowIndex'); }).addCallback(closeAndContinue).addErrback(failOnError); } function testAddRecordWithIndex() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('add record with index'); globalDb.branch().addCallback(addStoreWithIndex).addCallback(function(db) { var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var addTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var store = addTx.objectStore('store'); assertFalse(store.getIndex('index').isUnique()); assertEquals('value', store.getIndex('index').getKeyPath()); store.add({key: 'someKey', value: 'lookUpThis'}); goog.events.listen(addTx, COMPLETE, function() { var checkResultsTx = db.createTransaction(['store']); var index = checkResultsTx.objectStore('store').getIndex('index'); index.get('lookUpThis').addCallback(function(result) { assertNotUndefined(result); assertEquals('someKey', result.key); assertEquals('lookUpThis', result.value); }); index.getKey('lookUpThis').addCallback(function(result) { assertNotUndefined(result); assertEquals('someKey', result); }); goog.events.listen( checkResultsTx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, function() { closeAndContinue(db); }); }); }).addErrback(failOnError); } function testGetMultipleRecordsFromIndex() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('get multiple records from index'); globalDb.branch().addCallback(addStoreWithIndex).addCallback(function(db) { var COMPLETE = goog.db.Transaction.EventTypes.COMPLETE; var addTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); addTx.objectStore('store').add({key: '1', value: 'a'}); addTx.objectStore('store').add({key: '2', value: 'a'}); addTx.objectStore('store').add({key: '3', value: 'b'}); // The following line breaks Chrome 14, but not Chrome 15: // addTx.objectStore('store').add({key: '4'}); goog.events.listen( addTx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen(addTx, COMPLETE, function() { var checkResultsTx = db.createTransaction(['store']); var index = checkResultsTx.objectStore('store').getIndex('index'); index.getAll().addCallback(function(results) { assertNotUndefined(results); assertEquals(3, results.length); }); index.getAll('a').addCallback(function(results) { assertNotUndefined(results); assertEquals(2, results.length); }); index.getAllKeys().addCallback(function(results) { assertNotUndefined(results); assertEquals(3, results.length); }); index.getAllKeys('b').addCallback(function(results) { assertNotUndefined(results); assertEquals(1, results.length); }); goog.events.listen( checkResultsTx, goog.db.Transaction.EventTypes.ERROR, failOnErrorEvent); goog.events.listen(checkResultsTx, COMPLETE, function() { closeAndContinue(db); }); }); }).addErrback(failOnError); } function testUniqueIndex() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('adding to unique index'); globalDb.branch().addCallback(function(db) { return incrementVersion(db, function(ev, db, tx) { var store = db.createObjectStore('store', {keyPath: 'key'}); store.createIndex('index', 'value', {unique: true}); }); }).addCallback(function(db) { var tx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); assertTrue(tx.objectStore('store').getIndex('index').isUnique()); tx.objectStore('store').add({key: '1', value: 'a'}); tx.objectStore('store').add({key: '2', value: 'a'}); goog.events.listen(tx, goog.db.Transaction.EventTypes.ERROR, function(ev) { // expected assertTrue( 'Expected DATA_ERR, CONSTRAINT_ERR, was ', // Chrome 21, 23+. goog.db.Error.ErrorName.CONSTRAINT_ERR == ev.target.getName() || // Chrome 22. goog.db.Error.ErrorName.DATE_ERR == ev.target.getName()); closeAndContinue(db); }); }).addErrback(failOnError); } function testDeleteDatabase() { if (!idbSupported) { return; } asyncTestCase.waitForAsync('deleting database'); globalDb.branch().addCallback(addStore).addCallback(function(db) { db.close(); return goog.db.deleteDatabase(dbName, function() { fail('didn\'t expect deleteDatabase to be blocked'); }); }).addCallback(openDatabase). addCallback(assertStoreDoesntExist). addErrback(failOnError); } function testDeleteDatabaseIsBlocked() { if (!idbSupported) { return; } var wasBlocked = false; asyncTestCase.waitForAsync('deleting database (blocked)'); globalDb.branch().addCallback(addStore).addCallback(function(db) { db.close(); // Get a fresh connection, without any events registered on globalDb. return goog.db.openDatabase(dbName); }).addCallback(function(db) { return goog.db.deleteDatabase(dbName, function(ev) { wasBlocked = true; db.close(); }); }).addCallback(function() { assertTrue(wasBlocked); return openDatabase(); }).addCallback(assertStoreDoesntExist).addErrback(failOnError); } function testBlockedDeleteDatabaseWithVersionChangeEvent() { if (!idbSupported) { return; } var gotVersionChange = false; asyncTestCase.waitForAsync('deleting database (blocked)'); globalDb.branch().addCallback(addStore).addCallback(function(db) { db.close(); // Get a fresh connection, without any events registered on globalDb. return goog.db.openDatabase(dbName); }).addCallback(function(db) { goog.events.listen(db, goog.db.IndexedDb.EventType.VERSION_CHANGE, function(ev) { gotVersionChange = true; db.close(); }); return goog.db.deleteDatabase(dbName); }).addCallback(function() { assertTrue(gotVersionChange); return openDatabase(); }).addCallback(assertStoreDoesntExist); } function testDeleteNonExistentDatabase() { if (!idbSupported) { return; } // Deleting non-existent db is a no-op. Shall not throw anything. asyncTestCase.waitForAsync('check delete non-existent db'); globalDb.branch().addCallback(function(db) { db.close(); return goog.db.deleteDatabase('non-existent-db'); }).addCallbacks(function() { asyncTestCase.continueTesting(); }, failOnError); } function testObjectStoreCountAll() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStore, values, keys); var checkCountAll = function(db) { var tx = db.createTransaction(['store']); var store = tx.objectStore('store'); return store.count().addCallbacks(function(count) { assertEquals(values.length, count); tx.dispose(); closeAndContinue(db); }, function(e) { tx.dispose(); db.close(); failOnError(e); }); }; asyncTestCase.waitForAsync('testObjectStoreCountAll'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(checkCountAll); } function testObjectStoreCountSome() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStore, values, keys); var checkCountSome = function(db) { var tx = db.createTransaction(['store']); var store = tx.objectStore('store'); return store.count(goog.db.KeyRange.bound('b', 'c')).addCallbacks( function(count) { assertEquals(2, count); tx.dispose(); closeAndContinue(db); }, function(e) { tx.dispose(); db.close(); failOnError(e); }); }; asyncTestCase.waitForAsync('testObjectStoreCountSome'); globalDb.branch().addCallbacks(addStore, failOnError). addCallback(addData). addCallback(checkCountSome); } function testIndexCursorGet() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStoreWithObjects, values, keys); // Open the cursor over range ['b', 'c'], move in backwards direction. var openCursorAndCheck = function(db) { var cursorTx = db.createTransaction(['store']); var store = cursorTx.objectStore('store'); var index = store.getIndex('index'); var values = []; var keys = []; var txDeferred = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { txDeferred.callback(); }); var cursorDeferred = new goog.async.Deferred(); var cursor = index.openCursor( goog.db.KeyRange.bound('2', '3'), goog.db.Cursor.Direction.PREV); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { values.push(cursor.getValue()['value']); keys.push(cursor.getValue()['key']); cursor.next(); }); goog.events.listenOnce( cursor, [goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { cursorDeferred.callback(db); } else { cursorDeferred.errback(); } }); goog.async.DeferredList.gatherResults( [cursorDeferred, txDeferred]).addCallback( function(results) { goog.events.unlistenByKey(key); var db = results[0]; assertArrayEquals(['3', '2'], values); assertArrayEquals(['c', 'b'], keys); closeAndContinue(db); }); }; asyncTestCase.waitForAsync('testIndexCursorGet'); globalDb.branch().addCallbacks(addStoreWithIndex, failOnError). addCallback(addData). addCallback(openCursorAndCheck); } function testIndexCursorReplace() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStoreWithObjects, values, keys); // Store should contain ['1', '2', '5', '4'] after replacement. var checkStore = goog.partial(assertStoreObjectValues, ['1', '2', '5', '4']); // Use a bounded cursor for ['3', '4') to update value '3' -> '5'. var openCursorAndReplace = function(db) { var d = new goog.async.Deferred(); var cursorTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var txDeferred = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { txDeferred.callback(); }); var store = cursorTx.objectStore('store'); var index = store.getIndex('index'); var cursorDeferred = new goog.async.Deferred(); var cursor = index.openCursor( goog.db.KeyRange.bound('3', '4', false, true)); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { assertEquals('3', cursor.getValue()['value']); cursor.update( { 'key': cursor.getValue()['key'], 'value': '5' }).addCallback(function() { cursor.next(); }).addErrback(failOnError); }); goog.events.listenOnce( cursor, [goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { cursorDeferred.callback(db); } else { cursorDeferred.errback(); } }); goog.async.DeferredList.gatherResults( [cursorDeferred, txDeferred]).addCallbacks( function(results) { goog.events.unlistenByKey(key); d.callback(results[0]); }, failOnError); return d; }; // Setup and execute test case. asyncTestCase.waitForAsync('replacing value by cursor'); globalDb.branch().addCallbacks(addStoreWithIndex, failOnError). addCallback(addData). addCallback(openCursorAndReplace). addCallback(checkStore); } function testIndexCursorRemove() { if (!idbSupported) { return; } var values = ['1', '2', '3', '4']; var keys = ['a', 'b', 'c', 'd']; var addData = goog.partial(populateStoreWithObjects, values, keys); // Store should contain ['1', '2'] after removing elements. var checkStore = goog.partial(assertStoreObjectValues, ['1', '2']); // Use a bounded cursor for ('2', ...) to remove '3', '4'. var openCursorAndRemove = function(db) { var d = new goog.async.Deferred(); var cursorTx = db.createTransaction( ['store'], goog.db.Transaction.TransactionMode.READ_WRITE); var txDeferred = new goog.async.Deferred(); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.ERROR, failOnError); goog.events.listen( cursorTx, goog.db.Transaction.EventTypes.COMPLETE, function() { txDeferred.callback(); }); var store = cursorTx.objectStore('store'); var index = store.getIndex('index'); var cursorDeferred = new goog.async.Deferred(); var cursor = index.openCursor(goog.db.KeyRange.lowerBound('2', true)); var key = goog.events.listen( cursor, goog.db.Cursor.EventType.NEW_DATA, function() { cursor.remove('5').addCallback(function() { cursor.next(); }).addErrback(failOnError); }); goog.events.listenOnce( cursor, [goog.db.Cursor.EventType.COMPLETE, goog.db.Cursor.EventType.ERROR], function(evt) { goog.events.unlistenByKey(key); if (evt.type == goog.db.Cursor.EventType.COMPLETE) { cursorDeferred.callback(db); } else { cursorDeferred.errback(); } }); goog.async.DeferredList.gatherResults( [cursorDeferred, txDeferred]).addCallbacks( function(results) { goog.events.unlistenByKey(key); d.callback(results[0]); }, failOnError); return d; }; // Setup and execute test case. asyncTestCase.waitForAsync('removing value by cursor'); globalDb.branch().addCallbacks(addStoreWithIndex, failOnError). addCallback(addData). addCallback(openCursorAndRemove). addCallback(checkStore); }