/* global after:true, afterEach:true, before:true, beforeEach:true, describe:true, expect:true, it:true, Promise:true */ var DRIVERS = [ localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE ]; var SUPPORTED_DRIVERS = DRIVERS.filter(function(driverName) { return localforage.supports(driverName); }); var driverApiMethods = [ 'getItem', 'setItem', 'clear', 'length', 'removeItem', 'key', 'keys' ]; var indexedDB = // eslint-disable-next-line no-use-before-define indexedDB || window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB; describe('localForage API', function() { // https://github.com/mozilla/localForage#working-on-localforage it('has Promises available', function() { expect(Promise).to.be.a('function'); }); }); describe('localForage', function() { var appropriateDriver = (localforage.supports(localforage.INDEXEDDB) && localforage.INDEXEDDB) || (localforage.supports(localforage.WEBSQL) && localforage.WEBSQL) || (localforage.supports(localforage.LOCALSTORAGE) && localforage.LOCALSTORAGE); it( 'automatically selects the most appropriate driver (' + appropriateDriver + ')', function(done) { this.timeout(10000); localforage.ready().then( function() { expect(localforage.driver()).to.be(appropriateDriver); done(); }, function(error) { expect(error).to.be.an(Error); expect(error.message).to.be( 'No available storage method found.' ); expect(localforage.driver()).to.be(null); done(); } ); } ); it('errors when a requested driver is not found [callback]', function(done) { localforage.getDriver('UnknownDriver', null, function(error) { expect(error).to.be.an(Error); expect(error.message).to.be('Driver not found.'); done(); }); }); it('errors when a requested driver is not found [promise]', function(done) { localforage.getDriver('UnknownDriver').then(null, function(error) { expect(error).to.be.an(Error); expect(error.message).to.be('Driver not found.'); done(); }); }); it('retrieves the serializer [callback]', function(done) { localforage.getSerializer(function(serializer) { expect(serializer).to.be.an('object'); done(); }); }); it('retrieves the serializer [promise]', function(done) { var serializerPromise = localforage.getSerializer(); expect(serializerPromise).to.be.an('object'); expect(serializerPromise.then).to.be.a('function'); serializerPromise.then(function(serializer) { expect(serializer).to.be.an('object'); done(); }); }); it('does not support object parameter to setDriver', function(done) { var driverPreferedOrder = { '0': localforage.INDEXEDDB, '1': localforage.WEBSQL, '2': localforage.LOCALSTORAGE, length: 3 }; localforage.setDriver(driverPreferedOrder).then(null, function(error) { expect(error).to.be.an(Error); expect(error.message).to.be('No available storage method found.'); done(); }); }); it('skips drivers that fail to initilize', function(done) { var failingStorageDriver = (function() { function driverDummyMethod() { return Promise.reject(new Error('Driver Method Failed.')); } return { _driver: 'failingStorageDriver', _initStorage: function _initStorage() { return Promise.reject( new Error('Driver Failed to Initialize.') ); }, iterate: driverDummyMethod, getItem: driverDummyMethod, setItem: driverDummyMethod, removeItem: driverDummyMethod, clear: driverDummyMethod, length: driverDummyMethod, key: driverDummyMethod, keys: driverDummyMethod }; })(); var driverPreferedOrder = [ failingStorageDriver._driver, localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE ]; localforage .defineDriver(failingStorageDriver) .then(function() { return localforage.setDriver(driverPreferedOrder); }) .then(function() { return localforage.ready(); }) .then(function() { expect(localforage.driver()).to.be(appropriateDriver); done(); }); }); describe('createInstance()', function() { var oldConsoleInfo; before(function() { oldConsoleInfo = console.info; var logs = []; console.info = function() { console.info.logs.push({ args: arguments }); oldConsoleInfo.apply(this, arguments); }; console.info.logs = logs; }); after(function() { console.info = oldConsoleInfo; }); it('does not log unnecessary messages', function() { var oldLogCount = console.info.logs.length; var localforage2 = localforage.createInstance(); var localforage3 = localforage.createInstance(); return Promise.all([ localforage.ready(), localforage2.ready(), localforage3.ready() ]).then(function() { expect(console.info.logs.length).to.be(oldLogCount); }); }); }); }); SUPPORTED_DRIVERS.forEach(function(driverName) { describe(driverName + ' driver', function() { 'use strict'; this.timeout(30000); before(function(done) { localforage.setDriver(driverName).then(done); }); beforeEach(function(done) { localStorage.clear(); localforage.ready().then(function() { localforage.clear(done); }); }); it('has a localStorage API', function() { expect(localforage.getItem).to.be.a('function'); expect(localforage.setItem).to.be.a('function'); expect(localforage.clear).to.be.a('function'); expect(localforage.length).to.be.a('function'); expect(localforage.removeItem).to.be.a('function'); expect(localforage.key).to.be.a('function'); }); it('has the localForage API', function() { expect(localforage._initStorage).to.be.a('function'); expect(localforage.config).to.be.a('function'); expect(localforage.defineDriver).to.be.a('function'); expect(localforage.driver).to.be.a('function'); expect(localforage.supports).to.be.a('function'); expect(localforage.iterate).to.be.a('function'); expect(localforage.getItem).to.be.a('function'); expect(localforage.setItem).to.be.a('function'); expect(localforage.clear).to.be.a('function'); expect(localforage.length).to.be.a('function'); expect(localforage.removeItem).to.be.a('function'); expect(localforage.key).to.be.a('function'); expect(localforage.getDriver).to.be.a('function'); expect(localforage.setDriver).to.be.a('function'); expect(localforage.ready).to.be.a('function'); expect(localforage.createInstance).to.be.a('function'); expect(localforage.getSerializer).to.be.a('function'); expect(localforage.dropInstance).to.be.a('function'); }); // Make sure we don't support bogus drivers. it('supports ' + driverName + ' database driver', function() { expect(localforage.supports(driverName) === true); expect(localforage.supports('I am not a driver') === false); }); it('sets the right database driver', function() { expect(localforage.driver() === driverName); }); it('has an empty length by default', function(done) { localforage.length(function(err, length) { expect(length).to.be(0); done(); }); }); if (driverName === localforage.INDEXEDDB) { describe('Blob support', function() { var transaction; var called; var db; var blob = new Blob([''], { type: 'image/png' }); before(function() { db = localforage._dbInfo.db; transaction = db.transaction; db.transaction = function() { called += 1; return transaction.apply(db, arguments); }; }); beforeEach(function() { called = 0; }); it('not check for non Blob', function(done) { localforage.setItem('key', {}).then( function() { expect(called).to.be(1); done(); }, function(error) { done(error || 'error'); } ); }); it('check for Blob', function(done) { localforage.setItem('key', blob).then( function() { expect(called).to.be.above(1); done(); }, function(error) { done(error || 'error'); } ); }); it('check for Blob once', function(done) { localforage.setItem('key', blob).then( function() { expect(called).to.be(1); done(); }, function(error) { done(error || 'error'); } ); }); after(function() { localforage._dbInfo.db.transaction = transaction; }); }); describe('recover (reconnect) from IDBDatabase InvalidStateError', function() { beforeEach(function(done) { Promise.all([ localforage.setItem('key', 'value1'), localforage.setItem('key1', 'value1'), localforage.setItem('key2', 'value2'), localforage.setItem('key3', 'value3') ]).then( function() { localforage._dbInfo.db.close(); done(); }, function(error) { done(error || 'error'); } ); }); it('retrieves an item from the storage', function(done) { localforage.getItem('key').then( function(value) { expect(value).to.be('value1'); done(); }, function(error) { done(error || 'error'); } ); }); it('retrieves more than one items from the storage', function(done) { Promise.all([ localforage.getItem('key1'), localforage.getItem('key2'), localforage.getItem('key3') ]).then( function(values) { expect(values).to.eql([ 'value1', 'value2', 'value3' ]); done(); }, function(error) { done(error || 'error'); } ); }); it('stores and retrieves an item from the storage', function(done) { localforage .setItem('key', 'value1b') .then(function() { return localforage.getItem('key'); }) .then( function(value) { expect(value).to.be('value1b'); done(); }, function(error) { done(error || 'error'); } ); }); it('stores and retrieves more than one items from the storage', function(done) { Promise.all([ localforage.setItem('key1', 'value1b'), localforage.setItem('key2', 'value2b'), localforage.setItem('key3', 'value3b') ]) .then(function() { return Promise.all([ localforage.getItem('key1'), localforage.getItem('key2'), localforage.getItem('key3') ]); }) .then( function(values) { expect(values).to.eql([ 'value1b', 'value2b', 'value3b' ]); done(); }, function(error) { done(error || 'error'); } ); }); }); } if (driverName === localforage.WEBSQL) { describe('on QUOTA ERROR', function() { var transaction; var called; var db; function getQuotaErrorCode(transaction) { return new Promise(function(resolve) { transaction( function(t) { t.executeSql(''); }, function(err) { resolve(err.QUOTA_ERR); } ); }).catch(function(err) { return err.QUOTA_ERR; }); } beforeEach(function() { called = 0; db = localforage._dbInfo.db; transaction = db.transaction; db.transaction = function(fn, errFn) { called += 1; // restore the normal transaction, // so that subsequent operations work db.transaction = transaction; getQuotaErrorCode(transaction).then(function( QUOTA_ERR ) { var error = new Error(); error.code = QUOTA_ERR; errFn(error); }); }; }); it('should retry setItem', function(done) { localforage.setItem('key', {}).then( function() { expect(called).to.be(1); done(); }, function(error) { done(error || 'error'); } ); }); after(function() { db.transaction = transaction || db.transaction; }); }); } it('should iterate [callback]', function(done) { localforage.setItem('officeX', 'InitechX', function(err, setValue) { expect(setValue).to.be('InitechX'); localforage.getItem('officeX', function(err, value) { expect(value).to.be(setValue); localforage.setItem('officeY', 'InitechY', function( err, setValue ) { expect(setValue).to.be('InitechY'); localforage.getItem('officeY', function(err, value) { expect(value).to.be(setValue); var accumulator = {}; var iterationNumbers = []; localforage.iterate( function(value, key, iterationNumber) { accumulator[key] = value; iterationNumbers.push(iterationNumber); }, function() { try { expect(accumulator.officeX).to.be( 'InitechX' ); expect(accumulator.officeY).to.be( 'InitechY' ); expect(iterationNumbers).to.eql([1, 2]); done(); } catch (e) { done(e); } } ); }); }); }); }); }); it('should iterate [promise]', function(done) { var accumulator = {}; var iterationNumbers = []; return localforage .setItem('officeX', 'InitechX') .then(function(setValue) { expect(setValue).to.be('InitechX'); return localforage.getItem('officeX'); }) .then(function(value) { expect(value).to.be('InitechX'); return localforage.setItem('officeY', 'InitechY'); }) .then(function(setValue) { expect(setValue).to.be('InitechY'); return localforage.getItem('officeY'); }) .then(function(value) { expect(value).to.be('InitechY'); return localforage.iterate(function( value, key, iterationNumber ) { accumulator[key] = value; iterationNumbers.push(iterationNumber); }); }) .then(function() { expect(accumulator.officeX).to.be('InitechX'); expect(accumulator.officeY).to.be('InitechY'); expect(iterationNumbers).to.eql([1, 2]); done(); }); }); it('should break iteration with defined return value [callback]', function(done) { var breakCondition = 'Some value!'; localforage.setItem('officeX', 'InitechX', function(err, setValue) { expect(setValue).to.be('InitechX'); localforage.getItem('officeX', function(err, value) { expect(value).to.be(setValue); localforage.setItem('officeY', 'InitechY', function( err, setValue ) { expect(setValue).to.be('InitechY'); localforage.getItem('officeY', function(err, value) { expect(value).to.be(setValue); // Loop is broken within first iteration. localforage.iterate( function() { // Returning defined value will break the cycle. return breakCondition; }, function(err, loopResult) { // The value that broken the cycle is returned // as a result. expect(loopResult).to.be(breakCondition); done(); } ); }); }); }); }); }); it('should break iteration with defined return value [promise]', function(done) { var breakCondition = 'Some value!'; localforage .setItem('officeX', 'InitechX') .then(function(setValue) { expect(setValue).to.be('InitechX'); return localforage.getItem('officeX'); }) .then(function(value) { expect(value).to.be('InitechX'); return localforage.setItem('officeY', 'InitechY'); }) .then(function(setValue) { expect(setValue).to.be('InitechY'); return localforage.getItem('officeY'); }) .then(function(value) { expect(value).to.be('InitechY'); return localforage.iterate(function() { return breakCondition; }); }) .then(function(result) { expect(result).to.be(breakCondition); done(); }); }); it('should iterate() through only its own keys/values', function(done) { localStorage.setItem('local', 'forage'); localforage .setItem('office', 'Initech') .then(function() { return localforage.setItem('name', 'Bob'); }) .then(function() { // Loop through all key/value pairs; {local: 'forage'} set // manually should not be returned. var numberOfItems = 0; var iterationNumberConcat = ''; localStorage.setItem('locals', 'forages'); localforage.iterate( function(value, key, iterationNumber) { expect(key).to.not.be('local'); expect(value).to.not.be('forage'); numberOfItems++; iterationNumberConcat += iterationNumber; }, function(err) { if (!err) { // While there are 4 items in localStorage, // only 2 items were set using localForage. expect(numberOfItems).to.be(2); // Only 2 items were set using localForage, // so we should get '12' and not '1234' expect(iterationNumberConcat).to.be('12'); done(); } } ); }); }); // Test for https://github.com/mozilla/localForage/issues/175 it('nested getItem inside clear works [callback]', function(done) { localforage.setItem('hello', 'Hello World !', function() { localforage.clear(function() { localforage.getItem('hello', function(secondValue) { expect(secondValue).to.be(null); done(); }); }); }); }); it('nested getItem inside clear works [promise]', function(done) { localforage .setItem('hello', 'Hello World !') .then(function() { return localforage.clear(); }) .then(function() { return localforage.getItem('hello'); }) .then(function(secondValue) { expect(secondValue).to.be(null); done(); }); }); // Because localStorage doesn't support saving the `undefined` type, we // always return `null` so that localForage is consistent across // browsers. // https://github.com/mozilla/localForage/pull/42 it('returns null for undefined key [callback]', function(done) { localforage.getItem('key', function(err, value) { expect(value).to.be(null); done(); }); }); it('returns null for undefined key [promise]', function(done) { localforage.getItem('key').then(function(value) { expect(value).to.be(null); done(); }); }); it('saves an item [callback]', function(done) { localforage.setItem('office', 'Initech', function(err, setValue) { expect(setValue).to.be('Initech'); localforage.getItem('office', function(err, value) { expect(value).to.be(setValue); done(); }); }); }); it('saves an item [promise]', function(done) { localforage .setItem('office', 'Initech') .then(function(setValue) { expect(setValue).to.be('Initech'); return localforage.getItem('office'); }) .then(function(value) { expect(value).to.be('Initech'); done(); }); }); it('saves an item over an existing key [callback]', function(done) { localforage.setItem('4th floor', 'Mozilla', function( err, setValue ) { expect(setValue).to.be('Mozilla'); localforage.setItem('4th floor', 'Quora', function( err, newValue ) { expect(newValue).to.not.be(setValue); expect(newValue).to.be('Quora'); localforage.getItem('4th floor', function(err, value) { expect(value).to.not.be(setValue); expect(value).to.be(newValue); done(); }); }); }); }); it('saves an item over an existing key [promise]', function(done) { localforage .setItem('4e', 'Mozilla') .then(function(setValue) { expect(setValue).to.be('Mozilla'); return localforage.setItem('4e', 'Quora'); }) .then(function(newValue) { expect(newValue).to.not.be('Mozilla'); expect(newValue).to.be('Quora'); return localforage.getItem('4e'); }) .then(function(value) { expect(value).to.not.be('Mozilla'); expect(value).to.be('Quora'); done(); }); }); it('returns null when saving undefined [callback]', function(done) { localforage.setItem('undef', undefined, function(err, setValue) { expect(setValue).to.be(null); done(); }); }); it('returns null when saving undefined [promise]', function(done) { localforage.setItem('undef', undefined).then(function(setValue) { expect(setValue).to.be(null); done(); }); }); it('returns null when saving null [callback]', function(done) { localforage.setItem('null', null, function(err, setValue) { expect(setValue).to.be(null); done(); }); }); it('returns null when saving null [promise]', function(done) { localforage.setItem('null', null).then(function(setValue) { expect(setValue).to.be(null); done(); }); }); it('returns null for a non-existant key [callback]', function(done) { localforage.getItem('undef', function(err, value) { expect(value).to.be(null); done(); }); }); it('returns null for a non-existant key [promise]', function(done) { localforage.getItem('undef').then(function(value) { expect(value).to.be(null); done(); }); }); // github.com/mozilla/localforage/pull/24#discussion-diff-9389662R158 // localStorage's method API (`localStorage.getItem('foo')`) returns // `null` for undefined keys, even though its getter/setter API // (`localStorage.foo`) returns `undefined` for the same key. Gaia's // asyncStorage API, which is based on localStorage and upon which // localforage is based, ALSO returns `null`. BLARG! So for now, we // just return null, because there's no way to know from localStorage // if the key is ACTUALLY `null` or undefined but returning `null`. // And returning `undefined` here would break compatibility with // localStorage fallback. Maybe in the future we won't care... it('returns null from an undefined key [callback]', function(done) { localforage.key(0, function(err, key) { expect(key).to.be(null); done(); }); }); it('returns null from an undefined key [promise]', function(done) { localforage.key(0).then(function(key) { expect(key).to.be(null); done(); }); }); it('returns key name [callback]', function(done) { localforage.setItem('office', 'Initech').then(function() { localforage.key(0, function(err, key) { expect(key).to.be('office'); done(); }); }); }); it('returns key name [promise]', function(done) { localforage .setItem('office', 'Initech') .then(function() { return localforage.key(0); }) .then(function(key) { expect(key).to.be('office'); done(); }); }); it('removes an item [callback]', function(done) { localforage.setItem('office', 'Initech', function() { localforage.setItem('otherOffice', 'Initrode', function() { localforage.removeItem('office', function() { localforage.getItem('office', function( err, emptyValue ) { expect(emptyValue).to.be(null); localforage.getItem('otherOffice', function( err, value ) { expect(value).to.be('Initrode'); done(); }); }); }); }); }); }); it('removes an item [promise]', function(done) { localforage .setItem('office', 'Initech') .then(function() { return localforage.setItem('otherOffice', 'Initrode'); }) .then(function() { return localforage.removeItem('office'); }) .then(function() { return localforage.getItem('office'); }) .then(function(emptyValue) { expect(emptyValue).to.be(null); return localforage.getItem('otherOffice'); }) .then(function(value) { expect(value).to.be('Initrode'); done(); }); }); it('removes all items [callback]', function(done) { localforage.setItem('office', 'Initech', function() { localforage.setItem('otherOffice', 'Initrode', function() { localforage.length(function(err, length) { expect(length).to.be(2); localforage.clear(function() { localforage.getItem('office', function(err, value) { expect(value).to.be(null); localforage.length(function(err, length) { expect(length).to.be(0); done(); }); }); }); }); }); }); }); it('removes all items [promise]', function(done) { localforage .setItem('office', 'Initech') .then(function() { return localforage.setItem('otherOffice', 'Initrode'); }) .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(2); return localforage.clear(); }) .then(function() { return localforage.getItem('office'); }) .then(function(value) { expect(value).to.be(null); return localforage.length(); }) .then(function(length) { expect(length).to.be(0); done(); }); }); if (driverName === localforage.LOCALSTORAGE) { it('removes only own items upon clear', function(done) { localStorage.setItem('local', 'forage'); localforage .setItem('office', 'Initech') .then(function() { return localforage.clear(); }) .then(function() { expect(localStorage.getItem('local')).to.be('forage'); localStorage.clear(); done(); }); }); it('returns only its own keys from keys()', function(done) { localStorage.setItem('local', 'forage'); localforage .setItem('office', 'Initech') .then(function() { return localforage.keys(); }) .then(function(keys) { expect(keys).to.eql(['office']); localStorage.clear(); done(); }); }); it('counts only its own items with length()', function(done) { localStorage.setItem('local', 'forage'); localStorage.setItem('another', 'value'); localforage .setItem('office', 'Initech') .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(1); localStorage.clear(); done(); }); }); } it('has a length after saving an item [callback]', function(done) { localforage.length(function(err, length) { expect(length).to.be(0); localforage.setItem('rapper', 'Black Thought', function() { localforage.length(function(err, length) { expect(length).to.be(1); done(); }); }); }); }); it('has a length after saving an item [promise]', function(done) { localforage .length() .then(function(length) { expect(length).to.be(0); return localforage.setItem('lame rapper', 'Vanilla Ice'); }) .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(1); done(); }); }); // Deal with non-string keys, see issue #250 // https://github.com/mozilla/localForage/issues/250 it('casts an undefined key to a String', function(done) { localforage .setItem(undefined, 'goodness!') .then(function(value) { expect(value).to.be('goodness!'); return localforage.getItem(undefined); }) .then(function(value) { expect(value).to.be('goodness!'); return localforage.removeItem(undefined); }) .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(0); done(); }); }); it('casts a null key to a String', function(done) { localforage .setItem(null, 'goodness!') .then(function(value) { expect(value).to.be('goodness!'); return localforage.getItem(null); }) .then(function(value) { expect(value).to.be('goodness!'); return localforage.removeItem(null); }) .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(0); done(); }); }); it('casts a float key to a String', function(done) { localforage .setItem(537.35737, 'goodness!') .then(function(value) { expect(value).to.be('goodness!'); return localforage.getItem(537.35737); }) .then(function(value) { expect(value).to.be('goodness!'); return localforage.removeItem(537.35737); }) .then(function() { return localforage.length(); }) .then(function(length) { expect(length).to.be(0); done(); }); }); it('is retrieved by getDriver [callback]', function(done) { localforage.getDriver(driverName, function(driver) { expect(typeof driver).to.be('object'); driverApiMethods .concat('_initStorage') .forEach(function(methodName) { expect(typeof driver[methodName]).to.be('function'); }); expect(driver._driver).to.be(driverName); done(); }); }); it('is retrieved by getDriver [promise]', function(done) { localforage.getDriver(driverName).then(function(driver) { expect(typeof driver).to.be('object'); driverApiMethods .concat('_initStorage') .forEach(function(methodName) { expect(typeof driver[methodName]).to.be('function'); }); expect(driver._driver).to.be(driverName); done(); }); }); if ( driverName === localforage.WEBSQL || driverName === localforage.LOCALSTORAGE ) { it('exposes the serializer on the dbInfo object', function(done) { localforage.ready().then(function() { expect(localforage._dbInfo.serializer).to.be.an('object'); done(); }); }); } }); function prepareStorage(storageName) { // Delete IndexedDB storages (start from scratch) // Refers to issue #492 - https://github.com/mozilla/localForage/issues/492 if (driverName === localforage.INDEXEDDB) { return new Promise(function(resolve) { indexedDB.deleteDatabase(storageName).onsuccess = resolve; }); } // Otherwise, do nothing return Promise.resolve(); } describe(driverName + ' driver multiple instances', function() { 'use strict'; this.timeout(30000); var localforage2 = null; var localforage3 = null; before(function(done) { prepareStorage('storage2').then(function() { localforage2 = localforage.createInstance({ name: 'storage2', // We need a small value here // otherwise local PhantomJS test // will fail with SECURITY_ERR. // TravisCI seem to work fine though. size: 1024, storeName: 'storagename2' }); // Same name, but different storeName since this has been // malfunctioning before w/ IndexedDB. localforage3 = localforage.createInstance({ name: 'storage2', // We need a small value here // otherwise local PhantomJS test // will fail with SECURITY_ERR. // TravisCI seem to work fine though. size: 1024, storeName: 'storagename3' }); Promise.all([ localforage.setDriver(driverName), localforage2.setDriver(driverName), localforage3.setDriver(driverName) ]).then(function() { done(); }); }); }); beforeEach(function(done) { Promise.all([ localforage.clear(), localforage2.clear(), localforage3.clear() ]).then(function() { done(); }); }); it('is not be able to access values of other instances', function(done) { Promise.all([ localforage.setItem('key1', 'value1a'), localforage2.setItem('key2', 'value2a'), localforage3.setItem('key3', 'value3a') ]) .then(function() { return Promise.all([ localforage.getItem('key2').then(function(value) { expect(value).to.be(null); }), localforage2.getItem('key1').then(function(value) { expect(value).to.be(null); }), localforage2.getItem('key3').then(function(value) { expect(value).to.be(null); }), localforage3.getItem('key2').then(function(value) { expect(value).to.be(null); }) ]); }) .then( function() { done(); }, function(errors) { done(new Error(errors)); } ); }); it('retrieves the proper value when using the same key with other instances', function(done) { Promise.all([ localforage.setItem('key', 'value1'), localforage2.setItem('key', 'value2'), localforage3.setItem('key', 'value3') ]) .then(function() { return Promise.all([ localforage.getItem('key').then(function(value) { expect(value).to.be('value1'); }), localforage2.getItem('key').then(function(value) { expect(value).to.be('value2'); }), localforage3.getItem('key').then(function(value) { expect(value).to.be('value3'); }) ]); }) .then( function() { done(); }, function(errors) { done(new Error(errors)); } ); }); }); // Refers to issue #492 - https://github.com/mozilla/localForage/issues/492 describe( driverName + ' driver multiple instances (concurrent on same database)', function() { 'use strict'; this.timeout(30000); before(function() { return Promise.all([ prepareStorage('storage3'), prepareStorage('commonStorage'), prepareStorage('commonStorage2'), prepareStorage('commonStorage3') ]); }); it('chains operation on multiple stores', function() { var localforage1 = localforage.createInstance({ name: 'storage3', storeName: 'store1', size: 1024 }); var localforage2 = localforage.createInstance({ name: 'storage3', storeName: 'store2', size: 1024 }); var localforage3 = localforage.createInstance({ name: 'storage3', storeName: 'store3', size: 1024 }); var promise1 = localforage1 .setItem('key', 'value1') .then(function() { return localforage1.getItem('key'); }) .then(function(value) { expect(value).to.be('value1'); }); var promise2 = localforage2 .setItem('key', 'value2') .then(function() { return localforage2.getItem('key'); }) .then(function(value) { expect(value).to.be('value2'); }); var promise3 = localforage3 .setItem('key', 'value3') .then(function() { return localforage3.getItem('key'); }) .then(function(value) { expect(value).to.be('value3'); }); return Promise.all([promise1, promise2, promise3]); }); it('can create multiple instances of the same store', function() { var localforage1; var localforage2; var localforage3; Promise.resolve() .then(function() { localforage1 = localforage.createInstance({ name: 'commonStorage', storeName: 'commonStore', size: 1024 }); return localforage1.ready(); }) .then(function() { localforage2 = localforage.createInstance({ name: 'commonStorage', storeName: 'commonStore', size: 1024 }); return localforage2.ready(); }) .then(function() { localforage3 = localforage.createInstance({ name: 'commonStorage', storeName: 'commonStore', size: 1024 }); return localforage3.ready(); }) .then(function() { return Promise.resolve() .then(function() { return localforage1 .setItem('key1', 'value1') .then(function() { return localforage1.getItem('key1'); }) .then(function(value) { expect(value).to.be('value1'); }); }) .then(function() { return localforage2 .setItem('key2', 'value2') .then(function() { return localforage2.getItem('key2'); }) .then(function(value) { expect(value).to.be('value2'); }); }) .then(function() { return localforage3 .setItem('key3', 'value3') .then(function() { return localforage3.getItem('key3'); }) .then(function(value) { expect(value).to.be('value3'); }); }); }); }); it('can create multiple instances of the same store and do concurrent operations', function() { var localforage1; var localforage2; var localforage3; var localforage3b; Promise.resolve() .then(function() { localforage1 = localforage.createInstance({ name: 'commonStorage2', storeName: 'commonStore', size: 1024 }); return localforage1.ready(); }) .then(function() { localforage2 = localforage.createInstance({ name: 'commonStorage2', storeName: 'commonStore', size: 1024 }); return localforage2.ready(); }) .then(function() { localforage3 = localforage.createInstance({ name: 'commonStorage2', storeName: 'commonStore', size: 1024 }); return localforage3.ready(); }) .then(function() { localforage3b = localforage.createInstance({ name: 'commonStorage2', storeName: 'commonStore', size: 1024 }); return localforage3b.ready(); }) .then(function() { var promise1 = localforage1 .setItem('key1', 'value1') .then(function() { return localforage1.getItem('key1'); }) .then(function(value) { expect(value).to.be('value1'); }); var promise2 = localforage2 .setItem('key2', 'value2') .then(function() { return localforage2.getItem('key2'); }) .then(function(value) { expect(value).to.be('value2'); }); var promise3 = localforage3 .setItem('key3', 'value3') .then(function() { return localforage3.getItem('key3'); }) .then(function(value) { expect(value).to.be('value3'); }); var promise4 = localforage3b .setItem('key3', 'value3') .then(function() { return localforage3.getItem('key3'); }) .then(function(value) { expect(value).to.be('value3'); }); return Promise.all([ promise1, promise2, promise3, promise4 ]); }); }); it('can create multiple instances of the same store concurrently', function() { var localforage1 = localforage.createInstance({ name: 'commonStorage3', storeName: 'commonStore', size: 1024 }); var localforage2 = localforage.createInstance({ name: 'commonStorage3', storeName: 'commonStore', size: 1024 }); var localforage3 = localforage.createInstance({ name: 'commonStorage3', storeName: 'commonStore', size: 1024 }); var localforage3b = localforage.createInstance({ name: 'commonStorage3', storeName: 'commonStore', size: 1024 }); var promise1 = localforage1 .setItem('key1', 'value1') .then(function() { return localforage1.getItem('key1'); }) .then(function(value) { expect(value).to.be('value1'); }); var promise2 = localforage2 .setItem('key2', 'value2') .then(function() { return localforage2.getItem('key2'); }) .then(function(value) { expect(value).to.be('value2'); }); var promise3 = localforage3 .setItem('key3', 'value3') .then(function() { return localforage3.getItem('key3'); }) .then(function(value) { expect(value).to.be('value3'); }); var promise4 = localforage3b .setItem('key3', 'value3') .then(function() { return localforage3.getItem('key3'); }) .then(function(value) { expect(value).to.be('value3'); }); return Promise.all([promise1, promise2, promise3, promise4]); }); } ); describe(driverName + ' driver', function() { 'use strict'; var driverPreferedOrder; before(function() { // add some unsupported drivers before // and after the target driver driverPreferedOrder = ['I am a not supported driver']; if (!localforage.supports(localforage.WEBSQL)) { driverPreferedOrder.push(localforage.WEBSQL); } if (!localforage.supports(localforage.INDEXEDDB)) { driverPreferedOrder.push(localforage.INDEXEDDB); } if (!localforage.supports(localforage.LOCALSTORAGE)) { driverPreferedOrder.push(localforage.localStorage); } driverPreferedOrder.push(driverName); driverPreferedOrder.push('I am another not supported driver'); }); it('is used according to setDriver preference order', function(done) { localforage.setDriver(driverPreferedOrder).then(function() { expect(localforage.driver()).to.be(driverName); done(); }); }); }); describe( driverName + ' driver when the callback throws an Error', function() { 'use strict'; var testObj = { throwFunc: function() { testObj.throwFuncCalls++; throw new Error('Thrown test error'); }, throwFuncCalls: 0 }; beforeEach(function(done) { testObj.throwFuncCalls = 0; done(); }); it('resolves the promise of getItem()', function(done) { localforage.getItem('key', testObj.throwFunc).then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of setItem()', function(done) { localforage .setItem('key', 'test', testObj.throwFunc) .then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of clear()', function(done) { localforage.clear(testObj.throwFunc).then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of length()', function(done) { localforage.length(testObj.throwFunc).then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of removeItem()', function(done) { localforage .removeItem('key', testObj.throwFunc) .then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of key()', function(done) { localforage.key('key', testObj.throwFunc).then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); it('resolves the promise of keys()', function(done) { localforage.keys(testObj.throwFunc).then(function() { expect(testObj.throwFuncCalls).to.be(1); done(); }); }); } ); describe(driverName + ' driver when ready() gets rejected', function() { 'use strict'; this.timeout(30000); var _oldReady; beforeEach(function(done) { _oldReady = localforage.ready; localforage.ready = function() { return Promise.reject(); }; done(); }); afterEach(function(done) { localforage.ready = _oldReady; _oldReady = null; done(); }); driverApiMethods.forEach(function(methodName) { it('rejects ' + methodName + '() promise', function(done) { localforage[methodName]().then(null, function(/*err*/) { done(); }); }); }); }); }); DRIVERS.forEach(function(driverName) { describe(driverName + ' driver instance', function() { it('creates a new instance and sets the driver', function(done) { var localforage2 = localforage.createInstance({ name: 'storage2', driver: driverName, // We need a small value here // otherwise local PhantomJS test // will fail with SECURITY_ERR. // TravisCI seem to work fine though. size: 1024, storeName: 'storagename2' }); // since config actually uses setDriver which is async, // and since driver() and supports() are not defered (are sync), // we have to wait till an async method returns localforage2.length().then( function() { expect(localforage2.driver()).to.be(driverName); done(); }, function() { expect(localforage2.driver()).to.be(null); done(); } ); }); }); }); SUPPORTED_DRIVERS.forEach(function(driverName) { describe(driverName + ' driver dropInstance', function() { this.timeout(30000); function setCommonOpts(opts) { opts.driver = driverName; opts.size = 1024; return opts; } var dropStoreDbName = 'dropStoreDb'; var nodropInstance; var nodropInstanceOptions = setCommonOpts({ name: dropStoreDbName, storeName: 'nodropStore' }); var dropStoreInstance1; var dropStoreInstance1Options = setCommonOpts({ name: dropStoreDbName, storeName: 'dropStore' }); var dropStoreInstance2; var dropStoreInstance2Options = setCommonOpts({ name: dropStoreDbName, storeName: 'dropStore2' }); var dropStoreInstance3; var dropStoreInstance3Options = setCommonOpts({ name: dropStoreDbName, storeName: 'dropStore3' }); var dropDbInstance; var dropDbInstanceOptions = setCommonOpts({ name: 'dropDb', storeName: 'dropStore' }); var dropDb2Instance; var dropDb2InstanceOptions = setCommonOpts({ name: 'dropDb2', storeName: 'dropStore' }); var dropDb3name = 'dropDb3'; var dropDb3Instance1; var dropDb3Instance1Options = setCommonOpts({ name: dropDb3name, storeName: 'dropStore1' }); var dropDb3Instance2; var dropDb3Instance2Options = setCommonOpts({ name: dropDb3name, storeName: 'dropStore2' }); var dropDb3Instance3; var dropDb3Instance3Options = setCommonOpts({ name: dropDb3name, storeName: 'dropStore3' }); before(function() { nodropInstance = localforage.createInstance(nodropInstanceOptions); dropStoreInstance1 = localforage.createInstance( dropStoreInstance1Options ); dropStoreInstance2 = localforage.createInstance( dropStoreInstance2Options ); dropStoreInstance3 = localforage.createInstance( dropStoreInstance3Options ); dropDbInstance = localforage.createInstance(dropDbInstanceOptions); dropDb2Instance = localforage.createInstance( dropDb2InstanceOptions ); dropDb3Instance1 = localforage.createInstance( dropDb3Instance1Options ); dropDb3Instance2 = localforage.createInstance( dropDb3Instance2Options ); dropDb3Instance3 = localforage.createInstance( dropDb3Instance3Options ); return Promise.resolve() .then(function() { return nodropInstance.setItem('key1', 'value0'); }) .then(function() { return dropStoreInstance1.setItem('key1', 'value1'); }) .then(function() { return dropStoreInstance2.setItem('key1', 'value2'); }) .then(function() { return dropStoreInstance3.setItem('key1', 'value3'); }) .then(function() { return dropDbInstance.setItem('key1', 'value3'); }) .then(function() { return dropDb2Instance.setItem('key1', 'value3'); }) .then(function() { return dropDb3Instance1.setItem('key1', 'value1'); }) .then(function() { return dropDb3Instance2.setItem('key1', 'value2'); }) .then(function() { return dropDb3Instance3.setItem('key1', 'value3'); }); }); function expectStoreToNotExistAsync(options) { return new Promise(function(resolve, reject) { if (driverName === localforage.INDEXEDDB) { var req = indexedDB.open(options.name); req.onsuccess = function() { var db = req.result; if (!db) { reject(); return; } expect( db.objectStoreNames.contains(options.storeName) ).to.be(false); db.close(); resolve(); }; req.onerror = req.onblocked = reject; } else if (driverName === localforage.WEBSQL) { var db = openDatabase(options.name, '', '', 0); db.transaction(function(t) { t.executeSql( "SELECT name FROM sqlite_master WHERE type='table' AND name = ?", [options.storeName], function(t, results) { expect(results.rows.length).to.be(0); resolve(); }, reject ); }, reject); } else if (driverName === localforage.LOCALSTORAGE) { var keyPrefix = (function _getKeyPrefix( options, defaultConfig ) { var keyPrefix = options.name + '/'; if (options.storeName !== defaultConfig.storeName) { keyPrefix += options.storeName + '/'; } return keyPrefix; })(options, { name: 'localforage', storeName: 'keyvaluepairs' }); var foundLocalStorageKey = false; for ( var i = 0, length = localStorage.length; i < length; i++ ) { if (localStorage.key(i).indexOf(keyPrefix) === 0) { foundLocalStorageKey = true; break; } } expect(foundLocalStorageKey).to.be(false); resolve(); } else { throw new Error('Not Implemented Exception'); } }); } it('drops the current instance without affecting the rest', function() { return dropStoreInstance1 .dropInstance() .then(function() { return nodropInstance.getItem('key1'); }) .then(function(value) { expect(value).to.be('value0'); }); }); it('can recreate and set values to previously dropped instances', function() { return dropStoreInstance1 .dropInstance() .then(function() { return dropStoreInstance1.getItem('key1'); }) .then(function(value) { expect(value).to.be(null); return dropStoreInstance1.length(); }) .then(function(length) { expect(length).to.be(0); }) .then(function() { return dropStoreInstance1.setItem('key1', 'newvalue2'); }) .then(function() { return dropStoreInstance1.getItem('key1'); }) .then(function(value) { expect(value).to.be('newvalue2'); }); }); it('drops an other instance without affecting the rest', function() { var opts = { name: dropStoreInstance2Options.name, storeName: dropStoreInstance2Options.storeName }; return nodropInstance .dropInstance(opts) .then(function() { return nodropInstance.getItem('key1'); }) .then(function(value) { expect(value).to.be('value0'); }); }); it('the dropped instance is completely removed', function() { var opts = { name: dropStoreInstance3Options.name, storeName: dropStoreInstance3Options.storeName }; return dropStoreInstance3.dropInstance().then(function() { return expectStoreToNotExistAsync(opts); }); }); it('resolves when trying to drop a store that does not exit', function() { var opts = { name: dropStoreInstance3Options.name, storeName: 'NotExistingStore' + Date.now() }; return dropStoreInstance3.dropInstance(opts); }); function expectDBToNotExistAsync(options) { return new Promise(function(resolve, reject) { if (driverName === localforage.INDEXEDDB) { var req = indexedDB.open(options.name); req.onsuccess = function() { var db = req.result; if (!db) { reject(); return; } expect(db.objectStoreNames.length).to.be(0); db.close(); resolve(); }; req.onerror = req.onblocked = reject; } else if (driverName === localforage.WEBSQL) { var db = openDatabase(options.name, '', '', 0); db.transaction(function(t) { t.executeSql( "SELECT name FROM sqlite_master WHERE type='table'", [], function(t, results) { var stores = Array.prototype.filter.call( results.rows, function(obj) { return ( obj && obj.name && obj.name.indexOf('__') !== 0 ); } ); expect(stores.length).to.be(0); resolve(); }, reject ); }, reject); } else if (driverName === localforage.LOCALSTORAGE) { var keyPrefix = (function _getKeyPrefix(options) { return options.name + '/'; })(options); var foundLocalStorageKey = false; for ( var i = 0, length = localStorage.length; i < length; i++ ) { if (localStorage.key(i).indexOf(keyPrefix) === 0) { foundLocalStorageKey = true; break; } } expect(foundLocalStorageKey).to.be(false); resolve(); } else { throw new Error('Not Implemented Exception'); } }); } it('the dropped "DB" can be recreated', function() { var opts = { name: dropDbInstanceOptions.name }; return dropDbInstance .dropInstance(opts) .then(function() { return dropDbInstance.getItem('key1'); }) .then(function(value) { expect(value).to.be(null); }); }); it('the dropped "DB" is completely removed', function() { var opts = { name: dropDb2InstanceOptions.name }; return dropDb2Instance.dropInstance(opts).then(function() { return expectDBToNotExistAsync(opts); }); }); it('resolves when trying to drop a store of a "DB" that does not exit', function() { var opts = { name: 'NotExistingDB' + Date.now(), storeName: 'NotExistingStore' + Date.now() }; return dropStoreInstance3.dropInstance(opts); }); it('resolves when trying to drop a "DB" that does not exist', function() { var opts = { name: 'NotExistingDB' + Date.now() }; return dropStoreInstance3.dropInstance(opts); }); it('drops a "DB" that we previously dropped a store', function() { var opts = { name: dropStoreInstance3Options.name }; return dropStoreInstance3.dropInstance(opts).then(function() { return expectDBToNotExistAsync(opts); }); }); it('drops a "DB" after dropping all its stores', function() { var opts = { name: dropDb3name }; // Before trying to drop a different store/DB // make sure that the instance that you will use // is configured to use the same driver as well. return Promise.resolve() .then(function() { return dropDb3Instance1.dropInstance({ name: dropDb3name, storeName: dropDb3Instance1Options.storeName }); }) .then(function() { return dropDb3Instance1.dropInstance({ name: dropDb3name, storeName: dropDb3Instance2Options.storeName }); }) .then(function() { return dropDb3Instance1.dropInstance({ name: dropDb3name, storeName: dropDb3Instance3Options.storeName }); }) .then(function() { return dropDb3Instance1.dropInstance(opts); }) .then(function() { return expectDBToNotExistAsync(opts); }); }); }); });