Source code

Revision control

Copy as Markdown

Other Tools

import { actionTypes as at } from "common/Actions.mjs";
import { GlobalOverrider } from "test/unit/utils";
import { WebNotificationsFeed } from "lib/WebNotificationsFeed.sys.mjs";
describe("WebNotificationsFeed", () => {
let feed;
let overrider;
let sandbox;
let readUTF8Stub;
let pathJoinSpy;
function buildStoreText(entries) {
// entries: [{ origin, id, tag?, title? }]
const store = {};
for (const e of entries) {
if (!store[e.origin]) {
store[e.origin] = {};
}
store[e.origin][e.id] = {
id: e.id,
title: e.title ?? "t",
body: e.body ?? "b",
tag: e.tag ?? "",
};
}
return JSON.stringify(store);
}
function makeNotFoundError() {
const err = new Error("file not found");
err.name = "NotFoundError";
return err;
}
beforeEach(() => {
sandbox = sinon.createSandbox();
overrider = new GlobalOverrider();
readUTF8Stub = sandbox.stub().resolves("");
pathJoinSpy = sandbox.spy((...parts) => parts.join("/"));
overrider.set({
IOUtils: { readUTF8: readUTF8Stub },
PathUtils: {
profileDir: "/tmp/test-profile",
join: pathJoinSpy,
},
// Production code uses chrome's DOMException.isInstance; in the karma
// env there's only the standard DOM constructor, so we stub the static
// helper to recognize anything with a `.name` of "NotFoundError".
DOMException: {
isInstance(e) {
return !!e && typeof e === "object" && "name" in e;
},
},
});
feed = new WebNotificationsFeed();
feed.store = { dispatch: sandbox.spy() };
});
afterEach(() => {
overrider.restore();
sandbox.restore();
});
it("reads from notificationstore.json in the profile dir", async () => {
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(pathJoinSpy);
assert.calledWith(
pathJoinSpy,
"/tmp/test-profile",
"notificationstore.json"
);
assert.calledOnce(readUTF8Stub);
});
it("dispatches WEB_NOTIFICATIONS_UPDATED on INIT with a normalized payload", async () => {
readUTF8Stub.resolves(
buildStoreText([
{ origin: "https://example.com", id: "abc", tag: "inbox" },
{ origin: "https://example.com", id: "def" },
{ origin: "https://other.org", id: "ghi", tag: "ping" },
])
);
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_UPDATED);
assert.isObject(action.meta);
assert.equal(action.meta.to, "ActivityStream:Content");
assert.isUndefined(action.meta.toTarget);
const { data } = action;
assert.isNumber(data.lastUpdated);
assert.sameMembers(Object.keys(data.notifications), ["abc", "def", "ghi"]);
assert.equal(data.notifications.abc.origin, "https://example.com");
assert.equal(data.notifications.abc.tag, "inbox");
assert.isUndefined(data.notifications.abc.dataSerialized);
assert.deepEqual(data.byOrigin["https://example.com"].sort(), [
"abc",
"def",
]);
assert.deepEqual(data.byOrigin["https://other.org"], ["ghi"]);
});
it("answers NEW_TAB_LOAD with a snapshot targeted at the loading tab", async () => {
feed.onAction({ type: at.NEW_TAB_LOAD, meta: { fromTarget: "port-123" } });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(readUTF8Stub);
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_UPDATED);
assert.equal(action.meta.to, "ActivityStream:Content");
assert.equal(action.meta.toTarget, "port-123");
});
it("answers WEB_NOTIFICATIONS_REQUEST with a snapshot targeted at the requesting tab", async () => {
feed.onAction({
type: at.WEB_NOTIFICATIONS_REQUEST,
meta: { fromTarget: "port-abc" },
});
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(readUTF8Stub);
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_UPDATED);
assert.equal(action.meta.toTarget, "port-abc");
});
it("produces an empty snapshot when the file is empty", async () => {
readUTF8Stub.resolves("");
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_UPDATED);
assert.deepEqual(action.data.notifications, {});
assert.deepEqual(action.data.byOrigin, {});
});
it("treats a missing file as an empty snapshot (first-run profile)", async () => {
readUTF8Stub.rejects(makeNotFoundError());
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_UPDATED);
assert.deepEqual(action.data.notifications, {});
assert.deepEqual(action.data.byOrigin, {});
});
it("dispatches WEB_NOTIFICATIONS_ERROR when the read fails with a non-NotFound error", async () => {
readUTF8Stub.rejects(new Error("boom"));
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_ERROR);
assert.include(action.data.message, "boom");
});
it("dispatches WEB_NOTIFICATIONS_ERROR when the JSON is malformed", async () => {
readUTF8Stub.resolves("not valid json {{{");
feed.onAction({ type: at.INIT });
await Promise.resolve();
await Promise.resolve();
assert.calledOnce(feed.store.dispatch);
const [action] = feed.store.dispatch.firstCall.args;
assert.equal(action.type, at.WEB_NOTIFICATIONS_ERROR);
assert.isString(action.data.message);
});
it("ignores unrelated actions", () => {
feed.onAction({ type: at.UNINIT });
feed.onAction({ type: at.SYSTEM_TICK });
assert.notCalled(feed.store.dispatch);
assert.notCalled(readUTF8Stub);
});
});