/* global MockIndexedDB, MockNavigatorDatastore, mockLocalStorage, Datastore */ 'use strict'; require('/shared/test/unit/mocks/mock_indexedDB.js'); require('/shared/test/unit/mocks/mock_navigator_datastore.js'); require('mocks/mock_localStorage.js'); require('/js/datastore.js'); suite('Datastore', () => { var mockIndexedDB; var realNavigatorDatastores; var realLocalStorage; var datastoreName = 'datastore_name_example'; var bookmarks; setup(() => { mockIndexedDB = new MockIndexedDB(); mockLocalStorage.mSetup(); Object.defineProperty(window, 'localStorage', { configurable: true, get: () => mockLocalStorage }); realNavigatorDatastores = navigator.getDataStores; navigator.getDataStores = MockNavigatorDatastore.getDataStores; bookmarks = new Datastore(datastoreName); }); teardown(() => { mockIndexedDB.mTearDown(); Object.defineProperty(window, 'localStorage', { configurable: true, get: () => realLocalStorage }); navigator.getDataStores = realNavigatorDatastores; bookmarks = null; }); suite('Datastore constructor', () => { test('should store the name of the datastore', () => { assert.equal(bookmarks.name, datastoreName); assert.isTrue(bookmarks.revisionName.includes(datastoreName)); assert.isTrue(bookmarks.idbName.includes(datastoreName)); assert.isTrue(bookmarks.storeName.includes(datastoreName)); }); }); suite('Datastore#init()', () => { test('should open indexedDB and Datastore instances', done => { assert.isNull(bookmarks.db); assert.isNull(bookmarks.datastore); bookmarks.init().then(() => { assert.isObject(bookmarks.db); assert.isObject(bookmarks.datastore); done(); }); }); }); suite('Datastore#upgradeSchema()', () => { var names = []; var onUpgradeNeededEvent = version => { return { target: { result: { createObjectStore: (name, options) => { names.push(name); return { createIndex: (name, keyPath, options) => {} }; } } }, oldVersion: version }; }; setup(() => { names = []; }); test('should create an object store with older versions', () => { bookmarks.upgradeSchema(onUpgradeNeededEvent(0)); assert.deepEqual(names, [bookmarks.storeName]); }); test('should not create object stores with newer versions', () => { bookmarks.upgradeSchema(onUpgradeNeededEvent(2)); assert.equal(names.length, 0); }); }); suite('Datastore#synchronise()', () => { var stub; setup(done => { // Bookmarks calls synchronise once the datastore is open and the // event handler is registered. stub = sinon.stub(bookmarks, 'synchronise', () => { stub.restore(); done(); }); bookmarks.init(); }); teardown(() => { stub.restore(); }); test('should call set() on update', done => { stub = sinon.stub(bookmarks, 'set'); bookmarks.datastore._tasks[0] = { operation: 'update', id: 0, data: {} }; bookmarks.synchronise().then(() => { assert.isTrue(stub.calledOnce); done(); }); }); test('should call set() on add', done => { stub = sinon.stub(bookmarks, 'set'); bookmarks.datastore._tasks[0] = { operation: 'add', id: 0, data: {} }; bookmarks.synchronise().then(() => { assert.isTrue(stub.calledOnce); done(); }); }); test('should call remove() on remove', done => { stub = sinon.stub(bookmarks, 'remove'); bookmarks.datastore._tasks[0] = { operation: 'remove', id: 0, data: {} }; bookmarks.synchronise().then(() => { assert.isTrue(stub.calledOnce); done(); }); }); test('should call clear() on clear', done => { stub = sinon.stub(bookmarks, 'clear'); bookmarks.datastore._tasks[0] = { operation: 'clear', id: 0, data: {} }; bookmarks.synchronise().then(() => { assert.isTrue(stub.calledOnce); done(); }); }); test('should call updateRevision() when done', done => { stub = sinon.stub(bookmarks, 'updateRevision'); bookmarks.datastore._tasks = [{ operation: 'done', id: 0, data: null }]; bookmarks.synchronise().then(() => { assert.isTrue(stub.calledOnce); done(); }); }); }); suite('indexedDB backed methods', () => { var objectStores = {}; var deletedValues = []; var attachDBTransaction = objectStoreName => { objectStores = {}; deletedValues = []; bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => { var oncomplete = null; return { objectStore: name => { return { put: value => { if (name !== objectStoreName) { return; } objectStores[name] = value; if (oncomplete) { oncomplete(); oncomplete = null; } }, delete: value => { if (name !== objectStoreName) { return; } deletedValues.push(value); if (oncomplete) { oncomplete(); oncomplete = null; } }, clear: () => { if (name !== objectStoreName) { return; } if (oncomplete) { oncomplete(); oncomplete = null; } }, get: () => { if (name !== objectStoreName) { return; } return { set onsuccess(cb) { cb({ target: { result: { id: 'abc/', data: {} } } }); } }; }, openCursor: () => { return { set onsuccess(cb) { if (objectStoreNames.indexOf(name) === -1) { return; } cb({ target: { result: { value: { id: 'abc/', data: {} }, continue: () => { cb({ target: { result: { value: { id: 'def/', data: {} }, continue: () => {} } } }); } } } }); if (oncomplete) { oncomplete(); oncomplete = null; } } }; } }; }, set oncomplete(cb) { oncomplete = cb; }, onerror: () => {} }; }; }; setup(() => { bookmarks.db = {}; }); suite('Datastore#set()', () => { var data = { id: 'abc/', misc: 'def' }; setup(() => { attachDBTransaction(bookmarks.storeName); }); test('should store data', done => { bookmarks.set(data) .then(() => { assert.equal(Object.keys(objectStores).length, 1); assert.equal(objectStores[bookmarks.storeName].id, data.id); assert.equal(objectStores[bookmarks.storeName].data, data); done(); }); }); test('should emit an event', done => { document.addEventListener(bookmarks.name + '-set', evt => { assert.equal(evt.detail.id, data.id); done(); }); bookmarks.set(data); }); test('handles exception', done => { var realTransaction = bookmarks.db.transaction; bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => { var txn = realTransaction(objectStores, IDBTransactionMode); var realObjectStore = txn.objectStore; txn.objectStore = objectStoreName => { var objectStore = realObjectStore(objectStoreName); sinon.stub(objectStore, 'put', () => { throw 'this exception should be caught'; }); return objectStore; }; return txn; }; bookmarks.set(data).then(done, done); }); }); suite('Datastore#remove()', () => { var id = 'abc/'; setup(() => { attachDBTransaction(bookmarks.storeName); }); test('should remove data from object store', done => { bookmarks.remove(id) .then(() => { assert.deepEqual(deletedValues, [id]); done(); }); }); test('should emit an event', done => { document.addEventListener(bookmarks.name + '-removed', evt => { assert.equal(evt.detail.id, id); done(); }); bookmarks.remove(id); }); test('handles exception', done => { var realTransaction = bookmarks.db.transaction; bookmarks.db.transaction = (objectStoreNames, IDBTransactionMode) => { var txn = realTransaction(objectStores, IDBTransactionMode); var realObjectStore = txn.objectStore; txn.objectStore = objectStoreName => { var objectStore = realObjectStore(objectStoreName); sinon.stub(objectStore, 'delete', () => { throw 'this exception should be caught'; }); return objectStore; }; return txn; }; bookmarks.remove(id).then(done, done); }); }); suite('Datastore#clear()', () => { test('should remove all data from object store', done => { attachDBTransaction(bookmarks.storeName); bookmarks.clear() .then(() => { done(); }); }); test('should emit an event', done => { attachDBTransaction(bookmarks.storeName); document.addEventListener(bookmarks.name + '-cleared', () => { done(); }); bookmarks.clear(); }); }); suite('Datastore#get()', () => { test('should retrieve data from object store', done => { attachDBTransaction(bookmarks.storeName); bookmarks.get('abc/') .then(results => { assert.deepEqual(results, { id: 'abc/', data: {} }); done(); }); }); }); suite('Datastore#getAll()', () => { test('should retrieve data from object store', done => { attachDBTransaction(bookmarks.storeName); bookmarks.getAll() .then(results => { assert.equal(results.length, 2); assert.deepEqual(results[0], { id: 'abc/', data: {} }); assert.deepEqual(results[1], { id: 'def/', data: {} }); done(); }); }); test('should accept a filtering function as argument', done => { attachDBTransaction(bookmarks.storeName); bookmarks.getAll(entry => entry.id.includes('def/')) .then(results => { assert.equal(results.length, 1); assert.deepEqual(results[0], { id: 'def/', data: {} }); done(); }); }); }); }); suite('Datastore#updateRevision()', () => { setup(done => { bookmarks.init().then(done); }); test('should set the datastore revision ID in lastRevision', () => { assert.isNull(bookmarks.lastRevision); bookmarks.updateRevision(); assert.equal(bookmarks.lastRevision, bookmarks.datastore.revisionId); }); test('should update the datastore revision ID in lastRevision', done => { var newRevisionId = 'xxx-zzz'; bookmarks.datastore._tasks = [{ operation: 'done', id: 0, data: null }]; bookmarks.datastore.revisionId = newRevisionId; bookmarks.synchronise().then(() => { bookmarks.init().then(() => { assert.equal(bookmarks.lastRevision, newRevisionId); done(); }); }); }); }); suite('Datastore#onChange()', () => { test('should call synchronise()', done => { var synchroniseStub = sinon.stub(bookmarks, 'synchronise', () => Promise.resolve()); bookmarks.onChange(); setTimeout(() => { assert.isTrue(synchroniseStub.calledOnce); done(); }); }); }); });