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
import { render, fireEvent } from "@testing-library/react";
import { Provider } from "react-redux";
import { combineReducers, createStore } from "redux";
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
import { actionTypes as at } from "common/Actions.mjs";
import { Stocks } from "content-src/components/Widgets/Stocks/Stocks";
const mockState = {
...INITIAL_STATE,
Prefs: {
...INITIAL_STATE.Prefs,
values: {
...INITIAL_STATE.Prefs.values,
"widgets.system.enabled": true,
"widgets.system.stocks.enabled": true,
"widgets.stocks.enabled": true,
"widgets.stocks.size": "medium",
},
},
};
function WrapWithProvider({ children, state = INITIAL_STATE }) {
const store = createStore(combineReducers(reducers), state);
return <Provider store={store}>{children}</Provider>;
}
function renderStocks(dispatch = jest.fn(), props = {}) {
const { container, unmount } = render(
<WrapWithProvider state={mockState}>
<Stocks
dispatch={dispatch}
widgetsMayBeMaximized={true}
widgetEnabledMap={{}}
{...props}
/>
</WrapWithProvider>
);
return { container, unmount, dispatch };
}
describe("Stocks widget", () => {
it("renders the widget at the resolved size", () => {
const { container } = renderStocks();
const root = container.querySelector("article.stocks");
expect(root).toBeTruthy();
expect(root.className).toContain("medium-widget");
});
it("renders an always-visible localized title", () => {
const { container } = renderStocks();
const title = container.querySelector(
'[data-l10n-id="newtab-stocks-widget-title"]'
);
expect(title).toBeTruthy();
expect(title.getAttribute("aria-hidden")).toBeNull();
});
it("offers medium and large sizes only", () => {
const { container } = renderStocks();
const items = [
...container.querySelectorAll(
"#stocks-size-submenu panel-item[type='checkbox']"
),
];
const sizes = items.map(el => el.getAttribute("data-size"));
expect(sizes).toEqual(["medium", "large"]);
expect(items.every(el => !el.hasAttribute("disabled"))).toBe(true);
});
it("hides the widget by setting its enabled pref to false", () => {
const dispatch = jest.fn();
const { container } = renderStocks(dispatch);
const hide = container.querySelector(
'[data-l10n-id="newtab-widget-menu-hide"]'
);
fireEvent.click(hide);
const setPref = dispatch.mock.calls.find(
([action]) =>
action.type === at.SET_PREF &&
action.data?.name === "widgets.stocks.enabled"
);
expect(setPref).toBeTruthy();
expect(setPref[0].data.value).toBe(false);
});
it("records a telemetry event for the ticker-search stub", () => {
const dispatch = jest.fn();
const { container } = renderStocks(dispatch);
const search = container.querySelector(
'[data-l10n-id="newtab-stocks-menu-search"]'
);
fireEvent.click(search);
const evt = dispatch.mock.calls.find(
([action]) =>
action.type === at.WIDGETS_USER_EVENT &&
action.data?.user_action === "search_tickers"
);
expect(evt).toBeTruthy();
});
function renderStocksWithTickers(tickers, size = "medium") {
const state = {
...mockState,
Prefs: {
...mockState.Prefs,
values: { ...mockState.Prefs.values, "widgets.stocks.size": size },
},
Stocks: { tickers, lastUpdated: 1 },
};
return render(
<WrapWithProvider state={state}>
<Stocks
dispatch={jest.fn()}
widgetsMayBeMaximized={true}
widgetEnabledMap={{}}
/>
</WrapWithProvider>
);
}
// Matches what Merino actually returns: last_price ends with " USD" and
// todays_change_perc has no "%". The widget formats both for display.
const SAMPLE = [
{
ticker: "SPY",
name: "SPDR S&P 500 ETF Trust",
last_price: "$559.44 USD",
todays_change_perc: "+0.2",
},
{
ticker: "ONEQ",
name: "Fidelity Nasdaq Composite",
last_price: "$70.10 USD",
todays_change_perc: "-0.21",
},
{
ticker: "DIA",
name: "SPDR Dow Jones ETF",
last_price: "$430.00 USD",
todays_change_perc: "0.00",
},
{
ticker: "IWM",
name: "iShares Russell 2000 ETF",
last_price: "$220.00 USD",
todays_change_perc: "+1.0",
},
];
it("renders one card per ticker in the medium grid with formatted price and change", () => {
const { container } = renderStocksWithTickers(SAMPLE, "medium");
expect(
container.querySelectorAll(".stocks-grid .stock-ticker").length
).toBe(4);
expect(container.querySelector(".stock-ticker-symbol").textContent).toBe(
"SPY"
);
expect(container.querySelector(".stock-ticker-price").textContent).toBe(
"$559.44"
);
expect(container.querySelector(".stock-ticker-change").textContent).toBe(
"+0.2%"
);
});
it("renders placeholder cards before data arrives", () => {
const { container } = renderStocksWithTickers([], "medium");
expect(container.querySelector(".stocks-grid--loading")).toBeTruthy();
expect(
container.querySelectorAll(".stocks-grid .stock-ticker").length
).toBe(4);
expect(container.querySelectorAll(".stock-ticker-sr").length).toBe(0);
});
it("renders the large size as a vertical list showing full names", () => {
const { container } = renderStocksWithTickers(SAMPLE, "large");
expect(container.querySelector(".stocks-grid")).toBeNull();
expect(
container.querySelectorAll(".stocks-list .stock-ticker--large").length
).toBe(4);
expect(container.querySelector(".stock-ticker-name").textContent).toBe(
"SPDR S&P 500 ETF Trust"
);
expect(container.querySelector(".stock-ticker-price").textContent).toBe(
"$559.44"
);
expect(container.querySelector(".stock-ticker-change").textContent).toBe(
"+0.2%"
);
});
it("renders placeholder cards before data arrives at the large size", () => {
const { container } = renderStocksWithTickers([], "large");
expect(container.querySelector(".stocks-list--loading")).toBeTruthy();
expect(
container.querySelectorAll(".stocks-list .stock-ticker--large").length
).toBe(4);
expect(container.querySelectorAll(".stock-ticker-sr").length).toBe(0);
});
});