/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
goog.module('goog.dbTest');
goog.setTestOnly();
const Cursor = goog.require('goog.db.Cursor');
const DbError = goog.require('goog.db.Error');
const GoogPromise = goog.require('goog.Promise');
const IndexedDb = goog.require('goog.db.IndexedDb');
const KeyRange = goog.require('goog.db.KeyRange');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const TestCase = goog.require('goog.testing.TestCase');
const Transaction = goog.require('goog.db.Transaction');
const asserts = goog.require('goog.testing.asserts');
const events = goog.require('goog.events');
const googArray = goog.require('goog.array');
const googDb = goog.require('goog.db');
const product = goog.require('goog.userAgent.product');
const testSuite = goog.require('goog.testing.testSuite');
const idbSupported = product.CHROME;
let dbName;
const dbBaseName = 'testDb';
let globalDb = null;
let dbsToClose = [];
let propertyReplacer;
const baseVersion = 1;
let dbVersion = 1;
const TransactionMode = Transaction.TransactionMode;
const EventTypes = Transaction.EventTypes;
function openDatabase() {
return googDb.openDatabase(dbName).addCallback((db) => {
dbsToClose.push(db);
});
}
function incrementVersion(db, onUpgradeNeeded) {
db.close();
const onBlocked = (ev) => {
fail(`Upgrade to version ${dbVersion} is blocked.`);
};
return googDb.openDatabase(dbName, ++dbVersion, onUpgradeNeeded, onBlocked)
.addCallback((db) => {
dbsToClose.push(db);
assertEquals(dbVersion, db.getVersion());
});
}
function addStore(db) {
return incrementVersion(db, (ev, db, tx) => {
db.createObjectStore('store');
});
}
function addStoreWithIndex(db) {
return incrementVersion(db, (ev, db, tx) => {
const store = db.createObjectStore('store', {keyPath: 'key'});
store.createIndex('index', 'value');
});
}
function populateStore(values, keys, db) {
const putTx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = putTx.objectStore('store');
for (let i = 0; i < values.length; ++i) {
store.put(values[i], keys[i]);
}
return putTx.wait();
}
function populateStoreWithObjects(values, keys, db) {
const putTx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = putTx.objectStore('store');
googArray.forEach(values, (value, index) => {
store.put({'key': keys[index], 'value': value});
});
return putTx.wait();
}
function assertStoreValues(values, db) {
const assertStoreTx = db.createTransaction(['store']);
assertStoreTx.objectStore('store').getAll().addCallback((results) => {
assertSameElements(values, results);
});
}
/**
* Assert the keys are as expected.
* @param {!Array<string>} keys - The expected keys.
* @param {!IndexedDb} db - The indexed db.
*/
function assertStoreKeyValues(keys, db) {
const assertStoreTx = db.createTransaction(['store']);
assertStoreTx.objectStore('store').getAllKeys().addCallback((results) => {
assertSameElements(keys, results);
});
}
function assertStoreObjectValues(values, db) {
const assertStoreTx = db.createTransaction(['store']);
assertStoreTx.objectStore('store').getAll().addCallback((results) => {
const retrievedValues = googArray.map(results, (result) => result['value']);
assertSameElements(values, retrievedValues);
});
}
function assertStoreDoesntExist(db) {
try {
db.createTransaction(['store']);
fail('Create transaction with a non-existent store should have failed.');
} catch (e) {
// expected
assertEquals(e.getName(), DbError.ErrorName.NOT_FOUND_ERR);
}
}
function transactionToPromise(db, trx) {
return new GoogPromise((resolve, reject) => {
events.listen(trx, EventTypes.ERROR, reject);
events.listen(trx, EventTypes.COMPLETE, () => {
resolve(db);
});
});
}
// Calls onRecordReady each time that a new record can be read by the
// cursor with cursor.next(). Returns a promise that resolves or rejects
// based on when the cursor is complete or errors out. If onRecordReady
// returns a promise that promises is also waited on before the returned
// promise resolves.
function forEachRecord(cursor, onRecordReady) {
const promises = [];
return new GoogPromise((resolve, reject) => {
const key = events.listen(cursor, Cursor.EventType.NEW_DATA, () => {
const result = onRecordReady();
if (result && ('then' in result)) {
promises.push(result);
}
});
events.listenOnce(
cursor, [Cursor.EventType.COMPLETE, Cursor.EventType.ERROR],
(evt) => {
events.unlistenByKey(key);
if (evt.type == Cursor.EventType.COMPLETE) {
resolve();
} else {
reject(evt);
}
});
})
.then(() => GoogPromise.all(promises));
}
function failOnErrorEvent(ev) {
fail(ev.target.message);
}
testSuite({
setUpPage() {
propertyReplacer = new PropertyReplacer();
},
setUp() {
if (!idbSupported) {
return;
}
// Always use a clean database by generating a new database name.
dbName = dbBaseName + Date.now().toString();
globalDb = openDatabase();
},
tearDown() {
for (let i = 0; i < dbsToClose.length; i++) {
dbsToClose[i].close();
}
dbsToClose = [];
propertyReplacer.reset();
},
testDatabaseOpened() {
if (!idbSupported) {
return;
}
assertNotNull(globalDb);
return globalDb.branch().addCallback((db) => {
assertTrue(db.isOpen());
});
},
testOpenWithNewVersion() {
if (!idbSupported) {
return;
}
let upgradeNeeded = false;
return globalDb.branch()
.addCallback((db) => {
assertEquals(baseVersion, db.getVersion());
return incrementVersion(db, (ev, db, tx) => {
upgradeNeeded = true;
});
})
.addCallback((db) => {
assertTrue(upgradeNeeded);
});
},
testManipulateObjectStores() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback((db) => {
assertEquals(baseVersion, db.getVersion());
return incrementVersion(db, (ev, db, tx) => {
db.createObjectStore('basicStore');
db.createObjectStore('keyPathStore', {keyPath: 'keyGoesHere'});
db.createObjectStore('autoIncrementStore', {autoIncrement: true});
});
})
.addCallback((db) => {
const storeNames = db.getObjectStoreNames();
assertEquals(3, storeNames.length);
assertTrue(storeNames.contains('basicStore'));
assertTrue(storeNames.contains('keyPathStore'));
assertTrue(storeNames.contains('autoIncrementStore'));
return incrementVersion(db, (ev, db, tx) => {
db.deleteObjectStore('basicStore');
});
})
.addCallback((db) => {
const storeNames = db.getObjectStoreNames();
assertEquals(2, storeNames.length);
assertFalse(storeNames.contains('basicStore'));
assertTrue(storeNames.contains('keyPathStore'));
assertTrue(storeNames.contains('autoIncrementStore'));
});
},
testBadObjectStoreManipulation() {
if (!idbSupported) {
return;
}
const expectedCode = DbError.ErrorName.INVALID_STATE_ERR;
return globalDb.branch()
.addCallback((db) => {
try {
db.createObjectStore('diediedie');
fail('Create object store outside transaction should have failed.');
} catch (err) {
// expected
assertEquals(expectedCode, err.getName());
}
})
.addCallback(addStore)
.addCallback((db) => {
try {
db.deleteObjectStore('store');
fail('Delete object store outside transaction should have failed.');
} catch (err) {
// expected
assertEquals(expectedCode, err.getName());
}
})
.addCallback((db) => {
return incrementVersion(db, (ev, db, tx) => {
try {
db.deleteObjectStore('diediedie');
fail('Delete non-existent store should have failed.');
} catch (err) {
// expected
assertEquals(DbError.ErrorName.NOT_FOUND_ERR, err.getName());
}
});
});
},
testGetNonExistentObjectStore() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
const tx = db.createTransaction(['store']);
try {
tx.objectStore('diediedie');
fail('getting non-existent object store should have failed');
} catch (err) {
assertEquals(DbError.ErrorName.NOT_FOUND_ERR, err.getName());
}
});
},
testCreateTransaction() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
let tx = db.createTransaction(['store']);
assertEquals(
'mode not READ_ONLY', TransactionMode.READ_ONLY, tx.getMode());
tx = db.createTransaction(
['store'], Transaction.TransactionMode.READ_WRITE);
assertEquals(
'mode not READ_WRITE', TransactionMode.READ_WRITE, tx.getMode());
});
},
testPutRecord() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
const initialPutTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const putOperation = initialPutTx.objectStore('store').put(
{key: 'initial', value: 'value1'}, 'putKey');
putOperation.addCallback((key) => {
assertEquals('putKey', key);
});
return transactionToPromise(db, initialPutTx);
})
.addCallback((db) => {
const checkResultsTx = db.createTransaction(['store']);
const getOperation =
checkResultsTx.objectStore('store').get('putKey');
getOperation.addCallback((result) => {
assertEquals('initial', result.key);
assertEquals('value1', result.value);
});
return transactionToPromise(db, checkResultsTx);
})
.addCallback((db) => {
const overwriteTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const putOperation = overwriteTx.objectStore('store').put(
{key: 'overwritten', value: 'value2'}, 'putKey');
putOperation.addCallback((key) => {
assertEquals('putKey', key);
});
return transactionToPromise(db, overwriteTx);
})
.addCallback((db) => {
const checkOverwriteTx = db.createTransaction(['store']);
checkOverwriteTx.objectStore('store').get('putKey').addCallback(
(result) => {
// this is guaranteed to run before the COMPLETE event fires on
// the transaction
assertEquals('overwritten', result.key);
assertEquals('value2', result.value);
});
return transactionToPromise(db, checkOverwriteTx);
});
},
testAddRecord() {
TestCase.getActiveTestCase().promiseTimeout = 60 * 1000; // msecs
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
const initialAddTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const addOperation = initialAddTx.objectStore('store').add(
{key: 'hi', value: 'something'}, 'stuff');
addOperation.addCallback((key) => {
assertEquals('stuff', key);
});
return transactionToPromise(db, initialAddTx);
})
.addCallback((db) => {
const successfulAddTx = db.createTransaction(['store']);
const getOperation =
successfulAddTx.objectStore('store').get('stuff');
getOperation.addCallback((result) => {
assertEquals('hi', result.key);
assertEquals('something', result.value);
});
return transactionToPromise(db, successfulAddTx);
})
.addCallback((db) => {
const addOverwriteTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
addOverwriteTx.objectStore('store')
.add({key: 'bye', value: 'nothing'}, 'stuff')
.addErrback((err) => {
// expected
assertEquals(DbError.ErrorName.CONSTRAINT_ERR, err.getName());
});
return transactionToPromise(db, addOverwriteTx)
.then(
() => {
fail('adding existing record should not have succeeded');
},
(ev) => {
// expected
assertEquals(
DbError.ErrorName.CONSTRAINT_ERR, ev.target.getName());
});
});
},
testPutRecordKeyPathStore() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
db.createObjectStore('keyStore', {keyPath: 'key'});
}))
.addCallback((db) => {
const putTx =
db.createTransaction(['keyStore'], TransactionMode.READ_WRITE);
const putOperation = putTx.objectStore('keyStore')
.put({key: 'hi', value: 'something'});
putOperation.addCallback((key) => {
assertEquals('hi', key);
});
return transactionToPromise(db, putTx);
})
.addCallback((db) => {
const checkResultsTx = db.createTransaction(['keyStore']);
checkResultsTx.objectStore('keyStore')
.get('hi')
.addCallback((result) => {
assertNotUndefined(result);
assertEquals('hi', result.key);
assertEquals('something', result.value);
});
return transactionToPromise(db, checkResultsTx);
});
},
testPutBadRecordKeyPathStore() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
db.createObjectStore('keyStore', {keyPath: 'key'});
}))
.addCallback((db) => {
const badTx =
db.createTransaction(['keyStore'], TransactionMode.READ_WRITE);
return badTx.objectStore('keyStore')
.put({key: 'diedie', value: 'anything'}, 'badKey')
.then(
() => {
fail('inserting with explicit key should have failed');
},
(err) => {
// expected
assertEquals(DbError.ErrorName.DATA_ERR, err.getName());
});
});
},
testPutRecordAutoIncrementStore() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
db.createObjectStore('aiStore', {autoIncrement: true});
}))
.addCallback((db) => {
const tx =
db.createTransaction(['aiStore'], TransactionMode.READ_WRITE);
const putOperation1 = tx.objectStore('aiStore').put('1');
const putOperation2 = tx.objectStore('aiStore').put('2');
const putOperation3 = tx.objectStore('aiStore').put('3');
putOperation1.addCallback((key) => {
assertNotUndefined(key);
});
putOperation2.addCallback((key) => {
assertNotUndefined(key);
});
putOperation3.addCallback((key) => {
assertNotUndefined(key);
});
return transactionToPromise(db, tx);
})
.addCallback((db) => {
const tx = db.createTransaction(['aiStore']);
const getAllOperation = tx.objectStore('aiStore').getAll();
return getAllOperation.addCallback((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'));
});
});
},
testPutRecordKeyPathAndAutoIncrementStore() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
db.createObjectStore(
'hybridStore', {keyPath: 'key', autoIncrement: true});
}))
.addCallback((db) => {
const tx =
db.createTransaction(['hybridStore'], TransactionMode.READ_WRITE);
const putOperation =
tx.objectStore('hybridStore').put({value: 'whatever'});
putOperation.addCallback((key) => {
assertNotUndefined(key);
});
return putOperation.addCallback(() => db);
})
.addCallback((db) => {
const tx = db.createTransaction(['hybridStore']);
return tx.objectStore('hybridStore').getAll().then((results) => {
assertEquals(1, results.length);
assertEquals('whatever', results[0].value);
assertNotUndefined(results[0].key);
});
});
},
testPutIllegalRecords() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
const tx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
const promises = [];
const badKeyFail = (keyKind) => () =>
fail(`putting with ${keyKind} key should have failed`);
const assertExpectedError = (err) => {
assertEquals(DbError.ErrorName.DATA_ERR, err.getName());
};
promises.push(tx.objectStore('store')
.put('death', null)
.then(badKeyFail('null'), assertExpectedError));
promises.push(tx.objectStore('store')
.put('death', NaN)
.then(badKeyFail('NaN'), assertExpectedError));
promises.push(tx.objectStore('store')
.put('death', undefined)
.then(badKeyFail('undefined'), assertExpectedError));
return GoogPromise.all(promises);
});
},
testPutIllegalRecordsWithIndex() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback((db) => {
const tx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const promises = [];
const badKeyFail = (keyKind) => () => {
fail(`putting with ${keyKind} key should have failed`);
};
const assertExpectedError = (err) => {
// expected
assertEquals(DbError.ErrorName.DATA_ERR, err.getName());
};
promises.push(tx.objectStore('store')
.put({value: 'diediedie', key: null})
.then(badKeyFail('null'), assertExpectedError));
promises.push(tx.objectStore('store')
.put({value: 'dietodeath', key: NaN})
.then(badKeyFail('NaN'), assertExpectedError));
promises.push(
tx.objectStore('store')
.put({value: 'dietodeath', key: undefined})
.then(badKeyFail('undefined'), assertExpectedError));
return GoogPromise.all(promises);
});
},
testDeleteRecord() {
if (!idbSupported) {
return;
}
let db;
return globalDb.branch()
.addCallback(addStore)
.addCallback((openedDb) => {
db = openedDb;
return db.createTransaction(['store'], TransactionMode.READ_WRITE)
.objectStore('store')
.put({key: 'hi', value: 'something'}, 'stuff');
})
.addCallback(
() => db.createTransaction(['store'], TransactionMode.READ_WRITE)
.objectStore('store')
.remove('stuff'))
.addCallback(
() => db.createTransaction(['store']).objectStore('store').get(
'stuff'))
.addCallback((result) => {
assertUndefined(result);
});
},
testDeleteRange() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3'];
const keys = ['a', 'b', 'c'];
const addData = goog.partial(populateStore, values, keys);
const checkStore = goog.partial(assertStoreValues, ['1']);
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(
(db) => db.createTransaction(['store'], TransactionMode.READ_WRITE)
.objectStore('store')
.remove(KeyRange.bound('b', 'c'))
.then(() => db))
.addCallback(checkStore);
},
testGetAll() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3'];
const keys = ['a', 'b', 'c'];
const addData = goog.partial(populateStore, values, keys);
const checkStore = goog.partial(assertStoreValues, values);
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(checkStore);
},
testGetAllKeys() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3'];
const keys = ['a', 'b', 'c'];
const addData = goog.partial(populateStore, values, keys);
const checkStore = goog.partial(assertStoreKeyValues, keys);
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(checkStore);
},
testObjectStoreCursorGet() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStore, values, keys);
let db;
const resultValues = [];
const resultKeys = [];
// Open the cursor over range ['b', 'c'], move in backwards direction.
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback((theDb) => {
db = theDb;
const cursorTx = db.createTransaction(['store']);
const store = cursorTx.objectStore('store');
const cursor =
store.openCursor(KeyRange.bound('b', 'c'), Cursor.Direction.PREV);
const whenCursorComplete = forEachRecord(cursor, () => {
resultValues.push(cursor.getValue());
resultKeys.push(cursor.getKey());
cursor.next();
});
return GoogPromise.all([cursorTx.wait(), whenCursorComplete]);
})
.addCallback(() => {
assertArrayEquals(['3', '2'], resultValues);
assertArrayEquals(['c', 'b'], resultKeys);
});
},
testObjectStoreCursorReplace() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStore, values, keys);
// Store should contain ['1', '2', '5', '4'] after replacement.
const checkStore = goog.partial(assertStoreValues, ['1', '2', '5', '4']);
// Use a bounded cursor for ('b', 'c'] to update value '3' -> '5'.
const openCursorAndReplace = (db) => {
const cursorTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = cursorTx.objectStore('store');
const cursor = store.openCursor(KeyRange.bound('b', 'c', true));
const whenCursorComplete = forEachRecord(cursor, () => {
assertEquals('3', cursor.getValue());
return cursor.update('5').addCallback(() => {
cursor.next();
});
});
return GoogPromise.all([cursorTx.wait(), whenCursorComplete])
.then(() => db);
};
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(openCursorAndReplace)
.addCallback(checkStore);
},
testObjectStoreCursorRemove() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStore, values, keys);
// Store should contain ['1', '2'] after removing elements.
const checkStore = goog.partial(assertStoreValues, ['1', '2']);
// Use a bounded cursor for ('b', ...) to remove '3', '4'.
const openCursorAndRemove = (db) => {
const cursorTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = cursorTx.objectStore('store');
const cursor = store.openCursor(KeyRange.lowerBound('b', true));
const whenCursorComplete =
forEachRecord(cursor, () => cursor.remove('5').addCallback(() => {
cursor.next();
}));
return GoogPromise.all([cursorTx.wait(), whenCursorComplete])
.then((results) => db);
};
// Setup and execute test case.
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(openCursorAndRemove)
.addCallback(checkStore);
},
testClear() {
if (!idbSupported) {
return;
}
let db;
return globalDb.branch()
.addCallback(addStore)
.addCallback((theDb) => {
db = theDb;
const putTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
putTx.objectStore('store').put('1', 'a');
putTx.objectStore('store').put('2', 'b');
putTx.objectStore('store').put('3', 'c');
return putTx.wait();
})
.addCallback(
() => db.createTransaction(['store']).objectStore('store').getAll())
.addCallback((results) => {
assertEquals(3, results.length);
return db.createTransaction(['store'], TransactionMode.READ_WRITE)
.objectStore('store')
.clear();
})
.addCallback(
() => db.createTransaction(['store']).objectStore('store').getAll())
.addCallback((results) => {
assertEquals(0, results.length);
});
},
testCommit() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
return new Promise((resolve, reject) => {
const commitTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = commitTx.objectStore('store');
store.put('data', 'stuff');
commitTx.commit(false /* allowNoopWhenUnsupported */);
store.put('data', 'another stuff')
.addCallback(() => {
fail('Should not able to add new data after commit');
})
.addErrback((e) => {
assertEquals(
DbError.ErrorName.TRANSACTION_INACTIVE_ERR, e.getName());
});
events.listen(commitTx, EventTypes.ERROR, reject);
events.listen(commitTx, EventTypes.COMPLETE, () => {
resolve(db);
});
});
})
.addCallback((db) => {
const checkResultsTx = db.createTransaction(['store']);
return checkResultsTx.objectStore('store').getAll();
})
.addCallback((result) => {
// Only 1 entry is committed
assertEquals(1, result.length);
assertEquals('data', result[0]);
});
},
testCommitNotSupported() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
if (!IDBTransaction.prototype.hasOwnProperty('commit')) {
const commitTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
try {
commitTx.commit();
} catch (e) {
assertEquals(DbError.ErrorName.UNKNOWN_ERR, e.getName());
}
}
});
},
testCommitTwice() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
const commitTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
commitTx.commit(false /* allowNoopWhenUnsupported */);
try {
commitTx.commit(false /* allowNoopWhenUnsupported */);
} catch (e) {
assertEquals(DbError.ErrorName.INVALID_STATE_ERR, e.getName());
}
});
},
testAbortTransaction() {
if (!idbSupported) {
return;
}
let db;
return globalDb.branch()
.addCallback(addStore)
.addCallback((theDb) => {
db = theDb;
return new Promise((resolve, reject) => {
const abortTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
abortTx.objectStore('store')
.put('data', 'stuff')
.addCallback(() => {
abortTx.abort();
});
events.listen(abortTx, EventTypes.ERROR, reject);
events.listen(abortTx, EventTypes.COMPLETE, () => {
fail(
'transaction shouldn\'t have' +
' completed after being aborted');
});
events.listen(abortTx, EventTypes.ABORT, resolve);
});
})
.addCallback(() => {
const checkResultsTx = db.createTransaction(['store']);
return checkResultsTx.objectStore('store').get('stuff');
})
.addCallback((result) => {
assertUndefined(result);
});
},
testInactiveTransaction() {
if (!idbSupported) {
return;
}
let db;
let store;
let index;
const createAndFinishTransaction = (theDb) => {
db = theDb;
const tx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
store = tx.objectStore('store');
index = store.getIndex('index');
store.put({key: 'something', value: 'anything'});
return tx.wait();
};
const assertCantUseInactiveTransaction = () => {
const expectedCode = DbError.ErrorName.TRANSACTION_INACTIVE_ERR;
const promises = [];
const failOp = (op) => () => {
fail(`${op} with inactive transaction should have failed`);
};
const assertCorrectError = (err) => {
assertEquals(expectedCode, err.getName());
};
const keyRange = KeyRange.bound('a', 'a');
promises.push(store.put({key: 'another', value: 'thing'})
.then(failOp('putting'), assertCorrectError));
promises.push(store.add({key: 'another', value: 'thing'})
.then(failOp('adding'), assertCorrectError));
promises.push(store.remove('something')
.then(failOp('deleting'), assertCorrectError));
promises.push(
store.get('something').then(failOp('getting'), assertCorrectError));
promises.push(
store.getAll().then(failOp('getting all'), assertCorrectError));
promises.push(
store.clear().then(failOp('clearing all'), assertCorrectError));
promises.push(
index.get('anything')
.then(failOp('getting from index'), assertCorrectError));
promises.push(
index.getKey('anything')
.then(failOp('getting key from index'), assertCorrectError));
promises.push(
index.getAll('anything')
.then(failOp('getting all from index'), assertCorrectError));
promises.push(
index.getAllKeys('anything')
.then(failOp('getting all keys from index'), assertCorrectError));
promises.push(index.getAll(keyRange).then(
failOp('getting all from index'), assertCorrectError));
promises.push(index.getAllKeys(keyRange).then(
failOp('getting all from index'), assertCorrectError));
return GoogPromise.all(promises);
};
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback(createAndFinishTransaction)
.addCallback(assertCantUseInactiveTransaction);
},
testWrongTransactionMode() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
const tx = db.createTransaction(['store']);
assertEquals(Transaction.TransactionMode.READ_ONLY, tx.getMode());
const promises = [];
promises.push(tx.objectStore('store')
.put('KABOOM!', 'anything')
.then(
() => {
fail('putting should have failed');
},
(err) => {
assertEquals(
DbError.ErrorName.READ_ONLY_ERR,
err.getName());
}));
promises.push(tx.objectStore('store')
.add('EXPLODE!', 'die')
.then(
() => {
fail('adding should have failed');
},
(err) => {
assertEquals(
DbError.ErrorName.READ_ONLY_ERR,
err.getName());
}));
promises.push(tx.objectStore('store')
.remove('no key', 'nothing')
.then(
() => {
fail('deleting should have failed');
},
(err) => {
assertEquals(
DbError.ErrorName.READ_ONLY_ERR,
err.getName());
}));
return GoogPromise.all(promises);
});
},
testManipulateIndexes() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
const store = db.createObjectStore('store');
store.createIndex('index', 'attr1');
store.createIndex('uniqueIndex', 'attr2', {unique: true});
store.createIndex('multirowIndex', 'attr3', {multirow: true});
}))
.addCallback((db) => {
const tx = db.createTransaction(['store']);
const store = tx.objectStore('store');
const index = store.getIndex('index');
const uniqueIndex = store.getIndex('uniqueIndex');
const multirowIndex = store.getIndex('multirowIndex');
try {
const dies = store.getIndex('diediedie');
fail('getting non-existent index should have failed');
} catch (err) {
// expected
assertEquals(DbError.ErrorName.NOT_FOUND_ERR, err.getName());
}
return tx.wait();
})
.addCallback((db) => {
return incrementVersion(db, (ev, db, tx) => {
const store = tx.objectStore('store');
store.deleteIndex('index');
try {
store.deleteIndex('diediedie');
fail('deleting non-existent index should have failed');
} catch (err) {
// expected
assertEquals(DbError.ErrorName.NOT_FOUND_ERR, err.getName());
}
});
})
.addCallback((db) => {
const tx = db.createTransaction(['store']);
const store = tx.objectStore('store');
try {
const index = store.getIndex('index');
fail('getting deleted index should have failed');
} catch (err) {
// expected
assertEquals(DbError.ErrorName.NOT_FOUND_ERR, err.getName());
}
const uniqueIndex = store.getIndex('uniqueIndex');
const multirowIndex = store.getIndex('multirowIndex');
});
},
testAddRecordWithIndex() {
TestCase.getActiveTestCase().promiseTimeout = 60 * 1000; // msecs
if (!idbSupported) {
return;
}
const addData = (db) => {
const store = db.createTransaction(['store'], TransactionMode.READ_WRITE)
.objectStore('store');
assertFalse(store.getIndex('index').isUnique());
assertEquals('value', store.getIndex('index').getKeyPath());
return store.add({key: 'someKey', value: 'lookUpThis'})
.addCallback(() => db);
};
const readAndAssertAboutData = (db) => {
const index =
db.createTransaction(['store']).objectStore('store').getIndex(
'index');
const promises = [
index.get('lookUpThis').addCallback((result) => {
assertNotUndefined(result);
assertEquals('someKey', result.key);
assertEquals('lookUpThis', result.value);
}),
index.getKey('lookUpThis').addCallback((result) => {
assertNotUndefined(result);
assertEquals('someKey', result);
}),
];
return GoogPromise.all(promises).then(() => db);
};
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback(addData)
.addCallback(readAndAssertAboutData);
},
testGetMultipleRecordsFromIndex() {
if (!idbSupported) {
return;
}
const addData = (db) => {
const addTx = db.createTransaction(['store'], 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'});
return addTx.wait();
};
const readData = (db) => {
const index =
db.createTransaction(['store']).objectStore('store').getIndex(
'index');
const promises = [];
const keyRange = KeyRange.bound('a', 'a');
promises.push(index.getAll().addCallback((results) => {
assertNotUndefined(results);
assertEquals(3, results.length);
}));
promises.push(index.getAll('a').addCallback((results) => {
assertNotUndefined(results);
assertEquals(2, results.length);
}));
promises.push(index.getAllKeys().addCallback((results) => {
assertNotUndefined(results);
assertEquals(3, results.length);
assertArrayEquals(['1', '2', '3'], results);
}));
promises.push(index.getAllKeys('b').addCallback((results) => {
assertNotUndefined(results);
assertEquals(1, results.length);
assertArrayEquals(['3'], results);
}));
promises.push(index.getAll(keyRange).addCallback((results) => {
assertNotUndefined(results);
assertEquals(2, results.length);
}));
promises.push(index.getAllKeys(keyRange).addCallback((results) => {
assertNotUndefined(results);
assertEquals(2, results.length);
assertArrayEquals(['1', '2'], results);
}));
return GoogPromise.all(promises).then(() => db);
};
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback(addData)
.addCallback(readData);
},
testUniqueIndex() {
if (!idbSupported) {
return;
}
const storeDuplicatesToUniqueIndex = (db) => {
const tx = db.createTransaction(['store'], 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'});
return transactionToPromise(db, tx).then(
() => {
fail('Expected transaction violating unique constraint to fail');
},
(ev) => {
// expected
assertEquals(DbError.ErrorName.CONSTRAINT_ERR, ev.target.getName());
});
};
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
const store = db.createObjectStore('store', {keyPath: 'key'});
store.createIndex('index', 'value', {unique: true});
}))
.addCallback(storeDuplicatesToUniqueIndex);
},
testDeleteDatabase() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
db.close();
return googDb.deleteDatabase(dbName, () => {
fail('didn\'t expect deleteDatabase to be blocked');
});
})
.addCallback(openDatabase)
.addCallback(assertStoreDoesntExist);
},
testDeleteDatabaseIsBlocked() {
if (!idbSupported) {
return;
}
let wasBlocked = false;
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
db.close();
// Get a fresh connection, without any events registered on globalDb.
return googDb.openDatabase(dbName);
})
.addCallback((db) => {
dbsToClose.push(db);
return googDb.deleteDatabase(dbName, (ev) => {
wasBlocked = true;
db.close();
});
})
.addCallback(() => {
assertTrue(wasBlocked);
return openDatabase();
})
.addCallback(assertStoreDoesntExist);
},
testBlockedDeleteDatabaseWithVersionChangeEvent() {
if (!idbSupported) {
return;
}
let gotVersionChange = false;
return globalDb.branch()
.addCallback(addStore)
.addCallback((db) => {
db.close();
// Get a fresh connection, without any events registered on globalDb.
return googDb.openDatabase(dbName);
})
.addCallback((db) => {
dbsToClose.push(db);
events.listen(db, IndexedDb.EventType.VERSION_CHANGE, (ev) => {
gotVersionChange = true;
db.close();
});
return googDb.deleteDatabase(dbName);
})
.addCallback(() => {
assertTrue(gotVersionChange);
return openDatabase();
})
.addCallback(assertStoreDoesntExist);
},
testDeleteNonExistentDatabase() {
if (!idbSupported) {
return;
}
// Deleting non-existent db is a no-op. Shall not throw anything.
return globalDb.branch().addCallback((db) => {
db.close();
return googDb.deleteDatabase('non-existent-db');
});
},
testObjectStoreCountAll() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStore, values, keys);
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback((db) => {
const tx = db.createTransaction(['store']);
return tx.objectStore('store').count().addCallback((count) => {
assertEquals(values.length, count);
});
});
},
testObjectStoreCountSome() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStore, values, keys);
const countData = (db) => {
const tx = db.createTransaction(['store']);
return tx.objectStore('store')
.count(KeyRange.bound('b', 'c'))
.addCallback((count) => {
assertEquals(2, count);
});
};
return globalDb.branch()
.addCallback(addStore)
.addCallback(addData)
.addCallback(countData);
},
testIndexCursorGet() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStoreWithObjects, values, keys);
const valuesResult = [];
const keysResult = [];
// Open the cursor over range ['b', 'c'], move in backwards direction.
const walkBackwardsOverCursor = (db) => {
const cursorTx = db.createTransaction(['store']);
const index = cursorTx.objectStore('store').getIndex('index');
const values = [];
const keys = [];
const cursor =
index.openCursor(KeyRange.bound('2', '3'), Cursor.Direction.PREV);
const cursorFinished = forEachRecord(cursor, () => {
valuesResult.push(cursor.getValue()['value']);
keysResult.push(cursor.getValue()['key']);
cursor.next();
});
return GoogPromise.all([cursorFinished, cursorTx.wait()]).then(() => db);
};
return globalDb.branch()
.addCallbacks(addStoreWithIndex)
.addCallback(addData)
.addCallback(walkBackwardsOverCursor)
.addCallback((db) => {
assertArrayEquals(['3', '2'], valuesResult);
assertArrayEquals(['c', 'b'], keysResult);
});
},
testIndexCursorReplace() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStoreWithObjects, values, keys);
const valuesResult = [];
const keysResult = [];
// Store should contain ['1', '2', '5', '4'] after replacement.
const checkStore =
goog.partial(assertStoreObjectValues, ['1', '2', '5', '4']);
// Use a bounded cursor for ['3', '4') to update value '3' -> '5'.
const openCursorAndReplace = (db) => {
const cursorTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const index = cursorTx.objectStore('store').getIndex('index');
const cursor = index.openCursor(KeyRange.bound('3', '4', false, true));
const cursorFinished = forEachRecord(cursor, () => {
assertEquals('3', cursor.getValue()['value']);
return cursor.update({'key': cursor.getValue()['key'], 'value': '5'})
.addCallback(() => {
cursor.next();
});
});
return GoogPromise.all([cursorFinished, cursorTx.wait()])
.then((results) => db);
};
// Setup and execute test case.
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback(addData)
.addCallback(openCursorAndReplace)
.addCallback(checkStore);
},
testIndexCursorRemove() {
if (!idbSupported) {
return;
}
const values = ['1', '2', '3', '4'];
const keys = ['a', 'b', 'c', 'd'];
const addData = goog.partial(populateStoreWithObjects, values, keys);
// Store should contain ['1', '2'] after removing elements.
const checkStore = goog.partial(assertStoreObjectValues, ['1', '2']);
// Use a bounded cursor for ('2', ...) to remove '3', '4'.
const openCursorAndRemove = (db) => {
const cursorTx =
db.createTransaction(['store'], TransactionMode.READ_WRITE);
const store = cursorTx.objectStore('store');
const index = store.getIndex('index');
const cursor = index.openCursor(KeyRange.lowerBound('2', true));
const cursorFinished =
forEachRecord(cursor, () => cursor.remove('5').addCallback(() => {
cursor.next();
}));
return GoogPromise.all([cursorFinished, cursorTx.wait()])
.then((results) => db);
};
// Setup and execute test case.
return globalDb.branch()
.addCallback(addStoreWithIndex)
.addCallback(addData)
.addCallback(openCursorAndRemove)
.addCallback(checkStore);
},
testCanWaitForTransactionToComplete() {
if (!idbSupported) {
return;
}
return globalDb.branch().addCallback(addStore).addCallback((db) => {
const tx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
tx.objectStore('store').add({key: 'hi', value: 'something'}, 'stuff');
return tx.wait();
});
},
testWaitingOnTransactionThatHasAnError() {
if (!idbSupported) {
return;
}
return globalDb.branch()
.addCallback(
(db) => incrementVersion(
db,
(ev, db, tx) => {
const store = db.createObjectStore('store', {keyPath: 'key'});
store.createIndex('index', 'value', {unique: true});
}))
.addCallback((db) => {
const tx =
db.createTransaction(['store'], 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'});
return transactionToPromise(db, tx).then(
() => {
fail('expected transaction to fail');
},
(ev) => {
// expected
assertEquals(
DbError.ErrorName.CONSTRAINT_ERR, ev.target.getName());
});
});
},
testWaitingOnAnAbortedTransaction() {
if (!idbSupported) {
return;
}
return globalDb.addCallback(addStore).addCallback((db) => {
const tx = db.createTransaction(['store'], TransactionMode.READ_WRITE);
const waiting = tx.wait().then(
() => {
fail('Wait result should have failed');
},
(e) => {
assertEquals(DbError.ErrorName.ABORT_ERR, e.getName());
});
tx.abort();
return waiting;
});
},
});