From 50fa46b44608ac5e19a5d1d4af83b4ee29eb724a Mon Sep 17 00:00:00 2001 From: Andrei Oprea Date: Fri, 20 Apr 2018 18:18:44 +0200 Subject: [PATCH] Fix Bug 1449223 - Add Telemetry for failed IndexedDB transactions --- system-addon/lib/ActivityStream.jsm | 7 -- system-addon/lib/ActivityStreamStorage.jsm | 74 ++++++++++----- system-addon/lib/PrefsFeed.jsm | 11 ++- system-addon/lib/SectionsManager.jsm | 16 +++- system-addon/lib/SnippetsFeed.jsm | 3 +- system-addon/lib/Store.jsm | 17 +++- system-addon/lib/TopSitesFeed.jsm | 28 +++--- .../test/unit/lib/ActivityStream.test.js | 5 - .../unit/lib/ActivityStreamStorage.test.js | 91 +++++++++++++++++-- system-addon/test/unit/lib/PrefsFeed.test.js | 52 ++++++----- .../test/unit/lib/SectionsManager.test.js | 70 ++++++++------ .../test/unit/lib/SnippetsFeed.test.js | 58 +++++------- system-addon/test/unit/lib/Store.test.js | 54 +++++++---- .../test/unit/lib/TopSitesFeed.test.js | 61 ++++++++++--- 14 files changed, 360 insertions(+), 187 deletions(-) diff --git a/system-addon/lib/ActivityStream.jsm b/system-addon/lib/ActivityStream.jsm index 426172386..277be1c42 100644 --- a/system-addon/lib/ActivityStream.jsm +++ b/system-addon/lib/ActivityStream.jsm @@ -26,7 +26,6 @@ const {FaviconFeed} = ChromeUtils.import("resource://activity-stream/lib/Favicon const {TopSitesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopSitesFeed.jsm", {}); const {TopStoriesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {}); const {HighlightsFeed} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm", {}); -const {ActivityStreamStorage} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); const {ThemeFeed} = ChromeUtils.import("resource://activity-stream/lib/ThemeFeed.jsm", {}); const {MessageCenterFeed} = ChromeUtils.import("resource://activity-stream/lib/MessageCenterFeed.jsm", {}); @@ -278,7 +277,6 @@ this.ActivityStream = class ActivityStream { this.store = new Store(); this.feeds = FEEDS_CONFIG; this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG); - this._storage = new ActivityStreamStorage(["sectionPrefs", "snippets"]); } init() { @@ -286,11 +284,6 @@ this.ActivityStream = class ActivityStream { this._updateDynamicPrefs(); this._defaultPrefs.init(); - // Accessing the db causes the object stores to be created / migrated. - // This needs to happen before other instances try to access the db, which - // would update only a subset of the stores to the latest version. - this._storage.db; // eslint-disable-line no-unused-expressions - // Hook up the store and let all feeds and pages initialize this.store.init(this.feeds, ac.BroadcastToContent({ type: at.INIT, diff --git a/system-addon/lib/ActivityStreamStorage.jsm b/system-addon/lib/ActivityStreamStorage.jsm index e33ff58b5..33821df1a 100644 --- a/system-addon/lib/ActivityStreamStorage.jsm +++ b/system-addon/lib/ActivityStreamStorage.jsm @@ -2,50 +2,82 @@ ChromeUtils.defineModuleGetter(this, "IndexedDB", "resource://gre/modules/Indexe this.ActivityStreamStorage = class ActivityStreamStorage { /** - * @param storeName String with the store name to access or array of strings - * to create all the required stores + * @param storeNames Array of strings used to create all the required stores */ - constructor(storeName) { + constructor(options = {}) { + if (!options.storeNames || !options.telemetry) { + throw new Error(`storeNames and telemetry are required, called only with ${Object.keys(options)}`); + } + this.dbName = "ActivityStream"; this.dbVersion = 3; - this.storeName = storeName; + this.storeNames = options.storeNames; + this.telemetry = options.telemetry; } get db() { return this._db || (this._db = this._openDatabase()); } - async getStore() { - return (await this.db).objectStore(this.storeName, "readwrite"); + /** + * Public method that binds the store required by the consumer and exposes + * the private db getters and setters. + * + * @param storeName String name of desired store + */ + getDbTable(storeName) { + if (this.storeNames.includes(storeName)) { + return { + get: this._get.bind(this, storeName), + getAll: this._getAll.bind(this, storeName), + set: this._set.bind(this, storeName) + }; + } + + throw new Error(`Store name ${storeName} does not exist.`); } - async get(key) { - return (await this.getStore()).get(key); + async _getStore(storeName) { + return (await this.db).objectStore(storeName, "readwrite"); } - async getAll() { - return (await this.getStore()).getAll(); + _get(storeName, key) { + return this._requestWrapper(async () => (await this._getStore(storeName)).get(key)); } - async set(key, value) { - return (await this.getStore()).put(value, key); + _getAll(storeName) { + return this._requestWrapper(async () => (await this._getStore(storeName)).getAll()); + } + + _set(storeName, key, value) { + return this._requestWrapper(async () => (await this._getStore(storeName)).put(value, key)); } _openDatabase() { return IndexedDB.open(this.dbName, {version: this.dbVersion}, db => { // If provided with array of objectStore names we need to create all the // individual stores - if (Array.isArray(this.storeName)) { - this.storeName.forEach(store => { - if (!db.objectStoreNames.contains(store)) { - db.createObjectStore(store); - } - }); - } else if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName); - } + this.storeNames.forEach(store => { + if (!db.objectStoreNames.contains(store)) { + this._requestWrapper(() => db.createObjectStore(store)); + } + }); }); } + + async _requestWrapper(request) { + let result = null; + try { + result = await request(); + } catch (e) { + if (this.telemetry) { + this.telemetry.handleUndesiredEvent({data: {event: "TRANSACTION_FAILED"}}); + } + throw e; + } + + return result; + } }; function getDefaultOptions(options) { diff --git a/system-addon/lib/PrefsFeed.jsm b/system-addon/lib/PrefsFeed.jsm index 86807804c..a8313cbca 100644 --- a/system-addon/lib/PrefsFeed.jsm +++ b/system-addon/lib/PrefsFeed.jsm @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const {ActivityStreamStorage} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {}); const {Prefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {}); const {PrerenderData} = ChromeUtils.import("resource://activity-stream/common/PrerenderData.jsm", {}); @@ -32,7 +31,6 @@ this.PrefsFeed = class PrefsFeed { constructor(prefMap) { this._prefMap = prefMap; this._prefs = new Prefs(); - this._storage = new ActivityStreamStorage("sectionPrefs"); } // If any prefs or the theme are set to something other than what the @@ -92,6 +90,7 @@ this.PrefsFeed = class PrefsFeed { init() { this._prefs.observeBranch(this); + this._storage = this.store.dbStorage.getDbTable("sectionPrefs"); // Get the initial value of each activity stream pref const values = {}; @@ -118,8 +117,12 @@ this.PrefsFeed = class PrefsFeed { async _setIndexedDBPref(id, value) { const name = id === "topsites" ? id : `feeds.section.${id}`; - await this._storage.set(name, value); - this._setPrerenderPref(); + try { + await this._storage.set(name, value); + this._setPrerenderPref(); + } catch (e) { + Cu.reportError("Could not set section preferences."); + } } onAction(action) { diff --git a/system-addon/lib/SectionsManager.jsm b/system-addon/lib/SectionsManager.jsm index 34ee61209..74010be9f 100644 --- a/system-addon/lib/SectionsManager.jsm +++ b/system-addon/lib/SectionsManager.jsm @@ -7,7 +7,7 @@ ChromeUtils.import("resource://gre/modules/EventEmitter.jsm"); ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {}); -const {ActivityStreamStorage, getDefaultOptions} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); +const {getDefaultOptions} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); ChromeUtils.defineModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); @@ -80,8 +80,8 @@ const SectionsManager = { }, initialized: false, sections: new Map(), - async init(prefs = {}) { - this._storage = new ActivityStreamStorage("sectionPrefs"); + async init(prefs = {}, storage) { + this._storage = storage; for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS)) { const optionsPrefName = `${feedPrefName}.options`; @@ -126,13 +126,19 @@ const SectionsManager = { }, async addBuiltInSection(feedPrefName, optionsPrefValue = "{}") { let options; + let storedPrefs; try { options = JSON.parse(optionsPrefValue); } catch (e) { options = {}; Cu.reportError(`Problem parsing options pref for ${feedPrefName}`); } - const storedPrefs = await this._storage.get(feedPrefName) || {}; + try { + storedPrefs = await this._storage.get(feedPrefName) || {}; + } catch (e) { + storedPrefs = {}; + Cu.reportError(`Problem getting stored prefs for ${feedPrefName}`); + } const defaultSection = BUILT_IN_SECTIONS[feedPrefName](options); const section = Object.assign({}, defaultSection, {pref: Object.assign({}, defaultSection.pref, getDefaultOptions(storedPrefs))}); section.pref.feed = feedPrefName; @@ -406,7 +412,7 @@ class SectionsFeed { break; // Wait for pref values, as some sections have options stored in prefs case at.PREFS_INITIAL_VALUES: - SectionsManager.init(action.data); + SectionsManager.init(action.data, this.store.dbStorage.getDbTable("sectionPrefs")); break; case at.PREF_CHANGED: { if (action.data) { diff --git a/system-addon/lib/SnippetsFeed.jsm b/system-addon/lib/SnippetsFeed.jsm index d0534dc87..6542707fa 100644 --- a/system-addon/lib/SnippetsFeed.jsm +++ b/system-addon/lib/SnippetsFeed.jsm @@ -5,7 +5,6 @@ ChromeUtils.import("resource://gre/modules/Services.jsm"); const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {}); -const {ActivityStreamStorage} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); ChromeUtils.defineModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); @@ -39,7 +38,6 @@ this.SnippetsFeed = class SnippetsFeed { this._refresh = this._refresh.bind(this); this._totalBookmarks = null; this._totalBookmarksLastUpdated = null; - this._storage = new ActivityStreamStorage("snippets"); } get snippetsURL() { @@ -169,6 +167,7 @@ this.SnippetsFeed = class SnippetsFeed { } async init() { + this._storage = this.store.dbStorage.getDbTable("snippets"); Services.obs.addObserver(this, SEARCH_ENGINE_OBSERVER_TOPIC); this._previousSessionEnd = await this._storage.get("previousSessionEnd"); await this._refresh(); diff --git a/system-addon/lib/Store.jsm b/system-addon/lib/Store.jsm index a14e7833e..faff358dc 100644 --- a/system-addon/lib/Store.jsm +++ b/system-addon/lib/Store.jsm @@ -4,6 +4,7 @@ "use strict"; const {ActivityStreamMessageChannel} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {}); +const {ActivityStreamStorage} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); const {Prefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {}); const {reducers} = ChromeUtils.import("resource://activity-stream/common/Reducers.jsm", {}); const {redux} = ChromeUtils.import("resource://activity-stream/vendor/Redux.jsm", {}); @@ -34,6 +35,7 @@ this.Store = class Store { redux.combineReducers(reducers), redux.applyMiddleware(this._middleware, this._messageChannel.middleware) ); + this.storage = null; } /** @@ -112,7 +114,7 @@ this.Store = class Store { * to feeds when they're created. * @param {Action} uninitAction An optional action for when feeds uninit. */ - init(feedFactories, initAction, uninitAction) { + async init(feedFactories, initAction, uninitAction) { this._feedFactories = feedFactories; this._initAction = initAction; this._uninitAction = uninitAction; @@ -122,6 +124,8 @@ this.Store = class Store { this.initFeed(telemetryKey); } + await this._initIndexedDB(telemetryKey); + for (const pref of feedFactories.keys()) { if (pref !== telemetryKey && this._prefs.get(pref)) { this.initFeed(pref); @@ -140,6 +144,17 @@ this.Store = class Store { this._messageChannel.simulateMessagesForExistingTabs(); } + async _initIndexedDB(telemetryKey) { + this.dbStorage = new ActivityStreamStorage({ + storeNames: ["sectionPrefs", "snippets"], + telemetry: this.feeds.get(telemetryKey) + }); + // Accessing the db causes the object stores to be created / migrated. + // This needs to happen before other instances try to access the db, which + // would update only a subset of the stores to the latest version. + await this.dbStorage.db; // eslint-disable-line no-unused-expressions + } + /** * uninit - Uninitalizes each feed, clears them, and destroys the message * manager channel. diff --git a/system-addon/lib/TopSitesFeed.jsm b/system-addon/lib/TopSitesFeed.jsm index 806f656d8..9a4b4690a 100644 --- a/system-addon/lib/TopSitesFeed.jsm +++ b/system-addon/lib/TopSitesFeed.jsm @@ -10,7 +10,7 @@ const {TippyTopProvider} = ChromeUtils.import("resource://activity-stream/lib/Ti const {insertPinned, TOP_SITES_MAX_SITES_PER_ROW} = ChromeUtils.import("resource://activity-stream/common/Reducers.jsm", {}); const {Dedupe} = ChromeUtils.import("resource://activity-stream/common/Dedupe.jsm", {}); const {shortURL} = ChromeUtils.import("resource://activity-stream/lib/ShortURL.jsm", {}); -const {ActivityStreamStorage, getDefaultOptions} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); +const {getDefaultOptions} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamStorage.jsm", {}); ChromeUtils.defineModuleGetter(this, "filterAdult", "resource://activity-stream/lib/FilterAdult.jsm"); @@ -42,7 +42,14 @@ this.TopSitesFeed = class TopSitesFeed { this.pinnedCache = new LinksCache(NewTabUtils.pinnedLinks, "links", [...CACHED_LINK_PROPS_TO_MIGRATE, ...PINNED_FAVICON_PROPS_TO_MIGRATE]); PageThumbs.addExpirationFilter(this); - this._storage = new ActivityStreamStorage("sectionPrefs"); + } + + async init() { + await this._tippyTopProvider.init(); + // If the feed was previously disabled PREFS_INITIAL_VALUES was never received + this.refreshDefaults(this.store.getState().Prefs.values[DEFAULT_SITES_PREF]); + this._storage = this.store.dbStorage.getDbTable("sectionPrefs"); + this.refresh({broadcast: true}); } uninit() { @@ -165,14 +172,15 @@ this.TopSitesFeed = class TopSitesFeed { * @param {bool} options.broadcast Should the update be broadcasted. */ async refresh(options = {}) { - if (!this._tippyTopProvider.initialized) { - await this._tippyTopProvider.init(); - } - const links = await this.getLinksWithDefaults(); const newAction = {type: at.TOP_SITES_UPDATED, data: {links}}; - - const storedPrefs = await this._storage.get(SECTION_ID) || {}; + let storedPrefs; + try { + storedPrefs = await this._storage.get(SECTION_ID) || {}; + } catch (e) { + storedPrefs = {}; + Cu.reportError("Problem getting stored prefs for TopSites"); + } newAction.data.pref = getDefaultOptions(storedPrefs); if (options.broadcast) { @@ -374,9 +382,7 @@ this.TopSitesFeed = class TopSitesFeed { onAction(action) { switch (action.type) { case at.INIT: - // If the feed was previously disabled PREFS_INITIAL_VALUES was never received - this.refreshDefaults(this.store.getState().Prefs.values[DEFAULT_SITES_PREF]); - this.refresh({broadcast: true}); + this.init(); break; case at.SYSTEM_TICK: this.refresh({broadcast: false}); diff --git a/system-addon/test/unit/lib/ActivityStream.test.js b/system-addon/test/unit/lib/ActivityStream.test.js index cf42a9b50..2bd8128b8 100644 --- a/system-addon/test/unit/lib/ActivityStream.test.js +++ b/system-addon/test/unit/lib/ActivityStream.test.js @@ -34,7 +34,6 @@ describe("ActivityStream", () => { sandbox.stub(as.store, "uninit"); sandbox.stub(as._defaultPrefs, "init"); sandbox.stub(as._defaultPrefs, "reset"); - sandbox.stub(as._storage, "_openDatabase"); }); afterEach(() => sandbox.restore()); @@ -58,14 +57,10 @@ describe("ActivityStream", () => { it("should call .store.init", () => { assert.calledOnce(as.store.init); }); - it("should cause storage to open database", () => { - assert.calledOnce(as._storage._openDatabase); - }); it("should pass to Store an INIT event with the right version", () => { as = new ActivityStream({version: "1.2.3"}); sandbox.stub(as.store, "init"); sandbox.stub(as._defaultPrefs, "init"); - sandbox.stub(as._storage, "_openDatabase"); as.init(); diff --git a/system-addon/test/unit/lib/ActivityStreamStorage.test.js b/system-addon/test/unit/lib/ActivityStreamStorage.test.js index 97be5e4c2..4412309e3 100644 --- a/system-addon/test/unit/lib/ActivityStreamStorage.test.js +++ b/system-addon/test/unit/lib/ActivityStreamStorage.test.js @@ -9,9 +9,12 @@ describe("ActivityStreamStorage", () => { let storage; beforeEach(() => { sandbox = sinon.sandbox.create(); - indexedDB = {open: sandbox.stub().returns(Promise.resolve({}))}; + indexedDB = {open: sandbox.stub().resolves({})}; overrider.set({IndexedDB: indexedDB}); - storage = new ActivityStreamStorage("storage_test"); + storage = new ActivityStreamStorage({ + storeNames: ["storage_test"], + telemetry: {handleUndesiredEvent: sandbox.stub()} + }); }); afterEach(() => { sandbox.restore(); @@ -19,14 +22,61 @@ describe("ActivityStreamStorage", () => { it("should not throw an error when accessing db", async () => { assert.ok(storage.db); }); - it("should revert key value parameters for put", async () => { - const stub = sandbox.stub(); - sandbox.stub(storage, "getStore").resolves({put: stub}); + it("should throw if arguments not provided", () => { + assert.throws(() => new ActivityStreamStorage()); + }); + describe("#getDbTable", () => { + let testStorage; + let storeStub; + beforeEach(() => { + storeStub = { + getAll: sandbox.stub().resolves(), + get: sandbox.stub().resolves(), + put: sandbox.stub().resolves() + }; + sandbox.stub(storage, "_getStore").resolves(storeStub); + testStorage = storage.getDbTable("storage_test"); + }); + it("should reverse key value parameters for put", async () => { + await testStorage.set("key", "value"); - await storage.set("key", "value"); + assert.calledOnce(storeStub.put); + assert.calledWith(storeStub.put, "value", "key"); + }); + it("should return the correct value for get", async () => { + storeStub.get.withArgs("foo").resolves("foo"); - assert.calledOnce(stub); - assert.calledWith(stub, "value", "key"); + const result = await testStorage.get("foo"); + + assert.calledOnce(storeStub.get); + assert.equal(result, "foo"); + }); + it("should return the correct value for getAll", async () => { + storeStub.getAll.resolves(["bar"]); + + const result = await testStorage.getAll(); + + assert.calledOnce(storeStub.getAll); + assert.deepEqual(result, ["bar"]); + }); + it("should query the correct object store", async () => { + await testStorage.get(); + + assert.calledOnce(storage._getStore); + assert.calledWithExactly(storage._getStore, "storage_test"); + }); + it("should throw if table is not found", () => { + assert.throws(() => storage.getDbTable("undefined_store")); + }); + }); + it("should get the correct objectStore when calling _getStore", async () => { + const objectStoreStub = sandbox.stub(); + indexedDB.open.resolves({objectStore: objectStoreStub}); + + await storage._getStore("foo"); + + assert.calledOnce(objectStoreStub); + assert.calledWithExactly(objectStoreStub, "foo", "readwrite"); }); it("should create a db with the correct store name", async () => { const dbStub = {createObjectStore: sandbox.stub(), objectStoreNames: {contains: sandbox.stub().returns(false)}}; @@ -39,7 +89,10 @@ describe("ActivityStreamStorage", () => { assert.calledWithExactly(dbStub.createObjectStore, "storage_test"); }); it("should handle an array of object store names", async () => { - storage = new ActivityStreamStorage(["store1", "store2"]); + storage = new ActivityStreamStorage({ + storeNames: ["store1", "store2"], + telemetry: {} + }); const dbStub = {createObjectStore: sandbox.stub(), objectStoreNames: {contains: sandbox.stub().returns(false)}}; await storage.db; @@ -51,7 +104,10 @@ describe("ActivityStreamStorage", () => { assert.calledWith(dbStub.createObjectStore, "store2"); }); it("should skip creating existing stores", async () => { - storage = new ActivityStreamStorage(["store1", "store2"]); + storage = new ActivityStreamStorage({ + storeNames: ["store1", "store2"], + telemetry: {} + }); const dbStub = {createObjectStore: sandbox.stub(), objectStoreNames: {contains: sandbox.stub().returns(true)}}; await storage.db; @@ -60,4 +116,19 @@ describe("ActivityStreamStorage", () => { assert.notCalled(dbStub.createObjectStore); }); + describe("#_requestWrapper", () => { + it("should return a successful result", async () => { + const result = await storage._requestWrapper(() => Promise.resolve("foo")); + + assert.equal(result, "foo"); + assert.notCalled(storage.telemetry.handleUndesiredEvent); + }); + it("should report failures", async () => { + try { + await storage._requestWrapper(() => Promise.reject(new Error())); + } catch (e) { + assert.calledOnce(storage.telemetry.handleUndesiredEvent); + } + }); + }); }); diff --git a/system-addon/test/unit/lib/PrefsFeed.test.js b/system-addon/test/unit/lib/PrefsFeed.test.js index 07352625a..d569b1ac8 100644 --- a/system-addon/test/unit/lib/PrefsFeed.test.js +++ b/system-addon/test/unit/lib/PrefsFeed.test.js @@ -17,11 +17,18 @@ describe("PrefsFeed", () => { sandbox = sinon.sandbox.create(); FAKE_PREFS = new Map([["foo", 1], ["bar", 2]]); feed = new PrefsFeed(FAKE_PREFS); + const storage = { + getAll: sandbox.stub().resolves(), + set: sandbox.stub().resolves() + }; feed.store = { dispatch: sinon.spy(), getState() { return this.state; }, - state: {Theme: {className: ""}} + state: {Theme: {className: ""}}, + dbStorage: {getDbTable: sandbox.stub().returns(storage)} }; + // Setup for tests that don't call `init` + feed._storage = storage; feed._prefs = { get: sinon.spy(item => FAKE_PREFS.get(item)), set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)), @@ -31,17 +38,7 @@ describe("PrefsFeed", () => { ignoreBranch: sinon.spy(), reset: sinon.stub() }; - const fakeDB = { - objectStore: sandbox.stub().returns({ - get: sandbox.stub().returns(Promise.resolve()), - set: sandbox.stub().returns(Promise.resolve()) - }) - }; - overrider.set({ - PrivateBrowsingUtils: {enabled: true}, - ActivityStreamStorage: function Fake() {}, - IndexedDB: {open: () => Promise.resolve(fakeDB)} - }); + overrider.set({PrivateBrowsingUtils: {enabled: true}}); }); afterEach(() => { overrider.restore(); @@ -65,6 +62,12 @@ describe("PrefsFeed", () => { assert.calledOnce(feed._prefs.observeBranch); assert.calledWith(feed._prefs.observeBranch, feed); }); + it("should initialise the storage on init", () => { + feed.init(); + + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs"); + }); it("should remove the branch observer on uninit", () => { feed.onAction({type: at.UNINIT}); assert.calledOnce(feed._prefs.ignoreBranch); @@ -84,7 +87,6 @@ describe("PrefsFeed", () => { }); it("should set prerender pref to true if prefs match initial values", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); await feed._setPrerenderPref(); @@ -93,7 +95,6 @@ describe("PrefsFeed", () => { it("should set prerender pref to false if a pref does not match its initial value", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); FAKE_PREFS.set("showSearch", false); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); await feed._setPrerenderPref(); @@ -101,7 +102,7 @@ describe("PrefsFeed", () => { }); it("should set prerender pref to true if indexedDB prefs are unchanged", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([{collapsed: false}, {collapsed: false}])); + feed._storage.getAll.resolves([{collapsed: false}, {collapsed: false}]); await feed._setPrerenderPref(); @@ -110,7 +111,7 @@ describe("PrefsFeed", () => { it("should set prerender pref to false if a indexedDB pref changed value", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); FAKE_PREFS.set("showSearch", false); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([{collapsed: false}, {collapsed: true}])); + feed._storage.getAll.resolves([{collapsed: false}, {collapsed: true}]); await feed._setPrerenderPref(); @@ -170,7 +171,6 @@ describe("PrefsFeed", () => { assert.calledOnce(feed._setIndexedDBPref); }); it("should store the pref value", async () => { - sandbox.stub(feed._storage, "set").returns(Promise.resolve()); sandbox.stub(feed, "_setPrerenderPref"); await feed._setIndexedDBPref("topsites", "foo"); @@ -178,12 +178,20 @@ describe("PrefsFeed", () => { assert.calledWith(feed._storage.set, "topsites", "foo"); }); it("should call _setPrerenderPref", async () => { - sandbox.stub(feed._storage, "set").returns(Promise.resolve()); sandbox.stub(feed, "_setPrerenderPref"); await feed._setIndexedDBPref("topsites", "foo"); assert.calledOnce(feed._setPrerenderPref); }); + it("should catch any save errors", () => { + const globals = new GlobalOverrider(); + globals.sandbox.spy(global.Cu, "reportError"); + feed._storage.set.throws(new Error()); + + assert.doesNotThrow(() => feed._setIndexedDBPref()); + assert.calledOnce(Cu.reportError); + globals.restore(); + }); }); describe("onPrefChanged prerendering", () => { it("should not change the prerender pref if the pref is not included in invalidatingPrefs", () => { @@ -212,7 +220,6 @@ describe("PrefsFeed", () => { FAKE_PREFS.set("showSearch", false); feed._prefs.set("showSearch", true); feed.onPrefChanged("showSearch", true); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); await feed._setPrerenderPref(); @@ -223,7 +230,6 @@ describe("PrefsFeed", () => { FAKE_PREFS.set("showSearch", false); feed._prefs.set("showSearch", false); feed.onPrefChanged("showSearch", false); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); await feed._setPrerenderPref(); @@ -266,16 +272,14 @@ describe("PrefsFeed", () => { assert.calledOnce(feed._setPrerenderPref); }); - it("should should set the prerender pref to false if the theme is changed to be different than the default", async () => { + it("should set the prerender pref to false if the theme is changed to be different than the default", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); await feed._setPrerenderPref({className: "dark-theme"}); // feed.onAction({type: at.THEME_UPDATE, data: {className: "dark-theme"}}); assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, false); }); - it("should should set the prerender pref back to true if the theme is changed to the default", async () => { + it("should set the prerender pref back to true if the theme is changed to the default", async () => { Object.keys(initialPrefs).forEach(name => FAKE_PREFS.set(name, initialPrefs[name])); - sandbox.stub(feed._storage, "getAll").returns(Promise.resolve([])); feed.store.state.Theme.className = "dark-theme"; await feed._setPrerenderPref({className: ""}); assert.calledWith(feed._prefs.set, PRERENDER_PREF_NAME, true); diff --git a/system-addon/test/unit/lib/SectionsManager.test.js b/system-addon/test/unit/lib/SectionsManager.test.js index 2b330b6b7..41c156341 100644 --- a/system-addon/test/unit/lib/SectionsManager.test.js +++ b/system-addon/test/unit/lib/SectionsManager.test.js @@ -15,29 +15,26 @@ describe("SectionsManager", () => { let fakeServices; let fakePlacesUtils; let sandbox; + let storage; beforeEach(async () => { sandbox = sinon.sandbox.create(); globals = new GlobalOverrider(); fakeServices = {prefs: {getBoolPref: sandbox.stub(), addObserver: sandbox.stub(), removeObserver: sandbox.stub()}}; fakePlacesUtils = {history: {update: sinon.stub(), insert: sinon.stub()}}; - const fakeDB = { - objectStore: sandbox.stub().returns({ - get: sandbox.stub().returns(Promise.resolve()), - set: sandbox.stub().returns(Promise.resolve()) - }) - }; globals.set({ Services: fakeServices, - PlacesUtils: fakePlacesUtils, - ActivityStreamStorage: function Fake() {}, - IndexedDB: {open: () => Promise.resolve(fakeDB)} + PlacesUtils: fakePlacesUtils }); + // Redecorate SectionsManager to remove any listeners that have been added + EventEmitter.decorate(SectionsManager); + storage = { + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves() + }; }); afterEach(() => { - // Redecorate SectionsManager to remove any listeners that have been added - EventEmitter.decorate(SectionsManager); globals.restore(); sandbox.restore(); }); @@ -46,7 +43,7 @@ describe("SectionsManager", () => { it("should initialise the sections map with the built in sections", async () => { SectionsManager.sections.clear(); SectionsManager.initialized = false; - await SectionsManager.init(); + await SectionsManager.init({}, storage); assert.equal(SectionsManager.sections.size, 2); assert.ok(SectionsManager.sections.has("topstories")); assert.ok(SectionsManager.sections.has("highlights")); @@ -54,15 +51,20 @@ describe("SectionsManager", () => { it("should set .initialized to true", async () => { SectionsManager.sections.clear(); SectionsManager.initialized = false; - await SectionsManager.init(); + await SectionsManager.init({}, storage); assert.ok(SectionsManager.initialized); }); it("should add observer for context menu prefs", async () => { SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"}; - await SectionsManager.init(); + await SectionsManager.init({}, storage); assert.calledOnce(fakeServices.prefs.addObserver); assert.calledWith(fakeServices.prefs.addObserver, "MENU_ITEM_PREF", SectionsManager); }); + it("should save the reference to `storage` passed in", async () => { + await SectionsManager.init({}, storage); + + assert.equal(SectionsManager._storage, storage); + }); }); describe("#uninit", () => { it("should remove observer for context menu prefs", () => { @@ -89,6 +91,20 @@ describe("SectionsManager", () => { assert.calledOnce(Cu.reportError); }); + it("should not throw if the indexedDB operation fails", async () => { + globals.sandbox.spy(global.Cu, "reportError"); + storage.get.returns(new Error()); + SectionsManager._storage = storage; + + try { + await SectionsManager.addBuiltInSection("feeds.section.topstories"); + } catch (e) { + assert.fail(); + } + + assert.calledOnce(storage.get); + assert.calledOnce(Cu.reportError); + }); }); describe("#updateSectionPrefs", () => { it("should update the collapsed value of the section", async () => { @@ -202,7 +218,7 @@ describe("SectionsManager", () => { SectionsManager.updateSections = sinon.spy(); SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"}; - await SectionsManager.init(); + await SectionsManager.init({}, storage); observer.observe("", "nsPref:changed", "MENU_ITEM_PREF"); assert.calledOnce(SectionsManager.updateSections); @@ -374,13 +390,17 @@ describe("SectionsManager", () => { describe("SectionsFeed", () => { let feed; - let globals = new GlobalOverrider(); let sandbox; + let storage; beforeEach(() => { sandbox = sinon.sandbox.create(); SectionsManager.sections.clear(); SectionsManager.initialized = false; + storage = { + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves() + }; feed = new SectionsFeed(); feed.store = {dispatch: sinon.spy()}; feed.store = { @@ -394,22 +414,12 @@ describe("SectionsFeed", () => { } }, Sections: [{initialized: false}] - } + }, + dbStorage: {getDbTable: sandbox.stub().returns(storage)} }; - const fakeDB = { - objectStore: sandbox.stub().returns({ - get: sandbox.stub().returns(Promise.resolve()), - set: sandbox.stub().returns(Promise.resolve()) - }) - }; - globals.set({ - ActivityStreamStorage: function Fake() {}, - IndexedDB: {open: () => Promise.resolve(fakeDB)} - }); }); afterEach(() => { feed.uninit(); - globals.restore(); }); describe("#init", () => { it("should create a SectionsFeed", () => { @@ -429,7 +439,7 @@ describe("SectionsFeed", () => { } }); it("should call onAddSection for any already added sections in SectionsManager", async () => { - await SectionsManager.init(); + await SectionsManager.init({}, storage); assert.ok(SectionsManager.sections.has("topstories")); assert.ok(SectionsManager.sections.has("highlights")); const topstories = SectionsManager.sections.get("topstories"); @@ -551,6 +561,8 @@ describe("SectionsFeed", () => { feed.onAction({type: "PREFS_INITIAL_VALUES", data: {foo: "bar"}}); assert.calledOnce(SectionsManager.init); assert.calledWith(SectionsManager.init, {foo: "bar"}); + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs"); }); it("should call SectionsManager.addBuiltInSection on suitable PREF_CHANGED events", () => { sinon.spy(SectionsManager, "addBuiltInSection"); diff --git a/system-addon/test/unit/lib/SnippetsFeed.test.js b/system-addon/test/unit/lib/SnippetsFeed.test.js index 9da342327..c6edeb973 100644 --- a/system-addon/test/unit/lib/SnippetsFeed.test.js +++ b/system-addon/test/unit/lib/SnippetsFeed.test.js @@ -31,16 +31,9 @@ let overrider = new GlobalOverrider(); describe("SnippetsFeed", () => { let sandbox; let clock; - let fakeDB; beforeEach(() => { clock = sinon.useFakeTimers(); sandbox = sinon.sandbox.create(); - fakeDB = { - objectStore: sandbox.stub().returns({ - get: sandbox.stub().returns(Promise.resolve()), - set: sandbox.stub().returns(Promise.resolve()) - }) - }; overrider.set({ ProfileAge: class ProfileAge { constructor() { @@ -49,16 +42,7 @@ describe("SnippetsFeed", () => { } }, FxAccounts: {config: {promiseSignUpURI: sandbox.stub().returns(Promise.resolve(signUpUrl))}}, - NewTabUtils: {activityStreamProvider: {getTotalBookmarksCount: () => Promise.resolve(42)}}, - ActivityStreamStorage: class ActivityStreamStorage { - constructor() { - this.init = sandbox.stub.callsFake(Promise.resolve()); - } - init() { - return Promise.resolve(); - } - }, - IndexedDB: {open: () => Promise.resolve(fakeDB)} + NewTabUtils: {activityStreamProvider: {getTotalBookmarksCount: () => Promise.resolve(42)}} }); }); afterEach(() => { @@ -89,19 +73,22 @@ describe("SnippetsFeed", () => { fullData: true }); + const getStub = sandbox.stub(); + getStub.withArgs("previousSessionEnd").resolves(42); + getStub.withArgs("blockList").resolves([1]); const feed = new SnippetsFeed(); - feed.store = {dispatch: sandbox.stub()}; - sandbox.stub(feed._storage, "get") - .withArgs("previousSessionEnd") - .resolves(42) - .withArgs("blockList") - .resolves([1]); + feed.store = { + dispatch: sandbox.stub(), + dbStorage: {getDbTable: sandbox.stub().returns({get: getStub})} + }; clock.tick(WEEK_IN_MS * 2); await feed.init(); assert.calledOnce(feed.store.dispatch); + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "snippets"); const [action] = feed.store.dispatch.firstCall.args; assert.propertyVal(action, "type", at.SNIPPETS_DATA); @@ -132,8 +119,6 @@ describe("SnippetsFeed", () => { it("should call .uninit on an UNINIT action", () => { const feed = new SnippetsFeed(); sandbox.stub(feed, "uninit"); - sandbox.stub(feed._storage, "set"); - feed.store = {dispatch: sandbox.stub()}; feed.onAction({type: at.UNINIT}); assert.calledOnce(feed.uninit); @@ -141,7 +126,7 @@ describe("SnippetsFeed", () => { it("should broadcast a SNIPPETS_RESET on uninit", () => { const feed = new SnippetsFeed(); feed.store = {dispatch: sandbox.stub()}; - sandbox.stub(feed._storage, "set"); + feed._storage = {set: sandbox.stub()}; feed.uninit(); @@ -149,15 +134,14 @@ describe("SnippetsFeed", () => { }); it("should update the blocklist on SNIPPETS_BLOCKLIST_UPDATED", async () => { const feed = new SnippetsFeed(); - const saveBlockList = sandbox.stub(feed._storage, "set"); - sandbox.stub(feed._storage, "get").returns(["bar"]); feed.store = {dispatch: sandbox.stub()}; + feed._storage = {set: sandbox.stub(), get: sandbox.stub().returns(["bar"])}; await feed._saveBlockedSnippet("foo"); - assert.calledOnce(saveBlockList); - assert.equal(saveBlockList.args[0][0], "blockList"); - assert.deepEqual(saveBlockList.args[0][1], ["bar", "foo"]); + assert.calledOnce(feed._storage.set); + assert.equal(feed._storage.set.args[0][0], "blockList"); + assert.deepEqual(feed._storage.set.args[0][1], ["bar", "foo"]); }); it("should broadcast a SNIPPET_BLOCKED when a SNIPPETS_BLOCKLIST_UPDATED is received", () => { const feed = new SnippetsFeed(); @@ -177,12 +161,12 @@ describe("SnippetsFeed", () => { }); it("should set blockList to [] on SNIPPETS_BLOCKLIST_CLEARED", async () => { const feed = new SnippetsFeed(); - const stub = sandbox.stub(feed._storage, "set"); + feed._storage = {set: sandbox.stub()}; await feed._clearBlockList(); - assert.calledOnce(stub); - assert.calledWithExactly(stub, "blockList", []); + assert.calledOnce(feed._storage.set); + assert.calledWithExactly(feed._storage.set, "blockList", []); }); it("should dispatch an update event when the Search observer is called", async () => { const feed = new SnippetsFeed(); @@ -258,12 +242,12 @@ describe("SnippetsFeed", () => { it("should set _previousSessionEnd on uninit", async () => { const feed = new SnippetsFeed(); feed.store = {dispatch: sandbox.stub()}; + feed._storage = {set: sandbox.stub()}; feed._previousSessionEnd = null; - const stub = sandbox.stub(feed._storage, "set"); feed.uninit(); - assert.calledOnce(stub); - assert.calledWithExactly(stub, "previousSessionEnd", Date.now()); + assert.calledOnce(feed._storage.set); + assert.calledWithExactly(feed._storage.set, "previousSessionEnd", Date.now()); }); }); diff --git a/system-addon/test/unit/lib/Store.test.js b/system-addon/test/unit/lib/Store.test.js index 4b014403d..4e7e48d6e 100644 --- a/system-addon/test/unit/lib/Store.test.js +++ b/system-addon/test/unit/lib/Store.test.js @@ -6,6 +6,7 @@ describe("Store", () => { let Store; let sandbox; let store; + let dbStub; beforeEach(() => { sandbox = sinon.sandbox.create(); function ActivityStreamMessageChannel(options) { @@ -15,11 +16,18 @@ describe("Store", () => { this.middleware = sandbox.spy(s => next => action => next(action)); this.simulateMessagesForExistingTabs = sandbox.stub(); } + dbStub = sandbox.stub().resolves(); + function FakeActivityStreamStorage() { + this.db = {}; + sinon.stub(this, "db").get(dbStub); + } ({Store} = injector({ "lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}, - "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs} + "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}, + "lib/ActivityStreamStorage.jsm": {ActivityStreamStorage: FakeActivityStreamStorage} })); store = new Store(); + sandbox.stub(store, "_initIndexedDB").resolves(); }); afterEach(() => { sandbox.restore(); @@ -131,35 +139,47 @@ describe("Store", () => { }); }); describe("#init", () => { - it("should call .initFeed with each key", () => { + it("should call .initFeed with each key", async () => { sinon.stub(store, "initFeed"); store._prefs.set("foo", true); store._prefs.set("bar", true); - store.init(new Map([["foo", () => {}], ["bar", () => {}]])); + await store.init(new Map([["foo", () => {}], ["bar", () => {}]])); assert.calledWith(store.initFeed, "foo"); assert.calledWith(store.initFeed, "bar"); }); - it("should not initialize the feed if the Pref is set to false", () => { + it("should call _initIndexedDB", async () => { + await store.init(new Map()); + + assert.calledOnce(store._initIndexedDB); + assert.calledWithExactly(store._initIndexedDB, "feeds.telemetry"); + }); + it("should access the db property of indexedDB", async () => { + store._initIndexedDB.restore(); + await store.init(new Map()); + + assert.calledOnce(dbStub); + }); + it("should not initialize the feed if the Pref is set to false", async () => { sinon.stub(store, "initFeed"); store._prefs.set("foo", false); - store.init(new Map([["foo", () => {}]])); + await store.init(new Map([["foo", () => {}]])); assert.notCalled(store.initFeed); }); - it("should observe the pref branch", () => { + it("should observe the pref branch", async () => { sinon.stub(store._prefs, "observeBranch"); - store.init(new Map()); + await store.init(new Map()); assert.calledOnce(store._prefs.observeBranch); assert.calledWith(store._prefs.observeBranch, store); }); - it("should initialize the ActivityStreamMessageChannel channel", () => { - store.init(new Map()); + it("should initialize the ActivityStreamMessageChannel channel", async () => { + await store.init(new Map()); assert.calledOnce(store._messageChannel.createChannel); }); - it("should emit an initial event if provided", () => { + it("should emit an initial event if provided", async () => { sinon.stub(store, "dispatch"); const action = {type: "FOO"}; - store.init(new Map(), action); + await store.init(new Map(), action); assert.calledOnce(store.dispatch); assert.calledWith(store.dispatch, action); @@ -175,17 +195,17 @@ describe("Store", () => { store.init(feedFactories); assert.ok(telemetrySpy.calledBefore(fooSpy)); }); - it("should dispatch init/load events", () => { - store.init(new Map(), {type: "FOO"}); + it("should dispatch init/load events", async () => { + await store.init(new Map(), {type: "FOO"}); assert.calledOnce(store._messageChannel.simulateMessagesForExistingTabs); }); - it("should dispatch INIT before LOAD", () => { + it("should dispatch INIT before LOAD", async () => { const init = {type: "INIT"}; const load = {type: "TAB_LOAD"}; sandbox.stub(store, "dispatch"); store._messageChannel.simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load)); - store.init(new Map(), init); + await store.init(new Map(), init); assert.calledTwice(store.dispatch); assert.equal(store.dispatch.firstCall.args[0], init); @@ -229,13 +249,13 @@ describe("Store", () => { }); }); describe("#dispatch", () => { - it("should call .onAction of each feed", () => { + it("should call .onAction of each feed", async () => { const {dispatch} = store; const sub = {onAction: sinon.spy()}; const action = {type: "FOO"}; store._prefs.set("sub", true); - store.init(new Map([["sub", () => sub]])); + await store.init(new Map([["sub", () => sub]])); dispatch(action); diff --git a/system-addon/test/unit/lib/TopSitesFeed.test.js b/system-addon/test/unit/lib/TopSitesFeed.test.js index c393b6a33..973dde95c 100644 --- a/system-addon/test/unit/lib/TopSitesFeed.test.js +++ b/system-addon/test/unit/lib/TopSitesFeed.test.js @@ -85,18 +85,21 @@ describe("Top Sites Feed", () => { "lib/ActivityStreamStorage.jsm": {ActivityStreamStorage: function Fake() {}, getDefaultOptions} })); feed = new TopSitesFeed(); - feed._storage = { - init: sandbox.stub().returns(Promise.resolve()), - get: sandbox.stub().returns(Promise.resolve()), - set: sandbox.stub().returns(Promise.resolve()) + const storage = { + init: sandbox.stub().resolves(), + get: sandbox.stub().resolves(), + set: sandbox.stub().resolves() }; + // Setup for tests that don't call `init` but require feed.storage + feed._storage = storage; feed.store = { dispatch: sinon.spy(), getState() { return this.state; }, state: { Prefs: {values: {filterAdult: false, topSitesRows: 2}}, TopSites: {rows: Array(12).fill("site")} - } + }, + dbStorage: {getDbTable: sandbox.stub().returns(storage)} }; feed.dedupe.group = (...sites) => sites; links = FAKE_LINKS; @@ -441,15 +444,33 @@ describe("Top Sites Feed", () => { assert.calledWith(feed._fetchScreenshot, sinon.match.object, "custom"); }); }); + describe("#init", () => { + it("should call refresh (broadcast:true)", async () => { + sandbox.stub(feed, "refresh"); + + await feed.init(); + + assert.calledOnce(feed.refresh); + assert.calledWithExactly(feed.refresh, {broadcast: true}); + }); + it("should initialise _tippyTopProvider", async () => { + feed._tippyTopProvider.initialized = false; + + await feed.init(); + + assert.isTrue(feed._tippyTopProvider.initialized); + }); + it("should initialise the storage", async () => { + await feed.init(); + + assert.calledOnce(feed.store.dbStorage.getDbTable); + assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs"); + }); + }); describe("#refresh", () => { beforeEach(() => { sandbox.stub(feed, "_fetchIcon"); }); - it("should initialise _tippyTopProvider if it's not already initialised", async () => { - feed._tippyTopProvider.initialized = false; - await feed.refresh({broadcast: true}); - assert.ok(feed._tippyTopProvider.initialized); - }); it("should broadcast TOP_SITES_UPDATED", async () => { sinon.stub(feed, "getLinksWithDefaults").returns(Promise.resolve([])); @@ -492,6 +513,18 @@ describe("Top Sites Feed", () => { assert.notCalled(feed._storage.init); }); + it("should catch indexedDB errors", async () => { + feed._storage.get.throws(new Error()); + globals.sandbox.spy(global.Cu, "reportError"); + + try { + await feed.refresh({broadcast: false}); + } catch (e) { + assert.fails(); + } + + assert.calledOnce(Cu.reportError); + }); }); describe("#updateSectionPrefs", () => { it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => { @@ -701,10 +734,10 @@ describe("Top Sites Feed", () => { assert.calledOnce(feed.store.dispatch); assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED); }); - it("should call refresh on INIT action", async () => { - sinon.stub(feed, "refresh"); - await feed.onAction({type: at.INIT}); - assert.calledOnce(feed.refresh); + it("should call init on INIT action", async () => { + sinon.stub(feed, "init"); + feed.onAction({type: at.INIT}); + assert.calledOnce(feed.init); }); it("should call refresh on MIGRATION_COMPLETED action", async () => { sinon.stub(feed, "refresh");