Fix Bug 1449223 - Add Telemetry for failed IndexedDB transactions

This commit is contained in:
Andrei Oprea 2018-04-20 18:18:44 +02:00 коммит произвёл GitHub
Родитель e87a190e15
Коммит 50fa46b446
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 360 добавлений и 187 удалений

Просмотреть файл

@ -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,

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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();

Просмотреть файл

@ -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.

Просмотреть файл

@ -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});

Просмотреть файл

@ -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();

Просмотреть файл

@ -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);
}
});
});
});

Просмотреть файл

@ -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);

Просмотреть файл

@ -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");

Просмотреть файл

@ -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());
});
});

Просмотреть файл

@ -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);

Просмотреть файл

@ -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");