Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
import { actionTypes as at } from "common/Actions.mjs";
const {
TopSites,
App,
Prefs,
Dialog,
Sections,
Pocket,
DiscoveryStream,
Search,
WebNotifications,
ExternalComponents,
SportsWidget,
PictureOfTheDay,
} = reducers;
describe("Reducers", () => {
describe("App", () => {
it("should return the initial state", () => {
const nextState = App(undefined, { type: "FOO" });
expect(nextState).toBe(INITIAL_STATE.App);
});
it("should set initialized to true on INIT", () => {
const nextState = App(undefined, { type: "INIT" });
expect(nextState.initialized).toBe(true);
});
it("should show the customize panel on SHOW_PERSONALIZE", () => {
const nextState = App(undefined, { type: at.SHOW_PERSONALIZE });
expect(nextState.customizeMenuVisible).toBe(true);
expect(nextState.customizePanelWallpaperCategory).toBeNull();
});
it("should store the deep-linked wallpaper category on SHOW_PERSONALIZE", () => {
const nextState = App(undefined, {
type: at.SHOW_PERSONALIZE,
data: { wallpaperCategory: "firefox" },
});
expect(nextState.customizePanelWallpaperCategory).toBe("firefox");
});
it("should clear customize panel state on HIDE_PERSONALIZE", () => {
const nextState = App(
{
customizeMenuVisible: true,
customizePanelWallpaperCategory: "firefox",
},
{ type: at.HIDE_PERSONALIZE }
);
expect(nextState.customizeMenuVisible).toBe(false);
expect(nextState.customizePanelWallpaperCategory).toBeNull();
});
});
describe("TopSites", () => {
it("should return the initial state", () => {
const nextState = TopSites(undefined, { type: "FOO" });
expect(nextState).toBe(INITIAL_STATE.TopSites);
});
it("should add top sites on TOP_SITES_UPDATED", () => {
const newRows = [{ url: "foo.com" }, { url: "bar.com" }];
const nextState = TopSites(undefined, {
type: at.TOP_SITES_UPDATED,
data: { links: newRows },
});
expect(nextState.rows).toBe(newRows);
});
it("should not update state for empty action.data on TOP_SITES_UPDATED", () => {
const nextState = TopSites(undefined, { type: at.TOP_SITES_UPDATED });
expect(nextState).toBe(INITIAL_STATE.TopSites);
});
it("should initialize prefs on TOP_SITES_UPDATED", () => {
const nextState = TopSites(undefined, {
type: at.TOP_SITES_UPDATED,
data: { links: [], pref: "foo" },
});
expect(nextState.pref).toBe("foo");
});
it("should pass prevState.prefs if not present in TOP_SITES_UPDATED", () => {
const nextState = TopSites(
{ prefs: "foo" },
{ type: at.TOP_SITES_UPDATED, data: { links: [] } }
);
expect(nextState.prefs).toBe("foo");
});
it("should set editForm.site to action.data on TOP_SITES_EDIT", () => {
const data = { index: 7 };
const nextState = TopSites(undefined, { type: at.TOP_SITES_EDIT, data });
expect(nextState.editForm.index).toBe(data.index);
});
it("should set editForm to null on TOP_SITES_CANCEL_EDIT", () => {
const nextState = TopSites(undefined, { type: at.TOP_SITES_CANCEL_EDIT });
expect(nextState.editForm).toBeNull();
});
it("should preserve the editForm.index", () => {
const actionTypes = [
at.PREVIEW_RESPONSE,
at.PREVIEW_REQUEST,
at.PREVIEW_REQUEST_CANCEL,
];
actionTypes.forEach(type => {
const oldState = { editForm: { index: 0, previewUrl: "foo" } };
const action = { type, data: { url: "foo" } };
const nextState = TopSites(oldState, action);
expect(nextState.editForm.index).toBe(0);
});
});
it("should set previewResponse on PREVIEW_RESPONSE", () => {
const oldState = { editForm: { previewUrl: "url" } };
const action = {
type: at.PREVIEW_RESPONSE,
data: { preview: "data:123", url: "url" },
};
const nextState = TopSites(oldState, action);
expect(nextState.editForm.previewResponse).toBe("data:123");
});
it("should return previous state if action url does not match expected", () => {
const oldState = { editForm: { previewUrl: "foo" } };
const action = { type: at.PREVIEW_RESPONSE, data: { url: "bar" } };
const nextState = TopSites(oldState, action);
expect(nextState).toBe(oldState);
});
it("should return previous state if editForm is not set", () => {
const actionTypes = [
at.PREVIEW_RESPONSE,
at.PREVIEW_REQUEST,
at.PREVIEW_REQUEST_CANCEL,
];
actionTypes.forEach(type => {
const oldState = { editForm: null };
const action = { type, data: { url: "bar" } };
const nextState = TopSites(oldState, action);
expect(nextState).toBe(oldState);
});
});
it("should set previewResponse to null on PREVIEW_REQUEST", () => {
const oldState = { editForm: { previewResponse: "foo" } };
const action = { type: at.PREVIEW_REQUEST, data: {} };
const nextState = TopSites(oldState, action);
expect(nextState.editForm.previewResponse).toBeNull();
});
it("should set previewUrl on PREVIEW_REQUEST", () => {
const oldState = { editForm: {} };
const action = { type: at.PREVIEW_REQUEST, data: { url: "bar" } };
const nextState = TopSites(oldState, action);
expect(nextState.editForm.previewUrl).toBe("bar");
});
it("should add screenshots for SCREENSHOT_UPDATED", () => {
const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] };
const action = {
type: at.SCREENSHOT_UPDATED,
data: { url: "bar.com", screenshot: "data:123" },
};
const nextState = TopSites(oldState, action);
expect(nextState.rows).toEqual([
{ url: "foo.com" },
{ url: "bar.com", screenshot: "data:123" },
]);
});
it("should not modify rows if nothing matches the url for SCREENSHOT_UPDATED", () => {
const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] };
const action = {
type: at.SCREENSHOT_UPDATED,
data: { url: "baz.com", screenshot: "data:123" },
};
const nextState = TopSites(oldState, action);
expect(nextState).toEqual(oldState);
});
it("should bookmark an item on PLACES_BOOKMARK_ADDED", () => {
const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] };
const action = {
type: at.PLACES_BOOKMARK_ADDED,
data: {
url: "bar.com",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
dateAdded: 1234567,
},
};
const nextState = TopSites(oldState, action);
const [, newRow] = nextState.rows;
// new row has bookmark data
expect(newRow.url).toBe(action.data.url);
expect(newRow.bookmarkGuid).toBe(action.data.bookmarkGuid);
expect(newRow.bookmarkTitle).toBe(action.data.bookmarkTitle);
expect(newRow.bookmarkDateCreated).toBe(action.data.dateAdded);
// old row is unchanged
expect(nextState.rows[0]).toBe(oldState.rows[0]);
});
it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => {
const nextState = TopSites(undefined, { type: at.PLACES_BOOKMARK_ADDED });
expect(nextState).toBe(INITIAL_STATE.TopSites);
});
it("should remove a bookmark on PLACES_BOOKMARKS_REMOVED", () => {
const oldState = {
rows: [
{ url: "foo.com" },
{
url: "bar.com",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
dateAdded: 123456,
},
],
};
const action = {
type: at.PLACES_BOOKMARKS_REMOVED,
data: { urls: ["bar.com"] },
};
const nextState = TopSites(oldState, action);
const [, newRow] = nextState.rows;
// new row no longer has bookmark data
expect(newRow.url).toBe(oldState.rows[1].url);
expect(newRow.bookmarkGuid).toBeUndefined();
expect(newRow.bookmarkTitle).toBeUndefined();
expect(newRow.bookmarkDateCreated).toBeUndefined();
// old row is unchanged
expect(nextState.rows[0]).toEqual(oldState.rows[0]);
});
it("should not update state for empty action.data on PLACES_BOOKMARKS_REMOVED", () => {
const nextState = TopSites(undefined, {
type: at.PLACES_BOOKMARKS_REMOVED,
});
expect(nextState).toBe(INITIAL_STATE.TopSites);
});
it("should update prefs on TOP_SITES_PREFS_UPDATED", () => {
const state = TopSites(
{},
{ type: at.TOP_SITES_PREFS_UPDATED, data: { pref: "foo" } }
);
expect(state.pref).toBe("foo");
});
it("should not update state for empty action.data on PLACES_LINKS_DELETED", () => {
const nextState = TopSites(undefined, { type: at.PLACES_LINKS_DELETED });
expect(nextState).toBe(INITIAL_STATE.TopSites);
});
it("should remove the site on PLACES_LINKS_DELETED", () => {
const oldState = { rows: [{ url: "foo.com" }, { url: "bar.com" }] };
const deleteAction = {
type: at.PLACES_LINKS_DELETED,
data: { urls: ["foo.com"] },
};
const nextState = TopSites(oldState, deleteAction);
expect(nextState.rows).toEqual([{ url: "bar.com" }]);
});
it("should set showSearchShortcutsForm to true on TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", () => {
const data = { index: 7 };
const nextState = TopSites(undefined, {
type: at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL,
data,
});
expect(nextState.showSearchShortcutsForm).toBe(true);
});
it("should set showSearchShortcutsForm to false on TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", () => {
const nextState = TopSites(undefined, {
type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL,
});
expect(nextState.showSearchShortcutsForm).toBe(false);
});
it("should update searchShortcuts on UPDATE_SEARCH_SHORTCUTS", () => {
const shortcuts = [
{
keyword: "@google",
shortURL: "google",
searchIdentifier: /^google/,
},
{
keyword: "@baidu",
shortURL: "baidu",
searchIdentifier: /^baidu/,
},
];
const nextState = TopSites(undefined, {
type: at.UPDATE_SEARCH_SHORTCUTS,
data: { searchShortcuts: shortcuts },
});
expect(nextState.searchShortcuts).toEqual(shortcuts);
});
it("should set sov positions and state", () => {
const positions = [
{ position: 0, assignedPartner: "amp" },
{ position: 1, assignedPartner: "moz-sales" },
];
const nextState = TopSites(undefined, {
type: at.SOV_UPDATED,
data: { ready: true, positions },
});
expect(nextState.sov.ready).toBe(true);
expect(nextState.sov.positions).toBe(positions);
});
});
describe("Prefs", () => {
function prevState(custom = {}) {
return Object.assign({}, INITIAL_STATE.Prefs, custom);
}
it("should have the correct initial state", () => {
const state = Prefs(undefined, {});
expect(state).toEqual(INITIAL_STATE.Prefs);
});
describe("PREFS_INITIAL_VALUES", () => {
it("should return a new object", () => {
const state = Prefs(undefined, {
type: at.PREFS_INITIAL_VALUES,
data: {},
});
expect(state).not.toBe(INITIAL_STATE.Prefs);
});
it("should set initalized to true", () => {
const state = Prefs(undefined, {
type: at.PREFS_INITIAL_VALUES,
data: {},
});
expect(state.initialized).toBe(true);
});
it("should set .values", () => {
const newValues = { foo: 1, bar: 2 };
const state = Prefs(undefined, {
type: at.PREFS_INITIAL_VALUES,
data: newValues,
});
expect(state.values).toBe(newValues);
});
});
describe("PREF_CHANGED", () => {
it("should return a new Prefs object", () => {
const state = Prefs(undefined, {
type: at.PREF_CHANGED,
data: { name: "foo", value: 2 },
});
expect(state).not.toBe(INITIAL_STATE.Prefs);
});
it("should set the changed pref", () => {
const state = Prefs(prevState({ foo: 1 }), {
type: at.PREF_CHANGED,
data: { name: "foo", value: 2 },
});
expect(state.values.foo).toBe(2);
});
it("should return a new .pref object instead of mutating", () => {
const oldState = prevState({ foo: 1 });
const state = Prefs(oldState, {
type: at.PREF_CHANGED,
data: { name: "foo", value: 2 },
});
expect(state.values).not.toBe(oldState.values);
});
});
describe("MULTIPLE_PREFS_CHANGED", () => {
it("should merge multiple values in one pass and keep untouched keys", () => {
const oldState = { ...INITIAL_STATE.Prefs, values: { foo: 1, bar: 2 } };
const state = Prefs(oldState, {
type: at.MULTIPLE_PREFS_CHANGED,
data: { values: { foo: 3, baz: 4 } },
});
expect(state.values.foo).toBe(3);
expect(state.values.bar).toBe(2);
expect(state.values.baz).toBe(4);
});
it("should return a new .values object instead of mutating", () => {
const oldState = { ...INITIAL_STATE.Prefs, values: { foo: 1 } };
const state = Prefs(oldState, {
type: at.MULTIPLE_PREFS_CHANGED,
data: { values: { foo: 2 } },
});
expect(state.values).not.toBe(oldState.values);
});
});
});
describe("Dialog", () => {
it("should return INITIAL_STATE by default", () => {
expect(Dialog(undefined, { type: "non_existent" })).toBe(
INITIAL_STATE.Dialog
);
});
it("should toggle visible to true on DIALOG_OPEN", () => {
const action = { type: at.DIALOG_OPEN };
const nextState = Dialog(INITIAL_STATE.Dialog, action);
expect(nextState.visible).toBe(true);
});
it("should pass url data on DIALOG_OPEN", () => {
const action = { type: at.DIALOG_OPEN, data: "some url" };
const nextState = Dialog(INITIAL_STATE.Dialog, action);
expect(nextState.data).toBe(action.data);
});
it("should toggle visible to false on DIALOG_CANCEL", () => {
const action = { type: at.DIALOG_CANCEL, data: "some url" };
const nextState = Dialog(INITIAL_STATE.Dialog, action);
expect(nextState.visible).toBe(false);
});
it("should return inital state on DELETE_HISTORY_URL", () => {
const action = { type: at.DELETE_HISTORY_URL };
const nextState = Dialog(INITIAL_STATE.Dialog, action);
expect(nextState).toEqual(INITIAL_STATE.Dialog);
});
});
describe("Sections", () => {
let oldState;
beforeEach(() => {
oldState = new Array(5).fill(null).map((v, i) => ({
id: `foo_bar_${i}`,
title: `Foo Bar ${i}`,
initialized: false,
rows: [
{ url: "www.foo.bar", pocket_id: 123 },
{ url: "www.other.url" },
],
order: i,
type: "history",
}));
});
it("should return INITIAL_STATE by default", () => {
expect(Sections(undefined, { type: "non_existent" })).toBe(
INITIAL_STATE.Sections
);
});
it("should remove the correct section on SECTION_DEREGISTER", () => {
const newState = Sections(oldState, {
type: at.SECTION_DEREGISTER,
data: "foo_bar_2",
});
expect(newState).toHaveLength(4);
const expectedNewState = oldState.splice(2, 1) && oldState;
expect(newState).toEqual(expectedNewState);
});
it("should add a section on SECTION_REGISTER if it doesn't already exist", () => {
const action = {
type: at.SECTION_REGISTER,
data: { id: "foo_bar_5", title: "Foo Bar 5" },
};
const newState = Sections(oldState, action);
expect(newState).toHaveLength(6);
const insertedSection = newState.find(
section => section.id === "foo_bar_5"
);
expect(insertedSection.title).toBe(action.data.title);
});
it("should set newSection.rows === [] if no rows are provided on SECTION_REGISTER", () => {
const action = {
type: at.SECTION_REGISTER,
data: { id: "foo_bar_5", title: "Foo Bar 5" },
};
const newState = Sections(oldState, action);
const insertedSection = newState.find(
section => section.id === "foo_bar_5"
);
expect(insertedSection.rows).toEqual([]);
});
it("should update a section on SECTION_REGISTER if it already exists", () => {
const NEW_TITLE = "New Title";
const action = {
type: at.SECTION_REGISTER,
data: { id: "foo_bar_2", title: NEW_TITLE },
};
const newState = Sections(oldState, action);
expect(newState).toHaveLength(5);
const updatedSection = newState.find(
section => section.id === "foo_bar_2"
);
expect(updatedSection && updatedSection.title === NEW_TITLE).toBeTruthy();
});
it("should set initialized to false on SECTION_REGISTER if there are no rows", () => {
const NEW_TITLE = "New Title";
const action = {
type: at.SECTION_REGISTER,
data: { id: "bloop", title: NEW_TITLE },
};
const newState = Sections(oldState, action);
const updatedSection = newState.find(section => section.id === "bloop");
expect(updatedSection.initialized).toBe(false);
});
it("should set initialized to true on SECTION_REGISTER if there are rows", () => {
const NEW_TITLE = "New Title";
const action = {
type: at.SECTION_REGISTER,
data: { id: "bloop", title: NEW_TITLE, rows: [{}, {}] },
};
const newState = Sections(oldState, action);
const updatedSection = newState.find(section => section.id === "bloop");
expect(updatedSection.initialized).toBe(true);
});
it("should have no effect on SECTION_UPDATE if the id doesn't exist", () => {
const action = {
type: at.SECTION_UPDATE,
data: { id: "fake_id", data: "fake_data" },
};
const newState = Sections(oldState, action);
expect(newState).toEqual(oldState);
});
it("should update the section with the correct data on SECTION_UPDATE", () => {
const FAKE_DATA = { rows: ["some", "fake", "data"], foo: "bar" };
const action = {
type: at.SECTION_UPDATE,
data: Object.assign(FAKE_DATA, { id: "foo_bar_2" }),
};
const newState = Sections(oldState, action);
const updatedSection = newState.find(
section => section.id === "foo_bar_2"
);
expect(updatedSection).toMatchObject(FAKE_DATA);
});
it("should set initialized to true on SECTION_UPDATE if rows is defined on action.data", () => {
const data = { rows: [], id: "foo_bar_2" };
const action = { type: at.SECTION_UPDATE, data };
const newState = Sections(oldState, action);
const updatedSection = newState.find(
section => section.id === "foo_bar_2"
);
expect(updatedSection.initialized).toBe(true);
});
it("should retain pinned cards on SECTION_UPDATE", () => {
const ROW = { id: "row" };
let newState = Sections(oldState, {
type: at.SECTION_UPDATE,
data: Object.assign({ rows: [ROW] }, { id: "foo_bar_2" }),
});
let updatedSection = newState.find(section => section.id === "foo_bar_2");
expect(updatedSection.rows).toEqual([ROW]);
const PINNED_ROW = { id: "pinned", pinned: true, guid: "pinned" };
newState = Sections(newState, {
type: at.SECTION_UPDATE,
data: Object.assign({ rows: [PINNED_ROW] }, { id: "foo_bar_2" }),
});
updatedSection = newState.find(section => section.id === "foo_bar_2");
expect(updatedSection.rows).toEqual([PINNED_ROW]);
// Updating the section again should not duplicate pinned cards
newState = Sections(newState, {
type: at.SECTION_UPDATE,
data: Object.assign({ rows: [PINNED_ROW] }, { id: "foo_bar_2" }),
});
updatedSection = newState.find(section => section.id === "foo_bar_2");
expect(updatedSection.rows).toEqual([PINNED_ROW]);
// Updating the section should retain pinned card at its index
newState = Sections(newState, {
type: at.SECTION_UPDATE,
data: Object.assign({ rows: [ROW] }, { id: "foo_bar_2" }),
});
updatedSection = newState.find(section => section.id === "foo_bar_2");
expect(updatedSection.rows).toEqual([PINNED_ROW, ROW]);
// Clearing/Resetting the section should clear pinned cards
newState = Sections(newState, {
type: at.SECTION_UPDATE,
data: Object.assign({ rows: [] }, { id: "foo_bar_2" }),
});
updatedSection = newState.find(section => section.id === "foo_bar_2");
expect(updatedSection.rows).toEqual([]);
});
it("should have no effect on SECTION_UPDATE_CARD if the id or url doesn't exist", () => {
const noIdAction = {
type: at.SECTION_UPDATE_CARD,
data: {
id: "non-existent",
url: "www.foo.bar",
options: { title: "New title" },
},
};
const noIdState = Sections(oldState, noIdAction);
const noUrlAction = {
type: at.SECTION_UPDATE_CARD,
data: {
id: "foo_bar_2",
url: "www.non-existent.url",
options: { title: "New title" },
},
};
const noUrlState = Sections(oldState, noUrlAction);
expect(noIdState).toEqual(oldState);
expect(noUrlState).toEqual(oldState);
});
it("should update the card with the correct data on SECTION_UPDATE_CARD", () => {
const action = {
type: at.SECTION_UPDATE_CARD,
data: {
id: "foo_bar_2",
url: "www.other.url",
options: { title: "Fake new title" },
},
};
const newState = Sections(oldState, action);
const updatedSection = newState.find(
section => section.id === "foo_bar_2"
);
const updatedCard = updatedSection.rows.find(
card => card.url === "www.other.url"
);
expect(updatedCard.title).toBe("Fake new title");
});
it("should only update the cards belonging to the right section on SECTION_UPDATE_CARD", () => {
const action = {
type: at.SECTION_UPDATE_CARD,
data: {
id: "foo_bar_2",
url: "www.other.url",
options: { title: "Fake new title" },
},
};
const newState = Sections(oldState, action);
newState.forEach((section, i) => {
if (section.id !== "foo_bar_2") {
expect(section).toEqual(oldState[i]);
}
});
});
it("should allow action.data to set .initialized", () => {
const data = { rows: [], initialized: false, id: "foo_bar_2" };
const action = { type: at.SECTION_UPDATE, data };
const newState = Sections(oldState, action);
const updatedSection = newState.find(
section => section.id === "foo_bar_2"
);
expect(updatedSection.initialized).toBe(false);
});
it("should dedupe based on dedupeConfigurations", () => {
const site = { url: "foo.com" };
const highlights = { rows: [site], id: "highlights" };
const topstories = { rows: [site], id: "topstories" };
const dedupeConfigurations = [
{ id: "topstories", dedupeFrom: ["highlights"] },
];
const action = { data: { dedupeConfigurations }, type: "SECTION_UPDATE" };
const state = [highlights, topstories];
const nextState = Sections(state, action);
expect(nextState.find(s => s.id === "highlights").rows).toHaveLength(1);
expect(nextState.find(s => s.id === "topstories").rows).toHaveLength(0);
});
it("should remove blocked and deleted urls from all rows in all sections", () => {
const blockAction = {
type: at.PLACES_LINK_BLOCKED,
data: { url: "www.foo.bar" },
};
const deleteAction = {
type: at.PLACES_LINKS_DELETED,
data: { urls: ["www.foo.bar"] },
};
const newBlockState = Sections(oldState, blockAction);
const newDeleteState = Sections(oldState, deleteAction);
newBlockState.concat(newDeleteState).forEach(section => {
expect(section.rows).toEqual([{ url: "www.other.url" }]);
});
});
it("should not update state for empty action.data on PLACES_LINK_BLOCKED", () => {
const nextState = Sections(undefined, { type: at.PLACES_LINK_BLOCKED });
expect(nextState).toBe(INITIAL_STATE.Sections);
});
it("should not update state for empty action.data on PLACES_LINKS_DELETED", () => {
const nextState = Sections(undefined, { type: at.PLACES_LINKS_DELETED });
expect(nextState).toBe(INITIAL_STATE.Sections);
});
it("should not update state for empty action.data on PLACES_BOOKMARK_ADDED", () => {
const nextState = Sections(undefined, { type: at.PLACES_BOOKMARK_ADDED });
expect(nextState).toBe(INITIAL_STATE.Sections);
});
it("should bookmark an item when PLACES_BOOKMARK_ADDED is received", () => {
const action = {
type: at.PLACES_BOOKMARK_ADDED,
data: {
url: "www.foo.bar",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
dateAdded: 1234567,
},
};
const nextState = Sections(oldState, action);
// check a section to ensure the correct url was bookmarked
const [newRow, oldRow] = nextState[0].rows;
// new row has bookmark data
expect(newRow.url).toBe(action.data.url);
expect(newRow.type).toBe("bookmark");
expect(newRow.bookmarkGuid).toBe(action.data.bookmarkGuid);
expect(newRow.bookmarkTitle).toBe(action.data.bookmarkTitle);
expect(newRow.bookmarkDateCreated).toBe(action.data.dateAdded);
// old row is unchanged
expect(oldRow).toBe(oldState[0].rows[1]);
});
it("should not update state for empty action.data on PLACES_BOOKMARKS_REMOVED", () => {
const nextState = Sections(undefined, {
type: at.PLACES_BOOKMARKS_REMOVED,
});
expect(nextState).toBe(INITIAL_STATE.Sections);
});
it("should remove the bookmark when PLACES_BOOKMARKS_REMOVED is received", () => {
const action = {
type: at.PLACES_BOOKMARKS_REMOVED,
data: {
urls: ["www.foo.bar"],
bookmarkGuid: "bookmark123",
},
};
// add some bookmark data for the first url in rows
oldState.forEach(item => {
item.rows[0].bookmarkGuid = "bookmark123";
item.rows[0].bookmarkTitle = "Title for bar.com";
item.rows[0].bookmarkDateCreated = 1234567;
item.rows[0].type = "bookmark";
});
const nextState = Sections(oldState, action);
// check a section to ensure the correct bookmark was removed
const [newRow, oldRow] = nextState[0].rows;
// new row isn't a bookmark
expect(newRow.url).toBe(action.data.urls[0]);
expect(newRow.type).toBe("history");
expect(newRow.bookmarkGuid).toBeUndefined();
expect(newRow.bookmarkTitle).toBeUndefined();
expect(newRow.bookmarkDateCreated).toBeUndefined();
// old row is unchanged
expect(oldRow).toBe(oldState[0].rows[1]);
});
});
describe("Pocket", () => {
it("should return INITIAL_STATE by default", () => {
expect(Pocket(undefined, { type: "some_action" })).toBe(
INITIAL_STATE.Pocket
);
});
it("should set waitingForSpoc on a POCKET_WAITING_FOR_SPOC action", () => {
const state = Pocket(undefined, {
type: at.POCKET_WAITING_FOR_SPOC,
data: false,
});
expect(state.waitingForSpoc).toBe(false);
});
it("should set pocketCta with correct object on a POCKET_CTA", () => {
const data = {
cta_button: "cta button",
cta_text: "cta text",
cta_url: "https://cta-url.com",
use_cta: true,
};
const state = Pocket(undefined, { type: at.POCKET_CTA, data });
expect(state.pocketCta.ctaButton).toBe(data.cta_button);
expect(state.pocketCta.ctaText).toBe(data.cta_text);
expect(state.pocketCta.ctaUrl).toBe(data.cta_url);
expect(state.pocketCta.useCta).toBe(data.use_cta);
});
});
describe("DiscoveryStream", () => {
it("should return INITIAL_STATE by default", () => {
expect(DiscoveryStream(undefined, { type: "some_action" })).toBe(
INITIAL_STATE.DiscoveryStream
);
});
it("should set layout data with DISCOVERY_STREAM_LAYOUT_UPDATE", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: { layout: ["test"] },
});
expect(state.layout[0]).toBe("test");
});
it("should reset layout data with DISCOVERY_STREAM_LAYOUT_RESET", () => {
const layoutData = { layout: ["test"], lastUpdated: 123 };
const feedsData = {
"https://foo.com/feed1": { lastUpdated: 123, data: [1, 2, 3] },
};
const spocsData = {
lastUpdated: 123,
spocs: [1, 2, 3],
};
let state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,
data: layoutData,
});
state = DiscoveryStream(state, {
type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
data: feedsData,
});
state = DiscoveryStream(state, {
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data: spocsData,
});
state = DiscoveryStream(state, {
type: at.DISCOVERY_STREAM_LAYOUT_RESET,
});
expect(state).toEqual(INITIAL_STATE.DiscoveryStream);
});
it("should preserve sectionPersonalization with DISCOVERY_STREAM_LAYOUT_RESET", () => {
const personalization = {
sports: { isBlocked: true, isFollowed: false, title: "Sports" },
};
let state = DiscoveryStream(undefined, {
type: at.SECTION_PERSONALIZATION_UPDATE,
data: personalization,
});
state = DiscoveryStream(state, {
type: at.DISCOVERY_STREAM_LAYOUT_RESET,
});
expect(state.sectionPersonalization).toEqual(personalization);
});
it("should set config data with DISCOVERY_STREAM_CONFIG_CHANGE", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_CONFIG_CHANGE,
data: { enabled: true },
});
expect(state.config).toEqual({ enabled: true });
});
it("should set feeds as loaded with DISCOVERY_STREAM_FEEDS_UPDATE", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
});
expect(state.feeds.loaded).toBe(true);
});
it("should set spoc_endpoint with DISCOVERY_STREAM_SPOCS_ENDPOINT", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,
data: { url: "foo.com" },
});
expect(state.spocs.spocs_endpoint).toBe("foo.com");
});
it("should use initial state with DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,
data: {},
});
expect(state.spocs.placements).toEqual([]);
});
it("should set placements with DISCOVERY_STREAM_SPOCS_PLACEMENTS", () => {
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,
data: {
placements: [1, 2, 3],
},
});
expect(state.spocs.placements).toEqual([1, 2, 3]);
});
it("should set spocs with DISCOVERY_STREAM_SPOCS_UPDATE", () => {
const data = {
lastUpdated: 123,
spocs: [1, 2, 3],
spocsCacheUpdateTime: 10 * 60 * 1000,
spocsOnDemand: true,
};
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data,
});
expect(state.spocs).toEqual({
spocs_endpoint: "",
data: data.spocs,
lastUpdated: data.lastUpdated,
loaded: true,
frequency_caps: [],
blocked: [],
placements: [],
cacheUpdateTime: data.spocsCacheUpdateTime,
onDemand: {
enabled: data.spocsOnDemand,
loaded: false,
},
});
});
it("should default to a single spoc placement", () => {
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "https://foo.com" },
};
const oldState = {
spocs: {
data: {
spocs: {
items: [
{
url: "test-spoc.com",
},
],
},
},
loaded: true,
},
feeds: {
data: {},
loaded: true,
},
};
const newState = DiscoveryStream(oldState, deleteAction);
expect(newState.spocs.data.spocs.items).toHaveLength(1);
});
it("should handle no data from DISCOVERY_STREAM_SPOCS_UPDATE", () => {
const data = null;
const state = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOCS_UPDATE,
data,
});
expect(state.spocs).toEqual(INITIAL_STATE.DiscoveryStream.spocs);
});
it("should add blocked spocs to blocked array with DISCOVERY_STREAM_SPOC_BLOCKED", () => {
const firstState = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_SPOC_BLOCKED,
data: { url: "https://foo.com" },
});
const secondState = DiscoveryStream(firstState, {
type: at.DISCOVERY_STREAM_SPOC_BLOCKED,
data: { url: "https://bar.com" },
});
expect(firstState.spocs.blocked).toEqual(["https://foo.com"]);
expect(secondState.spocs.blocked).toEqual([
]);
});
it("should not update state for empty action.data on DISCOVERY_STREAM_LINK_BLOCKED", () => {
const newState = DiscoveryStream(undefined, {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
});
expect(newState).toBe(INITIAL_STATE.DiscoveryStream);
});
it("should not update state if feeds are not loaded", () => {
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "foo.com" },
};
const newState = DiscoveryStream(undefined, deleteAction);
expect(newState).toBe(INITIAL_STATE.DiscoveryStream);
});
it("should not update state if spocs and feeds data is undefined", () => {
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "foo.com" },
};
const oldState = {
spocs: {
data: {},
loaded: true,
placements: [{ name: "spocs" }],
},
feeds: {
data: {},
loaded: true,
},
};
const newState = DiscoveryStream(oldState, deleteAction);
expect(newState).toEqual(oldState);
});
it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from spocs if feeds data is empty", () => {
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "https://foo.com" },
};
const oldState = {
spocs: {
data: {
spocs: {
items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }],
},
},
loaded: true,
placements: [{ name: "spocs" }],
},
feeds: {
data: {},
loaded: true,
},
};
const newState = DiscoveryStream(oldState, deleteAction);
expect(newState.spocs.data.spocs.items).toEqual([
{ url: "test-spoc.com" },
]);
});
it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from feeds if spocs data is empty", () => {
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "https://foo.com" },
};
const oldState = {
spocs: {
data: {},
loaded: true,
placements: [{ name: "spocs" }],
},
feeds: {
data: {
data: {
recommendations: [
{ url: "https://foo.com" },
{ url: "test.com" },
],
},
},
},
loaded: true,
},
};
const newState = DiscoveryStream(oldState, deleteAction);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations
).toEqual([{ url: "test.com" }]);
});
it("should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from both feeds and spocs", () => {
const oldState = {
feeds: {
data: {
data: {
recommendations: [
{ url: "https://foo.com" },
{ url: "test.com" },
],
},
},
},
loaded: true,
},
spocs: {
data: {
spocs: {
items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }],
},
},
loaded: true,
placements: [{ name: "spocs" }],
},
};
const deleteAction = {
type: at.DISCOVERY_STREAM_LINK_BLOCKED,
data: { url: "https://foo.com" },
};
const newState = DiscoveryStream(oldState, deleteAction);
expect(newState.spocs.data.spocs.items).toEqual([
{ url: "test-spoc.com" },
]);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations
).toEqual([{ url: "test.com" }]);
});
it("should add boookmark details on PLACES_BOOKMARK_ADDED in both feeds and spocs", () => {
const oldState = {
feeds: {
data: {
data: {
recommendations: [
{ url: "https://foo.com" },
{ url: "test.com" },
],
},
},
},
loaded: true,
},
spocs: {
data: {
spocs: {
items: [{ url: "https://foo.com" }, { url: "test-spoc.com" }],
},
},
loaded: true,
placements: [{ name: "spocs" }],
},
};
const bookmarkAction = {
type: at.PLACES_BOOKMARK_ADDED,
data: {
url: "https://foo.com",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
dateAdded: 1234567,
},
};
const newState = DiscoveryStream(oldState, bookmarkAction);
expect(newState.spocs.data.spocs.items).toHaveLength(2);
expect(newState.spocs.data.spocs.items[0].bookmarkGuid).toBe(
bookmarkAction.data.bookmarkGuid
);
expect(newState.spocs.data.spocs.items[0].bookmarkTitle).toBe(
bookmarkAction.data.bookmarkTitle
);
expect(newState.spocs.data.spocs.items[1].bookmarkGuid).toBeUndefined();
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations
).toHaveLength(2);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations[0]
.bookmarkGuid
).toBe(bookmarkAction.data.bookmarkGuid);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations[0]
.bookmarkTitle
).toBe(bookmarkAction.data.bookmarkTitle);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations[1]
.bookmarkGuid
).toBeUndefined();
});
it("should remove boookmark details on PLACES_BOOKMARKS_REMOVED in both feeds and spocs", () => {
const oldState = {
feeds: {
data: {
data: {
recommendations: [
{
url: "https://foo.com",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
},
{ url: "test.com" },
],
},
},
},
loaded: true,
},
spocs: {
data: {
spocs: {
items: [
{
url: "https://foo.com",
bookmarkGuid: "bookmark123",
bookmarkTitle: "Title for bar.com",
},
{ url: "test-spoc.com" },
],
},
},
loaded: true,
placements: [{ name: "spocs" }],
},
};
const action = {
type: at.PLACES_BOOKMARKS_REMOVED,
data: {
urls: ["https://foo.com"],
},
};
const newState = DiscoveryStream(oldState, action);
expect(newState.spocs.data.spocs.items).toHaveLength(2);
expect(newState.spocs.data.spocs.items[0].bookmarkGuid).toBeUndefined();
expect(newState.spocs.data.spocs.items[0].bookmarkTitle).toBeUndefined();
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations
).toHaveLength(2);
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations[0]
.bookmarkGuid
).toBeUndefined();
expect(
newState.feeds.data["https://foo.com/feed1"].data.recommendations[0]
.bookmarkTitle
).toBeUndefined();
});
});
describe("Search", () => {
it("should return INITIAL_STATE by default", () => {
expect(Search(undefined, { type: "some_action" })).toBe(
INITIAL_STATE.Search
);
});
it("should set disable to true on DISABLE_SEARCH", () => {
const nextState = Search(undefined, { type: "DISABLE_SEARCH" });
expect(nextState.disable).toBe(true);
});
it("should set focus and disable to false on SHOW_SEARCH", () => {
const nextState = Search(undefined, { type: "SHOW_SEARCH" });
expect(nextState.disable).toBe(false);
});
});
describe("ExternalComponents", () => {
it("should return INITIAL_STATE by default", () => {
const nextState = ExternalComponents(undefined, { type: "some_action" });
expect(nextState).toBe(INITIAL_STATE.ExternalComponents);
});
it("should return initial state with empty components array", () => {
const nextState = ExternalComponents(undefined, { type: "some_action" });
expect(nextState.components).toEqual([]);
});
it("should update components on REFRESH_EXTERNAL_COMPONENTS", () => {
const testComponents = [
{
type: "SEARCH",
tagName: "test-component",
l10nURLs: [],
},
];
const nextState = ExternalComponents(undefined, {
type: at.REFRESH_EXTERNAL_COMPONENTS,
data: testComponents,
});
expect(nextState.components).toEqual(testComponents);
});
it("should preserve other state when updating components", () => {
const testComponents = [
{
type: "SEARCH",
tagName: "test-component",
l10nURLs: [],
},
];
const prevState = { components: [], otherProp: "value" };
const nextState = ExternalComponents(prevState, {
type: at.REFRESH_EXTERNAL_COMPONENTS,
data: testComponents,
});
expect(nextState.components).toEqual(testComponents);
expect(nextState.otherProp).toBe("value");
});
it("should replace existing components on REFRESH_EXTERNAL_COMPONENTS", () => {
const oldComponents = [
{
type: "OLD",
tagName: "old-component",
l10nURLs: [],
},
];
const newComponents = [
{
type: "NEW",
tagName: "new-component",
l10nURLs: [],
},
];
const prevState = { components: oldComponents };
const nextState = ExternalComponents(prevState, {
type: at.REFRESH_EXTERNAL_COMPONENTS,
data: newComponents,
});
expect(nextState.components).toEqual(newComponents);
expect(nextState.components).not.toEqual(oldComponents);
});
});
describe("PictureOfTheDay", () => {
it("PICTURE_OF_THE_DAY_UPDATE stores the picture fields", () => {
const next = PictureOfTheDay(INITIAL_STATE.PictureOfTheDay, {
type: at.PICTURE_OF_THE_DAY_UPDATE,
data: {
title: "T",
description: "D",
publishedDate: "2026-06-30",
lastUpdated: 123,
},
});
expect(next.imageUrl).toBe("https://example.com/x.jpg");
expect(next.description).toBe("D");
expect(next.publishedDate).toBe("2026-06-30");
expect(next.initialized).toBe(true);
});
it("defaults missing fields and returns prevState for other actions", () => {
const updated = PictureOfTheDay(INITIAL_STATE.PictureOfTheDay, {
type: at.PICTURE_OF_THE_DAY_UPDATE,
data: {},
});
expect(updated.imageUrl).toBe("");
expect(updated.description).toBe("");
const prev = INITIAL_STATE.PictureOfTheDay;
expect(PictureOfTheDay(prev, { type: "SOME_OTHER_ACTION" })).toBe(prev);
});
});
describe("SportsWidget", () => {
const baseMatches = {
previous: [],
current: [
{ global_event_id: 1, status_type: "live", home_score: 0 },
{ global_event_id: 2, status_type: "live", home_score: 1 },
],
next: [],
};
const stateWithMatches = () => ({
...INITIAL_STATE.SportsWidget,
data: { teams: [], matches: { ...baseMatches } },
});
it("WIDGETS_SPORTS_LIVE_UPDATE writes the incoming array to data.live", () => {
const liveEvents = [
{ global_event_id: 1, home_score: 2 },
{ global_event_id: 99, home_score: 0 },
];
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: { live: liveEvents, lastLiveUpdated: 12345 },
});
expect(next.data.live).toEqual(liveEvents);
});
it("WIDGETS_SPORTS_LIVE_UPDATE replaces data.live wholesale on each update", () => {
// /wcs/live returns the canonical set of in-progress games each tick,
// so the reducer simply overwrites; no merge against the prior set.
const prev = {
...stateWithMatches(),
data: {
...stateWithMatches().data,
live: [{ global_event_id: 1, home_score: 0 }],
},
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: {
live: [{ global_event_id: 2, home_score: 5 }],
lastLiveUpdated: 12345,
},
});
expect(next.data.live).toEqual([{ global_event_id: 2, home_score: 5 }]);
});
it("WIDGETS_SPORTS_LIVE_UPDATE records lastLiveUpdated at SportsWidget root", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: { live: [], lastLiveUpdated: 999_000 },
});
expect(next.lastLiveUpdated).toBe(999_000);
});
it("WIDGETS_SPORTS_LIVE_UPDATE lastLiveUpdated survives WIDGETS_SPORTS_WIDGET_SET", () => {
// Regression: the timestamp used to live under data.matches and was wiped
// by every post-match resync.
const afterLive = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: { live: [], lastLiveUpdated: 12345 },
});
const afterResync = SportsWidget(afterLive, {
type: at.WIDGETS_SPORTS_WIDGET_SET,
data: { teams: [], matches: { previous: [], current: [], next: [] } },
});
expect(afterResync.lastLiveUpdated).toBe(12345);
});
it("WIDGETS_SPORTS_LIVE_UPDATE preserves data.matches", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: {
live: [{ global_event_id: 1, home_score: 0 }],
lastLiveUpdated: 12345,
},
});
expect(next.data.matches).toEqual(baseMatches);
});
it("WIDGETS_SPORTS_LIVE_UPDATE preserves other SportsWidget fields", () => {
const prev = {
...stateWithMatches(),
widgetState: "sports-intro",
selectedTeams: ["ENG"],
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_LIVE_UPDATE,
data: { live: [], lastLiveUpdated: 1 },
});
expect(next.widgetState).toBe("sports-intro");
expect(next.selectedTeams).toEqual(["ENG"]);
});
it("WIDGETS_SPORTS_SET_CELEBRATIONS replaces the celebrations map", () => {
const celebrations = {
endedAt: { 7: 12345 },
celebrated: [3, 5],
};
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_CELEBRATIONS,
data: celebrations,
});
expect(next.celebrations).toEqual(celebrations);
});
it("WIDGETS_SPORTS_SET_CELEBRATIONS preserves other SportsWidget fields", () => {
const prev = {
...stateWithMatches(),
widgetState: "sports-matches",
selectedTeams: ["ENG"],
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_SET_CELEBRATIONS,
data: { endedAt: {}, celebrated: [1] },
});
expect(next.widgetState).toBe("sports-matches");
expect(next.selectedTeams).toEqual(["ENG"]);
expect(next.data.matches).toEqual(baseMatches);
});
describe("WIDGETS_SPORTS_WATCH_LIVE", () => {
const watchLiveData = {
your_region: [{ product_name: "SBS", entitlement: "Free", url: "u" }],
other_regions: [],
};
it("WIDGETS_SPORTS_WATCH_LIVE_SET stores the payload and marks it loaded", () => {
const next = SportsWidget(INITIAL_STATE.SportsWidget, {
type: at.WIDGETS_SPORTS_WATCH_LIVE_SET,
data: watchLiveData,
});
expect(next.watchLive).toEqual({
loaded: true,
data: watchLiveData,
});
});
it("WIDGETS_SPORTS_WATCH_LIVE_REQUEST shows the loading state when nothing is cached", () => {
const next = SportsWidget(INITIAL_STATE.SportsWidget, {
type: at.WIDGETS_SPORTS_WATCH_LIVE_REQUEST,
});
expect(next.watchLive).toEqual({ loaded: false, data: null });
});
it("WIDGETS_SPORTS_WATCH_LIVE_REQUEST preserves a previously-fetched payload", () => {
// The button is gated on this data; a re-request (modal refresh) must
// not drop it and hide the entry point mid-session.
const loaded = SportsWidget(INITIAL_STATE.SportsWidget, {
type: at.WIDGETS_SPORTS_WATCH_LIVE_SET,
data: watchLiveData,
});
const next = SportsWidget(loaded, {
type: at.WIDGETS_SPORTS_WATCH_LIVE_REQUEST,
});
expect(next.watchLive).toEqual({
loaded: true,
data: watchLiveData,
});
});
});
describe("WIDGETS_SPORTS_SET_LOAD_MORE", () => {
const matchA = {
global_event_id: 101,
home_team: { key: "ENG" },
away_team: { key: "FRA" },
date: "2026-06-22T18:00:00Z",
};
const matchB = {
global_event_id: 102,
home_team: { key: "ESP" },
away_team: { key: "POR" },
date: "2026-06-23T18:00:00Z",
};
it("merges partial load-more flags into the upcoming slot", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "upcoming", loading: true },
});
expect(next.loadMore.upcoming).toEqual({
loading: true,
exhausted: false,
lastFetchedDate: null,
});
// Results slot is unchanged.
expect(next.loadMore.results).toEqual(
INITIAL_STATE.SportsWidget.loadMore.results
);
});
it("merges partial load-more flags into the results slot", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "results", exhausted: true },
});
expect(next.loadMore.results).toEqual({
loading: false,
exhausted: true,
lastFetchedDate: null,
});
// Upcoming slot is unchanged.
expect(next.loadMore.upcoming).toEqual(
INITIAL_STATE.SportsWidget.loadMore.upcoming
);
});
it("appends upcoming matches to data.matches.next", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: {
direction: "upcoming",
matches: [matchA, matchB],
loading: false,
lastFetchedDate: "2026-06-21",
exhausted: false,
},
});
expect(next.data.matches.next).toEqual([matchA, matchB]);
expect(next.loadMore.upcoming.lastFetchedDate).toBe("2026-06-21");
expect(next.loadMore.upcoming.loading).toBe(false);
});
it("appends results matches to data.matches.previous", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: {
direction: "results",
matches: [matchA, matchB],
loading: false,
lastFetchedDate: "2026-05-26",
exhausted: false,
},
});
expect(next.data.matches.previous).toEqual([matchA, matchB]);
expect(next.loadMore.results.lastFetchedDate).toBe("2026-05-26");
});
it("dedupes upcoming appends against existing next[]", () => {
const prev = {
...stateWithMatches(),
data: {
teams: [],
matches: { previous: [], current: [], next: [matchA] },
},
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "upcoming", matches: [matchA, matchB] },
});
expect(next.data.matches.next).toEqual([matchA, matchB]);
});
it("dedupes results appends against existing previous[]", () => {
const prev = {
...stateWithMatches(),
data: {
teams: [],
matches: { previous: [matchA], current: [], next: [] },
},
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "results", matches: [matchA, matchB] },
});
expect(next.data.matches.previous).toEqual([matchA, matchB]);
});
it("dedupes by composite key when global_event_id is missing", () => {
const tbdMatch = {
home_team: { key: "TBD-1" },
away_team: { key: "TBD-2" },
date: "2026-07-04T18:00:00Z",
};
const prev = {
...stateWithMatches(),
data: {
teams: [],
matches: { previous: [], current: [], next: [tbdMatch] },
},
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "upcoming", matches: [tbdMatch, matchA] },
});
expect(next.data.matches.next).toEqual([tbdMatch, matchA]);
});
it("leaves existing matches alone when no new matches are provided", () => {
const next = SportsWidget(stateWithMatches(), {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "upcoming", loading: false, exhausted: true },
});
expect(next.data.matches).toEqual(baseMatches);
expect(next.loadMore.upcoming.exhausted).toBe(true);
});
it("ignores actions with an unknown direction", () => {
const prev = stateWithMatches();
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_SET_LOAD_MORE,
data: { direction: "sideways", loading: true },
});
// Unknown directions are a no-op — return the prior state untouched.
expect(next).toBe(prev);
});
});
it("WIDGETS_SPORTS_WIDGET_SET resets both load-more slots", () => {
const prev = {
...stateWithMatches(),
loadMore: {
upcoming: {
loading: false,
exhausted: true,
lastFetchedDate: "2026-06-21",
},
results: {
loading: true,
exhausted: false,
lastFetchedDate: "2026-05-26",
},
},
};
const next = SportsWidget(prev, {
type: at.WIDGETS_SPORTS_WIDGET_SET,
data: { teams: [], matches: { previous: [], current: [], next: [] } },
});
expect(next.loadMore).toEqual(INITIAL_STATE.SportsWidget.loadMore);
});
});
describe("Stocks", () => {
it("WIDGETS_STOCKS_UPDATE replaces tickers and sets lastUpdated", () => {
const action = {
type: at.WIDGETS_STOCKS_UPDATE,
data: {
tickers: [{ ticker: "SPY", name: "SPDR S&P 500 ETF Trust" }],
lastUpdated: 1700000000000,
},
};
const nextState = reducers.Stocks(undefined, action);
expect(nextState.tickers).toEqual(action.data.tickers);
expect(nextState.lastUpdated).toBe(1700000000000);
});
it("returns previous state for unrelated actions", () => {
const prev = { tickers: [{ ticker: "DIA" }], lastUpdated: 1 };
expect(reducers.Stocks(prev, { type: "SOME_OTHER_ACTION" })).toBe(prev);
});
it("WIDGETS_STOCKS_UPDATE stores the error flag", () => {
const action = {
type: at.WIDGETS_STOCKS_UPDATE,
data: { tickers: [], lastUpdated: 1700000000000, error: true },
};
const nextState = reducers.Stocks(undefined, action);
expect(nextState.error).toBe(true);
});
it("WIDGETS_STOCKS_UPDATE defaults error to false when omitted", () => {
const action = {
type: at.WIDGETS_STOCKS_UPDATE,
data: { tickers: [], lastUpdated: 1 },
};
const nextState = reducers.Stocks(undefined, action);
expect(nextState.error).toBe(false);
});
});
describe("WebNotifications", () => {
const notification = {
id: "n1",
origin: "https://example.com",
persistent: true,
title: "hi",
};
it("should return INITIAL_STATE by default", () => {
const nextState = WebNotifications(undefined, {
type: "some_action",
});
expect(nextState).toBe(INITIAL_STATE.WebNotifications);
});
it("should set initialized and clear error on WEB_NOTIFICATIONS_UPDATED", () => {
const prevState = {
...INITIAL_STATE.WebNotifications,
error: { step: "snapshot", message: "boom" },
};
const data = {
lastUpdated: 12345,
notifications: { abc: { id: "abc", origin: "https://example.com" } },
byOrigin: { "https://example.com": ["abc"] },
};
const nextState = WebNotifications(prevState, {
type: at.WEB_NOTIFICATIONS_UPDATED,
data,
});
expect(nextState.initialized).toBe(true);
expect(nextState.lastUpdated).toBe(12345);
expect(nextState.notifications).toEqual(data.notifications);
expect(nextState.byOrigin).toEqual(data.byOrigin);
expect(nextState.error).toBeNull();
});
it("should set error and preserve other fields on WEB_NOTIFICATIONS_ERROR", () => {
const prevState = {
...INITIAL_STATE.WebNotifications,
initialized: true,
notifications: { abc: { id: "abc" } },
};
const errorData = { step: "snapshot", message: "boom" };
const nextState = WebNotifications(prevState, {
type: at.WEB_NOTIFICATIONS_ERROR,
data: errorData,
});
expect(nextState.error).toEqual(errorData);
expect(nextState.initialized).toBe(true);
expect(nextState.notifications).toEqual(prevState.notifications);
});
it("should merge one notification into the index on WEB_NOTIFICATIONS_ADDED", () => {
const action = {
type: at.WEB_NOTIFICATIONS_ADDED,
data: { notification },
};
const nextState = WebNotifications(undefined, action);
expect(nextState.notifications.n1).toBe(notification);
expect(nextState.byOrigin["https://example.com"]).toEqual(["n1"]);
});
it("should not duplicate an existing id on WEB_NOTIFICATIONS_ADDED", () => {
const prevState = {
...INITIAL_STATE.WebNotifications,
notifications: { n1: notification },
byOrigin: { "https://example.com": ["n1"] },
};
const nextState = WebNotifications(prevState, {
type: at.WEB_NOTIFICATIONS_ADDED,
data: { notification },
});
expect(nextState.byOrigin["https://example.com"]).toEqual(["n1"]);
});
it("should drop ids and prune empty origins on WEB_NOTIFICATIONS_REMOVED", () => {
const prevState = {
...INITIAL_STATE.WebNotifications,
notifications: { n1: notification },
byOrigin: { "https://example.com": ["n1"] },
};
const nextState = WebNotifications(prevState, {
type: at.WEB_NOTIFICATIONS_REMOVED,
data: { removed: [{ origin: "https://example.com", id: "n1" }] },
});
expect(nextState.notifications).toEqual({});
expect(nextState.byOrigin["https://example.com"]).toBeUndefined();
});
});
});