/* 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 { PictureOfTheDay } from "content-src/components/Widgets/PictureOfTheDay/PictureOfTheDay"; const mockState = { ...INITIAL_STATE, Prefs: { ...INITIAL_STATE.Prefs, values: { ...INITIAL_STATE.Prefs.values, "widgets.system.enabled": true, "widgets.system.pictureOfTheDay.enabled": true, "widgets.pictureOfTheDay.enabled": true, "widgets.pictureOfTheDay.size": "medium", "widgets.pictureOfTheDay.setAsWallpaper.enabled": true, "newtabWallpapers.enabled": true, "newtabWallpapers.user.enabled": true, "newtabWallpapers.customWallpaper.enabled": true, }, }, }; function WrapWithProvider({ children, state = INITIAL_STATE }) { const store = createStore(combineReducers(reducers), state); return {children}; } function renderWidget(dispatch = jest.fn(), props = {}, state = mockState) { const handleUserInteraction = props.handleUserInteraction || jest.fn(); const { container, unmount } = render( ); return { container, unmount, dispatch, handleUserInteraction }; } describe("PictureOfTheDay widget", () => { it("renders the widget at the resolved size", () => { const { container } = renderWidget(); const root = container.querySelector("article.picture-of-the-day"); expect(root).toBeTruthy(); expect(root.className).toContain("medium-widget"); }); it("renders the empty-state message and the show (eye) button", () => { const { container } = renderWidget(); expect( container.querySelector('[data-l10n-id="newtab-picture-check-back"]') ).toBeTruthy(); expect( container.querySelector('[data-l10n-id="newtab-picture-show-button"]') ).toBeTruthy(); }); 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 = jest.fn(); renderWidget(dispatch); 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: "picture_of_the_day", widget_size: "medium", }); }); }); it("offers only medium and large in the resize submenu", () => { const { container } = renderWidget(); const sizes = [ ...container.querySelectorAll( "#picture-of-the-day-size-submenu [data-size]" ), ].map(el => el.getAttribute("data-size")); expect(sizes).toEqual(["medium", "large"]); }); it("hides the widget by setting its enabled pref to false", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch); fireEvent.click( container.querySelector('[data-l10n-id="newtab-widget-menu-hide"]') ); const setPref = dispatch.mock.calls.find( ([action]) => action.type === at.SET_PREF && action.data?.name === "widgets.pictureOfTheDay.enabled" ); expect(setPref).toBeTruthy(); expect(setPref[0].data.value).toBe(false); }); it("emits a user event when the show (eye) button is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch); fireEvent.click(container.querySelector(".picture-of-the-day-show-button")); const evt = dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "show_picture" ); expect(evt).toBeTruthy(); expect(evt[0].data.widget_name).toBe("picture_of_the_day"); }); it("emits telemetry for both the manage-wallpaper and hide-photo menu items", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch); const manageWallpaper = container.querySelector( '[data-l10n-id="newtab-picture-menu-manage-wallpaper"]' ); expect(manageWallpaper).toBeTruthy(); fireEvent.click(manageWallpaper); expect( dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "manage_wallpaper" ) ).toBeTruthy(); const hidePhoto = container.querySelector( '[data-l10n-id="newtab-picture-menu-hide-photo"]' ); expect(hidePhoto).toBeTruthy(); fireEvent.click(hidePhoto); expect( dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "hide_photo" ) ).toBeTruthy(); }); it("opens the Customize panel when Manage wallpaper is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch); fireEvent.click( container.querySelector( '[data-l10n-id="newtab-picture-menu-manage-wallpaper"]' ) ); expect( dispatch.mock.calls.find( ([action]) => action.type === at.SHOW_PERSONALIZE ) ).toBeTruthy(); }); describe("populated state", () => { const populatedState = { ...mockState, PictureOfTheDay: { ...INITIAL_STATE.PictureOfTheDay, imageUrl: "https://example.com/potd.jpg", title: "Test picture", description: "A test picture description.", publishedDate: "2026-07-01", }, }; const withPrefs = extraPrefs => ({ ...populatedState, Prefs: { ...populatedState.Prefs, values: { ...populatedState.Prefs.values, ...extraPrefs }, }, }); it("renders the picture, source line, description, and Set wallpaper button", () => { const { container } = renderWidget(jest.fn(), {}, populatedState); const root = container.querySelector("article.picture-of-the-day"); expect(root.className).toContain("has-picture"); const img = container.querySelector("img.picture-of-the-day-image"); expect(img).toBeTruthy(); expect(img.getAttribute("src")).toBe("https://example.com/potd.jpg"); expect(img.getAttribute("alt")).toBe("A test picture description."); expect( container.querySelector('[data-l10n-id="newtab-picture-header-main"]') ).toBeTruthy(); expect( container.querySelector(".picture-of-the-day-description").textContent ).toBe("A test picture description."); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeTruthy(); // No empty-state eye/message while a picture is shown. expect( container.querySelector(".picture-of-the-day-show-button") ).toBeFalsy(); }); it("renders the New badge until the widget has been interacted with", () => { const { container } = renderWidget(jest.fn(), {}, populatedState); const badge = container.querySelector(".picture-of-the-day-new-badge"); expect(badge).toBeTruthy(); // type="new" logs a missing-string error on newtab, so it must stay off. expect(badge.hasAttribute("type")).toBe(false); expect(badge.getAttribute("data-l10n-id")).toBe( "newtab-widget-lists-label-new" ); }); it("hides the New badge once the interaction pref is set", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.interaction": true }) ); expect( container.querySelector(".picture-of-the-day-new-badge") ).toBeFalsy(); }); it("falls back to the empty state when the picture fails to load", () => { const { container } = renderWidget(jest.fn(), {}, populatedState); const img = container.querySelector("img.picture-of-the-day-image"); expect(img).toBeTruthy(); fireEvent.error(img); expect( container.querySelector("img.picture-of-the-day-image") ).toBeFalsy(); expect( container.querySelector('[data-l10n-id="newtab-picture-check-back"]') ).toBeTruthy(); expect( container.querySelector("article.picture-of-the-day").className ).not.toContain("has-picture"); }); it("dispatches WIDGETS_PICTURE_SET_WALLPAPER when Set wallpaper is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, populatedState); fireEvent.click( container.querySelector(".picture-of-the-day-set-wallpaper") ); expect( dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_PICTURE_SET_WALLPAPER ) ).toBeTruthy(); }); it("keeps the Set wallpaper button collapsed with a checkmark while the current picture is the active wallpaper", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.wallpaperActive": "2026-07-01" }) ); const button = container.querySelector( ".picture-of-the-day-set-wallpaper" ); expect(button.classList.contains("is-collapsed")).toBe(true); expect(button.classList.contains("no-expand")).toBe(true); expect(button.getAttribute("iconsrc")).toBe( "chrome://global/skin/icons/check.svg" ); }); it("does nothing when the checkmark (already-set) button is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget( dispatch, {}, withPrefs({ "widgets.pictureOfTheDay.wallpaperActive": "2026-07-01" }) ); fireEvent.click( container.querySelector(".picture-of-the-day-set-wallpaper") ); expect( dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_PICTURE_SET_WALLPAPER ) ).toBeFalsy(); }); it("hides the checkmark while wallpapers are toggled off, keeping the stored date", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.wallpaperActive": "2026-07-01", "newtabWallpapers.user.enabled": false, }) ); const button = container.querySelector( ".picture-of-the-day-set-wallpaper" ); expect(button.classList.contains("is-collapsed")).toBe(false); expect(button.getAttribute("iconsrc")).toBe( "chrome://browser/skin/canvas.svg" ); }); it("re-offers the Set wallpaper CTA when the active-wallpaper date is for a different picture", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.wallpaperActive": "2026-06-30" }) ); const button = container.querySelector( ".picture-of-the-day-set-wallpaper" ); expect(button.classList.contains("is-collapsed")).toBe(false); expect(button.getAttribute("iconsrc")).toBe( "chrome://browser/skin/canvas.svg" ); }); it("hides the Set wallpaper button when custom wallpapers are disabled", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "newtabWallpapers.customWallpaper.enabled": false }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeFalsy(); }); it("hides the Set wallpaper button when wallpapers are disabled", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "newtabWallpapers.enabled": false }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeFalsy(); }); it("hides the Set wallpaper button when the feature is disabled", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.setAsWallpaper.enabled": false }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeFalsy(); }); it("shows the Set wallpaper button when a trainhopConfig override enables the feature despite the pref being off", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.setAsWallpaper.enabled": false, trainhopConfig: { widgets: { pictureOfTheDaySetAsWallpaperEnabled: true }, }, }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeTruthy(); }); it("shows the Set wallpaper button when the dedicated widgetPictureOfTheDay namespace enables the feature", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.setAsWallpaper.enabled": false, trainhopConfig: { widgetPictureOfTheDay: { setAsWallpaperEnabled: true }, }, }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeTruthy(); }); it("lets the dedicated widgetPictureOfTheDay namespace win over the shared widgets key", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.setAsWallpaper.enabled": true, trainhopConfig: { widgetPictureOfTheDay: { setAsWallpaperEnabled: false }, widgets: { pictureOfTheDaySetAsWallpaperEnabled: true }, }, }) ); expect( container.querySelector(".picture-of-the-day-set-wallpaper") ).toBeFalsy(); }); it("dismisses by setting dismissedDate to the picture's date", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, populatedState); fireEvent.click( container.querySelector( '[data-l10n-id="newtab-picture-menu-hide-photo"]' ) ); const setPref = dispatch.mock.calls.find( ([action]) => action.type === at.SET_PREF && action.data?.name === "widgets.pictureOfTheDay.dismissedDate" ); expect(setPref).toBeTruthy(); expect(setPref[0].data.value).toBe("2026-07-01"); }); it("shows the empty state while today's picture is dismissed; the eye clears it", () => { const dispatch = jest.fn(); const { container } = renderWidget( dispatch, {}, withPrefs({ "widgets.pictureOfTheDay.dismissedDate": "2026-07-01" }) ); expect( container.querySelector("img.picture-of-the-day-image") ).toBeFalsy(); expect( container.querySelector('[data-l10n-id="newtab-picture-check-back"]') ).toBeTruthy(); fireEvent.click( container.querySelector(".picture-of-the-day-show-button") ); const cleared = dispatch.mock.calls.find( ([action]) => action.type === at.SET_PREF && action.data?.name === "widgets.pictureOfTheDay.dismissedDate" ); expect(cleared).toBeTruthy(); expect(cleared[0].data.value).toBe(""); }); it("shows a new day's picture even if a previous day was dismissed", () => { const { container } = renderWidget( jest.fn(), {}, withPrefs({ "widgets.pictureOfTheDay.dismissedDate": "2026-06-30" }) ); expect( container.querySelector("img.picture-of-the-day-image") ).toBeTruthy(); }); it("falls back to the local day when the picture has no published date", () => { const dispatch = jest.fn(); const { container } = renderWidget( dispatch, {}, { ...populatedState, PictureOfTheDay: { ...populatedState.PictureOfTheDay, publishedDate: "", }, } ); fireEvent.click( container.querySelector( '[data-l10n-id="newtab-picture-menu-hide-photo"]' ) ); const setPref = dispatch.mock.calls.find( ([action]) => action.type === at.SET_PREF && action.data?.name === "widgets.pictureOfTheDay.dismissedDate" ); expect(setPref).toBeTruthy(); expect(setPref[0].data.value).toBe(new Date().toDateString()); }); }); describe("attribution and source links", () => { const SOURCE_URL = "https://commons.wikimedia.org/wiki/File:Example.jpg"; const LICENSE_URL = "https://creativecommons.org/licenses/by-sa/4.0/"; const attributedState = { ...mockState, PictureOfTheDay: { ...INITIAL_STATE.PictureOfTheDay, imageUrl: "https://example.com/potd.jpg", description: "A test picture description.", publishedDate: "2026-07-01", author: "Jane Doe", sourceUrl: SOURCE_URL, licenseLabel: "CC BY-SA 4.0", licenseUrl: LICENSE_URL, }, }; const findOpenLink = dispatch => dispatch.mock.calls.find(([action]) => action.type === at.OPEN_LINK); it("renders the author credit, source link, and license link", () => { const { container } = renderWidget(jest.fn(), {}, attributedState); const author = container.querySelector( ".picture-of-the-day-attribution-author" ); expect(author.getAttribute("data-l10n-id")).toBe( "newtab-picture-attribution-author" ); expect(author.getAttribute("data-l10n-args")).toContain("Jane Doe"); expect( container.querySelector( '[data-l10n-id="newtab-picture-attribution-source-link"]' ) ).toBeTruthy(); const license = container.querySelector( '[data-l10n-id="newtab-picture-attribution-license"]' ); expect(license).toBeTruthy(); expect(license.textContent).toBe("CC BY-SA 4.0"); }); it("keeps the title and description as plain (non-link) text", () => { const { container } = renderWidget(jest.fn(), {}, attributedState); expect( container.querySelector('p[data-l10n-id="newtab-picture-header-main"]') ).toBeTruthy(); expect( container.querySelector("p.picture-of-the-day-description") ).toBeTruthy(); }); it("opens the source page in a new tab from the attribution link", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, attributedState); const sourceLink = container.querySelector( '[data-l10n-id="newtab-picture-attribution-source-link"]' ); fireEvent.click(sourceLink); const openLink = findOpenLink(dispatch); expect(openLink[0].data.url).toBe(SOURCE_URL); expect(openLink[0].data.where).toBe("tab"); }); it("opens the license page in a new tab from the license link", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, attributedState); fireEvent.click( container.querySelector( '[data-l10n-id="newtab-picture-attribution-license"]' ) ); expect(findOpenLink(dispatch)[0].data.url).toBe(LICENSE_URL); }); it("records an open_license user event when the license link is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, attributedState); fireEvent.click( container.querySelector( '[data-l10n-id="newtab-picture-attribution-license"]' ) ); const evt = dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "open_license" ); expect(evt).toBeTruthy(); expect(evt[0].data.widget_name).toBe("picture_of_the_day"); }); it("opens the source when the image is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, attributedState); fireEvent.click( container.querySelector(".picture-of-the-day-image-link") ); expect(findOpenLink(dispatch)[0].data.url).toBe(SOURCE_URL); }); it("omits attribution parts and links whose fields are absent", () => { const { container } = renderWidget( jest.fn(), {}, { ...attributedState, PictureOfTheDay: { ...attributedState.PictureOfTheDay, author: "", sourceUrl: "", licenseLabel: "", licenseUrl: "", }, } ); expect( container.querySelector(".picture-of-the-day-attribution") ).toBeFalsy(); expect( container.querySelector(".picture-of-the-day-image-link") ).toBeFalsy(); expect( container.querySelector('p[data-l10n-id="newtab-picture-header-main"]') ).toBeTruthy(); }); }); describe("telemetry (Bug 2050977)", () => { const populated = { ...mockState, PictureOfTheDay: { ...INITIAL_STATE.PictureOfTheDay, imageUrl: "https://example.com/potd.jpg", title: "Test picture", description: "A test picture description.", publishedDate: "2026-07-01", }, }; it("records a widgets_enabled=false event when hidden from the menu", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch); fireEvent.click( container.querySelector('[data-l10n-id="newtab-widget-menu-hide"]') ); const enabled = dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_ENABLED ); expect(enabled).toBeTruthy(); expect(enabled[0].data).toMatchObject({ widget_name: "picture_of_the_day", enabled: false, }); }); it("records a set_wallpaper user event when Set wallpaper is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget(dispatch, {}, populated); fireEvent.click( container.querySelector(".picture-of-the-day-set-wallpaper") ); const evt = dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "set_wallpaper" ); expect(evt).toBeTruthy(); expect(evt[0].data.widget_name).toBe("picture_of_the_day"); }); it("records an open_source user event when the source line is clicked", () => { const dispatch = jest.fn(); const { container } = renderWidget( dispatch, {}, { ...populated, PictureOfTheDay: { ...populated.PictureOfTheDay, sourceUrl: "https://commons.wikimedia.org/wiki/File:Example.jpg", }, } ); fireEvent.click( container.querySelector(".picture-of-the-day-source-link") ); const evt = dispatch.mock.calls.find( ([action]) => action.type === at.WIDGETS_USER_EVENT && action.data?.user_action === "open_source" ); expect(evt).toBeTruthy(); expect(evt[0].data.widget_name).toBe("picture_of_the_day"); }); }); describe("records interaction on every action (cross-widget ask)", () => { const populated = { ...mockState, PictureOfTheDay: { ...INITIAL_STATE.PictureOfTheDay, imageUrl: "https://example.com/potd.jpg", title: "Test picture", description: "A test picture description.", publishedDate: "2026-07-01", }, }; const sourced = { ...populated, PictureOfTheDay: { ...populated.PictureOfTheDay, sourceUrl: "https://commons.wikimedia.org/wiki/File:Example.jpg", licenseLabel: "CC BY-SA 4.0", licenseUrl: "https://creativecommons.org/licenses/by-sa/4.0/", }, }; it.each([ [ "learn more", mockState, '[data-l10n-id="newtab-picture-menu-learn-more"]', ], [ "manage wallpaper", mockState, '[data-l10n-id="newtab-picture-menu-manage-wallpaper"]', ], ["show (eye)", mockState, ".picture-of-the-day-show-button"], [ "change size", mockState, '#picture-of-the-day-size-submenu [data-size="large"]', ], ["set wallpaper", populated, ".picture-of-the-day-set-wallpaper"], [ "hide today's picture", populated, '[data-l10n-id="newtab-picture-menu-hide-photo"]', ], ["open source", sourced, ".picture-of-the-day-source-link"], [ "open license", sourced, '[data-l10n-id="newtab-picture-attribution-license"]', ], ])( "flips widgets.pictureOfTheDay.interaction when %s is used", (_label, state, selector) => { const { container, handleUserInteraction } = renderWidget( jest.fn(), {}, state ); fireEvent.click(container.querySelector(selector)); expect(handleUserInteraction).toHaveBeenCalledWith("pictureOfTheDay"); } ); }); });