/* 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 { 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 { Crossword } from "content-src/components/Widgets/Crossword/Crossword"; const baseState = { ...INITIAL_STATE, Prefs: { ...INITIAL_STATE.Prefs, values: { ...INITIAL_STATE.Prefs.values, "widgets.system.enabled": true, "widgets.enabled": true, "widgets.system.crossword.enabled": true, "widgets.crossword.enabled": true, "widgets.crossword.size": "", "widgets.crossword.endpoint": "https://prod-games-particle.merino.prod.webservices.mozgcp.net/index.html", }, }, }; function WrapWithProvider({ children, state = baseState }) { const store = createStore(combineReducers(reducers), state); return {children}; } function renderCrossword({ state = baseState, dispatch = jest.fn(), widgetsMayBeMaximized = true, } = {}) { const widgetEnabledMap = { crossword: true }; const utils = render( ); return { ...utils, dispatch }; } describe("", () => { describe("rendering", () => { it("renders the article root with the default medium size class", () => { const { container } = renderCrossword(); const root = container.querySelector("article.crossword"); expect(root).toBeInTheDocument(); expect(root).toHaveClass("widget"); expect(root).toHaveClass("col-4"); expect(root).toHaveClass("medium-widget"); }); it("applies the size from the size pref when set", () => { const state = { ...baseState, Prefs: { ...baseState.Prefs, values: { ...baseState.Prefs.values, "widgets.crossword.size": "large", }, }, }; const { container } = renderCrossword({ state }); expect(container.querySelector("article.crossword")).toHaveClass( "large-widget" ); }); it("renders the size submenu only when widgetsMayBeMaximized", () => { const { container, rerender } = renderCrossword({ widgetsMayBeMaximized: true, }); expect( container.querySelector("#crossword-size-submenu") ).toBeInTheDocument(); rerender( ); expect( container.querySelector("#crossword-size-submenu") ).not.toBeInTheDocument(); }); }); describe("hide action", () => { it("dispatches SET_PREF and WIDGETS_ENABLED when hide is clicked", () => { const { container, dispatch } = renderCrossword(); const hideItem = container.querySelector( "panel-item[data-l10n-id='newtab-widget-menu-hide']" ); fireEvent.click(hideItem); const setPrefCall = dispatch.mock.calls.find( ([action]) => action?.type === at.SET_PREF && action.data?.name === "widgets.crossword.enabled" ); expect(setPrefCall?.[0].data.value).toBe(false); const enabledCall = dispatch.mock.calls.find( ([action]) => action?.type === at.WIDGETS_ENABLED ); expect(enabledCall?.[0].data).toMatchObject({ widget_name: "crossword", widget_source: "context_menu", enabled: false, }); }); }); describe("learn more action", () => { it("dispatches OPEN_LINK with the shared SUMO URL", () => { const { container, dispatch } = renderCrossword(); const learnMoreItem = container.querySelector("panel-item.learn-more"); fireEvent.click(learnMoreItem); const openLinkCall = dispatch.mock.calls.find( ([action]) => action?.type === at.OPEN_LINK ); expect(openLinkCall?.[0].data.url).toBe( "https://support.mozilla.org/kb/firefox-new-tab-widgets" ); }); it("dispatches a learn_more WIDGETS_USER_EVENT", () => { const { container, dispatch } = renderCrossword(); fireEvent.click(container.querySelector("panel-item.learn-more")); const userEventCall = dispatch.mock.calls.find( ([action]) => action?.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "learn_more" ); expect(userEventCall?.[0].data).toMatchObject({ widget_name: "crossword", widget_source: "context_menu", user_action: "learn_more", widget_size: "medium", }); }); }); describe("impression telemetry", () => { let originalIntersectionObserver; let observerInstances; beforeEach(() => { observerInstances = []; originalIntersectionObserver = global.IntersectionObserver; global.IntersectionObserver = class MockIntersectionObserver { constructor(callback) { this.callback = callback; this.observed = []; observerInstances.push(this); } observe(el) { this.observed.push(el); } unobserve() {} disconnect() {} }; }); afterEach(() => { global.IntersectionObserver = originalIntersectionObserver; }); it("dispatches WIDGETS_IMPRESSION only once even when intersection fires repeatedly", () => { const { dispatch } = renderCrossword(); const [observer] = observerInstances; const [target] = observer.observed; observer.callback([{ isIntersecting: true, target }], observer); observer.callback([{ isIntersecting: true, target }], observer); const impressions = dispatch.mock.calls.filter( ([action]) => action?.type === at.WIDGETS_IMPRESSION ); expect(impressions).toHaveLength(1); expect(impressions[0][0].data).toMatchObject({ widget_name: "crossword", widget_size: "medium", }); }); }); describe("crossword iframe", () => { it("renders a sandboxed iframe pointing at the resolved endpoint", () => { const { container } = renderCrossword(); const frame = container.querySelector("iframe.crossword-frame"); expect(frame).toBeInTheDocument(); expect(frame).toHaveAttribute( "sandbox", "allow-scripts allow-same-origin" ); expect(frame).toHaveAttribute( "src", "https://prod-games-particle.merino.prod.webservices.mozgcp.net/index.html" ); }); it("prefers the trainhopConfig endpoint over the raw pref", () => { const trainhopEndpoint = "https://example.com/crossword/index.html"; const state = { ...baseState, Prefs: { ...baseState.Prefs, values: { ...baseState.Prefs.values, trainhopConfig: { widgets: { crosswordEndpoint: trainhopEndpoint }, }, }, }, }; const { container } = renderCrossword({ state }); expect(container.querySelector("iframe.crossword-frame")).toHaveAttribute( "src", trainhopEndpoint ); }); }); describe("change size action", () => { it("dispatches SET_PREF and a change_size WIDGETS_USER_EVENT when a size is picked", () => { const { container, dispatch } = renderCrossword(); const largeSizeItem = container.querySelector( "#crossword-size-submenu panel-item[data-size='large']" ); fireEvent.click(largeSizeItem); const setPrefCall = dispatch.mock.calls.find( ([action]) => action?.type === at.SET_PREF && action.data?.name === "widgets.crossword.size" ); expect(setPrefCall?.[0].data.value).toBe("large"); const userEventCall = dispatch.mock.calls.find( ([action]) => action?.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "change_size" ); expect(userEventCall?.[0].data).toMatchObject({ widget_name: "crossword", widget_source: "context_menu", user_action: "change_size", action_value: "large", widget_size: "large", }); }); }); });