/* 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, act } 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 { Clocks } from "content-src/components/Widgets/Clocks/Clocks";
import { isValidPaletteName } from "content-src/components/Widgets/Clocks/ClocksHelpers";
jest.mock("content-src/components/Widgets/Clocks/ClocksHelpers.mjs", () => ({
...jest.requireActual(
"content-src/components/Widgets/Clocks/ClocksHelpers.mjs"
),
getRandomLabelColor: jest.fn().mockReturnValue("cyan"),
}));
// Stub Intl.DateTimeFormat().resolvedOptions().timeZone so tests are
// deterministic regardless of the CI/developer machine's local zone. We
// pin the local zone to Europe/Berlin to match the default sample.
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
beforeAll(() => {
Intl.DateTimeFormat.prototype.resolvedOptions = function () {
const opts = originalResolvedOptions.call(this);
return { ...opts, timeZone: "Europe/Berlin" };
};
});
afterAll(() => {
Intl.DateTimeFormat.prototype.resolvedOptions = originalResolvedOptions;
});
const mockState = {
...INITIAL_STATE,
Prefs: {
...INITIAL_STATE.Prefs,
values: {
...INITIAL_STATE.Prefs.values,
"widgets.system.enabled": true,
"widgets.enabled": true,
"widgets.system.clocks.enabled": true,
"widgets.clocks.enabled": true,
"widgets.clocks.size": "large",
},
},
};
function WrapWithProvider({ children, state = INITIAL_STATE }) {
const store = createStore(combineReducers(reducers), state);
return {children};
}
function renderClocks(size = "large", state = mockState, dispatch = jest.fn()) {
const { container, unmount, rerender } = render(
);
return { container, unmount, rerender, dispatch };
}
const withClockZones = zones => ({
...mockState,
Prefs: {
...mockState.Prefs,
values: {
...mockState.Prefs.values,
"widgets.clocks.zones": JSON.stringify(zones),
},
},
});
describe(" (Widgets/Clocks)", () => {
describe("rendering", () => {
it("renders exactly four clock rows (hard-coded default set)", () => {
const { container } = renderClocks();
expect(container.querySelectorAll(".clocks-row")).toHaveLength(4);
});
it("shows the add button when fewer than four clocks are saved", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
expect(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
).toBeInTheDocument();
});
it("hides the add button when four clocks are already saved", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "Australia/Sydney", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
{ timeZone: "America/Los_Angeles", label: null, labelColor: null },
])
);
expect(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
).not.toBeInTheDocument();
expect(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-menu-button']"
)
).toBeInTheDocument();
});
it("renders the default cities in order (Berlin, Sydney, New York, Los Angeles)", () => {
const { container } = renderClocks();
const rows = container.querySelectorAll(".clocks-row");
const cities = Array.from(rows).map(
r => r.querySelector(".clocks-city").textContent
);
expect(cities).toEqual(["Berlin", "Sydney", "New York", "Los Angeles"]);
});
it("renders saved clock zones in pref order and preserves duplicate zones", () => {
const state = {
...mockState,
Prefs: {
...mockState.Prefs,
values: {
...mockState.Prefs.values,
"widgets.clocks.zones": JSON.stringify([
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "America/New_York",
city: "New York",
label: "Family",
labelColor: "green",
},
]),
},
},
};
const { container } = renderClocks("large", state);
const cities = Array.from(container.querySelectorAll(".clocks-city")).map(
el => el.textContent
);
const labels = Array.from(
container.querySelectorAll(".clocks-label-chip")
).map(el => el.textContent);
expect(cities).toEqual(["Boston", "New York"]);
expect(labels).toEqual(["Office", "Family"]);
});
it("backfills missing label colors for saved nickname clocks", () => {
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: null,
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { dispatch } = renderClocks("large", withClockZones(savedZones));
const setPrefCall = dispatch.mock.calls.find(
([action]) =>
action.type === at.SET_PREF &&
action.data?.name === "widgets.clocks.zones"
)?.[0];
expect(setPrefCall).toEqual(
expect.objectContaining({
type: at.SET_PREF,
data: expect.objectContaining({
name: "widgets.clocks.zones",
}),
})
);
const persistedZones = JSON.parse(setPrefCall.data.value);
expect(persistedZones[0]).toMatchObject({
timeZone: "America/New_York",
city: "Boston",
label: "Office",
});
expect(isValidPaletteName(persistedZones[0].labelColor)).toBe(true);
expect(persistedZones[1]).toEqual(savedZones[1]);
});
it("does not dispatch a backfill SET_PREF when every labeled clock already has a color", () => {
// Guards against a future refactor accidentally re-firing the
// backfill on every render.
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { dispatch } = renderClocks("large", withClockZones(savedZones));
const zonesPrefCall = dispatch.mock.calls.find(
([action]) =>
action.type === at.SET_PREF &&
action.data?.name === "widgets.clocks.zones"
);
expect(zonesPrefCall).toBeUndefined();
});
it("falls back to default clock zones when the saved pref is invalid", () => {
const state = {
...mockState,
Prefs: {
...mockState.Prefs,
values: {
...mockState.Prefs.values,
"widgets.clocks.zones": "{",
},
},
};
const { container } = renderClocks("large", state);
const cities = Array.from(container.querySelectorAll(".clocks-city")).map(
el => el.textContent
);
expect(cities).toEqual(["Berlin", "Sydney", "New York", "Los Angeles"]);
});
it("applies the size-specific class to the article root", () => {
expect(
renderClocks("small").container.querySelector(
".clocks-widget.small-widget"
)
).toBeInTheDocument();
expect(
renderClocks("medium").container.querySelector(
".clocks-widget.medium-widget"
)
).toBeInTheDocument();
expect(
renderClocks("large").container.querySelector(
".clocks-widget.large-widget"
)
).toBeInTheDocument();
});
it("defaults to medium size when the size prop is falsy", () => {
// Pass null rather than undefined — undefined would let the renderClocks
// default ("large") kick in, which is not what we want to test.
const { container } = renderClocks(null);
expect(
container.querySelector(".clocks-widget.medium-widget")
).toBeInTheDocument();
});
it("renders IATA abbreviations in small and medium sizes", () => {
const smallCities = Array.from(
renderClocks("small").container.querySelectorAll(".clocks-city")
).map(el => el.textContent);
expect(smallCities).toEqual(["BER", "SYD", "NYC", "LAX"]);
const mediumCities = Array.from(
renderClocks("medium").container.querySelectorAll(".clocks-city")
).map(el => el.textContent);
expect(mediumCities).toEqual(["BER", "SYD", "NYC", "LAX"]);
});
it("renders label chips only in Large size", () => {
const labeledZones = withClockZones([
{ timeZone: "Europe/Berlin", label: "Home", labelColor: "cyan" },
{ timeZone: "Australia/Sydney", label: "Work", labelColor: "green" },
{ timeZone: "America/New_York", label: "NYC", labelColor: "yellow" },
{ timeZone: "America/Los_Angeles", label: "LA", labelColor: "purple" },
]);
const large = renderClocks("large", labeledZones).container;
expect(large.querySelectorAll(".clocks-label-chip").length).toBe(4);
expect(
renderClocks("small", labeledZones).container.querySelectorAll(
".clocks-label-chip"
).length
).toBe(0);
expect(
renderClocks("medium", labeledZones).container.querySelectorAll(
".clocks-label-chip"
).length
).toBe(0);
});
it("applies saved palette colors to label chips", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: "Home", labelColor: "cyan" },
{ timeZone: "Australia/Sydney", label: "Work", labelColor: "green" },
{ timeZone: "America/New_York", label: "NYC", labelColor: "yellow" },
{
timeZone: "America/Los_Angeles",
label: "LA",
labelColor: "purple",
},
])
);
const chips = Array.from(
container.querySelectorAll(".clocks-label-chip")
);
const paletteClasses = [];
for (const el of chips) {
const match = Array.from(el.classList).find(c =>
c.startsWith("clocks-chip-")
);
paletteClasses.push(match);
}
expect(paletteClasses).toEqual([
"clocks-chip-cyan",
"clocks-chip-green",
"clocks-chip-yellow",
"clocks-chip-purple",
]);
});
it("sets an aria-label on each clock row with full city + TZ + time", () => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2026-04-20T13:44:00Z"));
try {
const { container } = renderClocks("small");
const rows = container.querySelectorAll(".clocks-row");
// The UI abbreviates the city in Small size ("BER") but the aria-label
// always uses the full city name for screen readers.
expect(rows[0].getAttribute("aria-label")).toMatch(/^Berlin, /);
expect(rows[3].getAttribute("aria-label")).toMatch(/^Los Angeles, /);
} finally {
jest.useRealTimers();
}
});
it("prefixes the label in the aria-label for large-size labeled clocks", () => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2026-04-20T13:44:00Z"));
try {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: "Home", labelColor: "cyan" },
{
timeZone: "America/Los_Angeles",
label: "Family",
labelColor: "green",
},
])
);
const rows = container.querySelectorAll(".clocks-row");
expect(rows[0].getAttribute("aria-label")).toMatch(/^Home, Berlin, /);
expect(rows[1].getAttribute("aria-label")).toMatch(
/^Family, Los Angeles, /
);
} finally {
jest.useRealTimers();
}
});
});
describe("live time", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2026-04-20T13:44:00Z"));
});
afterEach(() => {
jest.useRealTimers();
});
it("advances the displayed time on each minute boundary (self-rescheduling timeout)", () => {
const { container } = renderClocks();
// Extract just the minute portion — the hour differs per zone and per
// locale's 12/24h default, but the minute value is the same across all
// four clocks and is what this test actually cares about.
const minutes = () =>
Array.from(container.querySelectorAll(".clocks-time")).map(
el => el.textContent.match(/:(\d{2})/)?.[1]
);
// Initial tick fires synchronously inside the useEffect; system time is
// pinned to 13:44:00Z in beforeEach.
expect(minutes()).toEqual(["44", "44", "44", "44"]);
act(() => {
jest.advanceTimersByTime(60_000);
});
expect(minutes()).toEqual(["45", "45", "45", "45"]);
// The second advance only changes the display if the first tick
// rescheduled itself. A broken one-shot setTimeout would leave the
// widget frozen at :45.
act(() => {
jest.advanceTimersByTime(60_000);
});
expect(minutes()).toEqual(["46", "46", "46", "46"]);
});
it("has no pending timers after unmount", () => {
const { unmount } = renderClocks();
expect(jest.getTimerCount()).toBeGreaterThan(0);
unmount();
expect(jest.getTimerCount()).toBe(0);
});
});
describe("impression telemetry", () => {
let originalIntersectionObserver;
let lastCallback;
beforeEach(() => {
originalIntersectionObserver = globalThis.IntersectionObserver;
globalThis.IntersectionObserver = class {
constructor(cb) {
lastCallback = cb;
}
observe() {}
unobserve() {}
disconnect() {}
};
});
afterEach(() => {
globalThis.IntersectionObserver = originalIntersectionObserver;
lastCallback = undefined;
});
it("fires WIDGETS_IMPRESSION once when the widget intersects", () => {
const { container, dispatch } = renderClocks("large");
const widget = container.querySelector(".clocks-widget");
act(() => {
lastCallback([{ target: widget, isIntersecting: true }]);
});
const impressions = dispatch.mock.calls.filter(
([a]) => a.type === at.WIDGETS_IMPRESSION
);
expect(impressions).toHaveLength(1);
expect(impressions[0][0]).toMatchObject({
type: at.WIDGETS_IMPRESSION,
data: { widget_name: "clocks", widget_size: "large" },
});
});
it("does not fire WIDGETS_IMPRESSION on subsequent intersections", () => {
const { container, dispatch } = renderClocks();
const widget = container.querySelector(".clocks-widget");
act(() => {
lastCallback([{ target: widget, isIntersecting: true }]);
lastCallback([{ target: widget, isIntersecting: true }]);
});
const impressions = dispatch.mock.calls.filter(
([a]) => a.type === at.WIDGETS_IMPRESSION
);
expect(impressions).toHaveLength(1);
});
it("does not fire when isIntersecting is false", () => {
const { container, dispatch } = renderClocks();
const widget = container.querySelector(".clocks-widget");
act(() => {
lastCallback([{ target: widget, isIntersecting: false }]);
});
const impressions = dispatch.mock.calls.filter(
([a]) => a.type === at.WIDGETS_IMPRESSION
);
expect(impressions).toHaveLength(0);
});
});
describe("context menu", () => {
it("renders the context menu button with the clock-specific a11y label", () => {
const { container } = renderClocks();
expect(
container.querySelector(
".clocks-context-menu-button[data-l10n-id='newtab-clock-widget-menu-button']"
)
).toBeInTheDocument();
});
it("contains Change size submenu with small, medium, large items", () => {
const { container } = renderClocks();
expect(
container.querySelector(
"span[data-l10n-id='newtab-widget-menu-change-size']"
)
).toBeInTheDocument();
["small", "medium", "large"].forEach(s => {
expect(
container.querySelector(
`panel-item[data-l10n-id='newtab-widget-size-${s}']`
)
).toBeInTheDocument();
});
});
it("checks the current size in the submenu", () => {
const { container } = renderClocks("large");
expect(
container
.querySelector("panel-item[data-l10n-id='newtab-widget-size-large']")
.hasAttribute("checked")
).toBe(true);
expect(
container
.querySelector("panel-item[data-l10n-id='newtab-widget-size-small']")
.hasAttribute("checked")
).toBe(false);
});
it("contains hide (singular 'Hide clock') and learn-more items", () => {
const { container } = renderClocks();
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-hide']"
)
).toBeInTheDocument();
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-learn-more']"
)
).toBeInTheDocument();
});
it("clears is-dismissed class when the mouse leaves the widget", () => {
const { container } = renderClocks();
const widget = container.querySelector(".clocks-widget");
const item = container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-hide']"
);
fireEvent.click(item);
expect(widget.classList.contains("is-dismissed")).toBe(true);
fireEvent.mouseLeave(widget);
expect(widget.classList.contains("is-dismissed")).toBe(false);
});
});
describe("context menu actions & telemetry", () => {
it("dispatches SET_PREF(widgets.clocks.size) and WIDGETS_USER_EVENT on submenu size click", () => {
const { container, dispatch } = renderClocks();
const submenuNode = container.querySelector(
"panel-list[id='clocks-size-submenu']"
);
const mockItem = document.createElement("div");
mockItem.dataset.size = "small";
const event = new MouseEvent("click", { bubbles: true });
Object.defineProperty(event, "composedPath", {
value: () => [mockItem],
});
act(() => {
submenuNode.dispatchEvent(event);
});
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toMatchObject({
type: at.SET_PREF,
data: { name: "widgets.clocks.size", value: "small" },
});
expect(dispatch.mock.calls[1][0]).toMatchObject({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "change_size",
action_value: "small",
widget_size: "small",
}),
});
});
it("dispatches SET_PREF(widgets.clocks.enabled, false) and WIDGETS_ENABLED on hide click", () => {
const { container, dispatch } = renderClocks();
const item = container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-hide']"
);
fireEvent.click(item);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toMatchObject({
type: at.SET_PREF,
data: { name: "widgets.clocks.enabled", value: false },
});
expect(dispatch.mock.calls[1][0]).toMatchObject({
type: at.WIDGETS_ENABLED,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
enabled: false,
widget_size: "large",
}),
});
});
it("dispatches OPEN_LINK and WIDGETS_USER_EVENT on learn-more click", () => {
const { container, dispatch } = renderClocks();
const item = container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-learn-more']"
);
fireEvent.click(item);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toMatchObject({
type: at.OPEN_LINK,
data: {
url: "https://support.mozilla.org/kb/firefox-new-tab-widgets",
},
});
expect(dispatch.mock.calls[1][0]).toMatchObject({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "learn_more",
widget_size: "large",
}),
});
});
});
describe("add clock flow", () => {
it("hides the '+' button at the default 4-clock max", () => {
const { container } = renderClocks();
const addButton = container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
);
expect(addButton).not.toBeInTheDocument();
});
it("opens the add clock form when fewer than four clocks are saved", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
const addButton = container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
);
expect(addButton.hasAttribute("disabled")).toBe(false);
fireEvent.click(addButton);
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
expect(
container.querySelector(
"moz-input-search[data-l10n-id='newtab-clock-widget-search-location-input']"
)
).toBeInTheDocument();
});
it("shows filtered location results when a search query is entered", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Ber",
});
fireEvent.input(searchInput);
expect(
container.querySelector(
".clocks-search-result .clocks-search-result-city"
)
).toBeInTheDocument();
});
it("dispatches SET_PREF when an exact city name is entered and Add is clicked", () => {
const savedZones = [
{ timeZone: "Australia/Sydney", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([
...savedZones,
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
]),
},
})
);
});
it("dispatches WIDGETS_USER_EVENT with add_clock when saving a new clock", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Australia/Sydney", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "toolbar",
user_action: "add_clock",
widget_size: "large",
}),
})
);
});
it("filters results and dispatches SET_PREF when a result then Add is clicked", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
// moz-input-search is a custom element without a native value setter;
// define value as a writable own property so React can also update it.
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Syd",
});
fireEvent.input(searchInput);
const sydneyButton = Array.from(
container.querySelectorAll(".clocks-search-result")
).find(
el =>
el.querySelector(".clocks-search-result-city")?.textContent ===
"Sydney"
);
expect(sydneyButton).toBeInTheDocument();
// Clicking a result selects the location; clicking Add clock saves it.
fireEvent.click(sydneyButton);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([
...savedZones,
{
timeZone: "Australia/Sydney",
city: "Sydney",
label: null,
labelColor: null,
},
]),
},
})
);
});
it("closes the form after adding a clock", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
const addButton = container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
);
fireEvent.click(addButton);
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(
container.querySelector(".clocks-add-form")
).not.toBeInTheDocument();
});
it("Add clock button inside the form is disabled until a location is selected", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const addClockButton = container.querySelector(
"moz-button.clocks-form-submit"
);
expect(addClockButton.hasAttribute("disabled")).toBe(true);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
expect(addClockButton.hasAttribute("disabled")).toBe(false);
});
it("includes the nickname as label when one is entered", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Syd",
});
fireEvent.input(searchInput);
fireEvent.click(
Array.from(container.querySelectorAll(".clocks-search-result")).find(
el =>
el.querySelector(".clocks-search-result-city")?.textContent ===
"Sydney"
)
);
const nicknameInput = container.querySelector(".clocks-nickname-input");
Object.defineProperty(nicknameInput, "value", {
configurable: true,
writable: true,
value: "Work",
});
fireEvent.input(nicknameInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([
...savedZones,
{
timeZone: "Australia/Sydney",
city: "Sydney",
label: "Work",
labelColor: "cyan",
},
]),
},
})
);
});
it("dispatches WIDGETS_USER_EVENT with add_nickname when saving a clock with a nickname", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Syd",
});
fireEvent.input(searchInput);
fireEvent.click(
Array.from(container.querySelectorAll(".clocks-search-result")).find(
el =>
el.querySelector(".clocks-search-result-city")?.textContent ===
"Sydney"
)
);
const nicknameInput = container.querySelector(".clocks-nickname-input");
Object.defineProperty(nicknameInput, "value", {
configurable: true,
writable: true,
value: "Work",
});
fireEvent.input(nicknameInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "toolbar",
user_action: "add_nickname",
widget_size: "large",
}),
})
);
});
it("does not dispatch add_nickname when saving a clock without a nickname", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Australia/Sydney", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
const nicknameEvents = dispatch.mock.calls.filter(
([action]) =>
action.type === at.WIDGETS_USER_EVENT &&
action.data?.user_action === "add_nickname"
);
expect(nicknameEvents).toHaveLength(0);
});
it("does not dispatch add_nickname when editing a clock that already has a label", () => {
const savedZones = [
{
timeZone: "Europe/Berlin",
label: "Home",
labelColor: "cyan",
},
{
timeZone: "America/New_York",
label: null,
labelColor: null,
},
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(container.querySelector(".clocks-edit-item-edit-button"));
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
const nicknameEvents = dispatch.mock.calls.filter(
([action]) =>
action.type === at.WIDGETS_USER_EVENT &&
action.data?.user_action === "add_nickname"
);
expect(nicknameEvents).toHaveLength(0);
});
it("dispatches add_nickname when editing an unlabeled clock and adding a label", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(container.querySelector(".clocks-edit-item-edit-button"));
const nicknameInput = container.querySelector(".clocks-nickname-input");
Object.defineProperty(nicknameInput, "value", {
configurable: true,
writable: true,
value: "Work",
});
fireEvent.input(nicknameInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
const nicknameEvents = dispatch.mock.calls.filter(
([action]) =>
action.type === at.WIDGETS_USER_EVENT &&
action.data?.user_action === "add_nickname"
);
expect(nicknameEvents).toHaveLength(1);
expect(nicknameEvents[0][0]).toMatchObject({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "manage",
user_action: "add_nickname",
widget_size: "large",
}),
});
});
it("limits nickname labels to 11 characters", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Syd",
});
fireEvent.input(searchInput);
fireEvent.click(
Array.from(container.querySelectorAll(".clocks-search-result")).find(
el =>
el.querySelector(".clocks-search-result-city")?.textContent ===
"Sydney"
)
);
const nicknameInput = container.querySelector(".clocks-nickname-input");
Object.defineProperty(nicknameInput, "value", {
configurable: true,
writable: true,
value: "Very Long Work Label",
});
fireEvent.input(nicknameInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([
...savedZones,
{
timeZone: "Australia/Sydney",
city: "Sydney",
label: "Very Long W",
labelColor: "cyan",
},
]),
},
})
);
});
it("Cancel button closes the form without saving", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-cancel']"
)
);
expect(
container.querySelector(".clocks-add-form")
).not.toBeInTheDocument();
expect(dispatch).not.toHaveBeenCalledWith(
expect.objectContaining({ type: at.SET_PREF })
);
});
it("pressing Enter on Cancel does not save the form", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
dispatch.mockClear();
fireEvent.keyDown(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-cancel']"
),
{ key: "Enter" }
);
expect(dispatch).not.toHaveBeenCalledWith(
expect.objectContaining({ type: at.SET_PREF })
);
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
});
it("keeps the form open when blur has no next focused element", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
fireEvent.blur(container.querySelector(".clocks-add-form"), {
relatedTarget: null,
});
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
});
it("closes the form on Escape", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
fireEvent.keyDown(container.querySelector(".clocks-add-form"), {
key: "Escape",
});
expect(
container.querySelector(".clocks-add-form")
).not.toBeInTheDocument();
});
it("pressing Enter in the form saves the clock without clicking Add", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
fireEvent.keyDown(container.querySelector(".clocks-add-form"), {
key: "Enter",
});
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: expect.objectContaining({ name: "widgets.clocks.zones" }),
})
);
expect(
container.querySelector(".clocks-add-form")
).not.toBeInTheDocument();
});
it("expands a small widget to large while the add panel is open", () => {
const { container } = renderClocks(
"small",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
expect(
container.querySelector(".clocks-widget.small-widget")
).toBeInTheDocument();
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-add']"
)
);
expect(
container.querySelector(
".clocks-widget.large-widget.is-clock-form-open"
)
).toBeInTheDocument();
fireEvent.click(
container.querySelector(
"moz-button[data-l10n-id='newtab-clock-widget-button-cancel']"
)
);
expect(
container.querySelector(".clocks-widget.small-widget")
).toBeInTheDocument();
});
it("Edit clocks opens the manage view instead of the add form", () => {
const { container } = renderClocks(
"large",
withClockZones([
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
expect(container.querySelector(".clocks-edit-panel")).toBeInTheDocument();
expect(
container.querySelector(".clocks-add-form")
).not.toBeInTheDocument();
expect(
container.querySelector(".clocks-edit-back-button")
).toBeInTheDocument();
expect(container.querySelectorAll(".clocks-edit-item")).toHaveLength(2);
});
it("dispatches WIDGETS_USER_EVENT with expand when the edit panel opens", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "expand",
widget_size: "large",
}),
})
);
});
it("dispatches WIDGETS_USER_EVENT with collapse when the back button closes the edit panel", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
dispatch.mockClear();
fireEvent.click(
container.querySelector(
".clocks-edit-panel moz-button.clocks-edit-back-button"
)
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "collapse",
widget_size: "large",
}),
})
);
});
it("preserves the panel open source across an in-panel form save when emitting collapse", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Australia/Sydney", label: null, labelColor: null },
])
);
// Open the edit (manage) panel via the context menu.
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
// Open the add form from inside the manage panel.
fireEvent.click(container.querySelector(".clocks-edit-add-button"));
// Save a new clock from the in-panel form.
const searchInput = container.querySelector(
".clocks-search-location-input"
);
Object.defineProperty(searchInput, "value", {
configurable: true,
writable: true,
value: "Berlin",
});
fireEvent.input(searchInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
dispatch.mockClear();
// Close the manage panel via the back button.
fireEvent.click(
container.querySelector(
".clocks-edit-panel moz-button.clocks-edit-back-button"
)
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "collapse",
widget_size: "large",
}),
})
);
});
it("closes the edit panel on Escape", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.keyDown(container.querySelector(".clocks-edit-panel"), {
key: "Escape",
});
expect(
container.querySelector(".clocks-edit-panel")
).not.toBeInTheDocument();
});
it("each clock item in the edit panel has tabIndex=0 so keyboard focus reveals its action buttons", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
const editItems = container.querySelectorAll(".clocks-edit-item");
expect(editItems.length).toBeGreaterThan(0);
editItems.forEach(item => {
expect(item.getAttribute("tabindex")).toBe("0");
});
});
it("expands a medium widget to large while the edit panel is open", () => {
const { container } = renderClocks(
"medium",
withClockZones([
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
])
);
expect(
container.querySelector(".clocks-widget.medium-widget")
).toBeInTheDocument();
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
expect(
container.querySelector(".clocks-widget.large-widget.is-editing-clocks")
).toBeInTheDocument();
fireEvent.click(
container.querySelector(
".clocks-edit-panel moz-button.clocks-edit-back-button"
)
);
expect(
container.querySelector(".clocks-widget.medium-widget")
).toBeInTheDocument();
});
it("shows an add button in the edit view when more clocks can be added", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
expect(
container.querySelector(".clocks-edit-header .clocks-edit-add-button")
).toBeInTheDocument();
});
it("opens the clock form in Save mode from the manage view and updates the clock", () => {
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(container.querySelector(".clocks-edit-item-edit-button"));
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
expect(
container.querySelector("moz-button.clocks-form-submit")
).toBeInTheDocument();
const nicknameInput = container.querySelector(".clocks-nickname-input");
Object.defineProperty(nicknameInput, "value", {
configurable: true,
writable: true,
value: "HQ",
});
fireEvent.input(nicknameInput);
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([
{
timeZone: "America/New_York",
city: "Boston",
label: "HQ",
labelColor: "cyan",
},
savedZones[1],
]),
},
})
);
expect(container.querySelector(".clocks-edit-panel")).toBeInTheDocument();
});
it("dispatches WIDGETS_USER_EVENT with edit_clock when saving an edited clock", () => {
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(container.querySelector(".clocks-edit-item-edit-button"));
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "manage",
user_action: "edit_clock",
widget_size: "large",
}),
})
);
});
it("removes a clock from the manage view while keeping the panel open", () => {
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(
container.querySelector(".clocks-edit-item-remove-button")
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.SET_PREF,
data: {
name: "widgets.clocks.zones",
value: JSON.stringify([savedZones[1]]),
},
})
);
expect(container.querySelector(".clocks-edit-panel")).toBeInTheDocument();
});
it("dispatches WIDGETS_USER_EVENT with remove_clock when removing a clock from the manage view", () => {
const savedZones = [
{
timeZone: "America/New_York",
city: "Boston",
label: "Office",
labelColor: "cyan",
},
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-edit']"
)
);
fireEvent.click(
container.querySelector(".clocks-edit-item-remove-button")
);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "manage",
user_action: "remove_clock",
widget_size: "large",
}),
})
);
});
it("dispatches WIDGETS_USER_EVENT with remove_clock and widget_source 'row' when using the inline remove button", () => {
const savedZones = [
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
];
const { container, dispatch } = renderClocks(
"large",
withClockZones(savedZones)
);
fireEvent.click(container.querySelector(".clocks-row-remove-button"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "row",
user_action: "remove_clock",
widget_size: "large",
}),
})
);
});
it("shows only the inline edit action when one clock is visible", () => {
const { container } = renderClocks(
"large",
withClockZones([
{
timeZone: "Europe/Berlin",
city: "Berlin",
label: null,
labelColor: null,
},
])
);
expect(
container.querySelector(".clocks-row-edit-button")
).toBeInTheDocument();
expect(
container.querySelector(".clocks-row-remove-button")
).not.toBeInTheDocument();
});
it("does not render inline row actions in the small widget", () => {
const { container } = renderClocks(
"small",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
expect(
container.querySelector(".clocks-row-edit-button")
).not.toBeInTheDocument();
expect(
container.querySelector(".clocks-row-remove-button")
).not.toBeInTheDocument();
});
it("clicking the inline row edit button opens the clock form", () => {
const { container } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
fireEvent.click(container.querySelector(".clocks-row-edit-button"));
expect(container.querySelector(".clocks-add-form")).toBeInTheDocument();
expect(
container.querySelector("moz-button.clocks-form-submit")
).toBeInTheDocument();
});
it("dispatches WIDGETS_USER_EVENT with edit_clock and widget_source 'row' when saving after inline row edit", () => {
const { container, dispatch } = renderClocks(
"large",
withClockZones([
{ timeZone: "Europe/Berlin", label: null, labelColor: null },
{ timeZone: "America/New_York", label: null, labelColor: null },
])
);
fireEvent.click(container.querySelector(".clocks-row-edit-button"));
fireEvent.click(container.querySelector("moz-button.clocks-form-submit"));
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "row",
user_action: "edit_clock",
widget_size: "large",
}),
})
);
});
});
describe("hour format toggle", () => {
function renderWithHourFormatPref(prefValue) {
const state = {
...mockState,
Prefs: {
...mockState.Prefs,
values: {
...mockState.Prefs.values,
"widgets.clocks.hourFormat": prefValue,
},
},
};
return renderClocks("large", state);
}
it("shows 'Switch to 24h' when pref is '12'", () => {
const { container } = renderWithHourFormatPref("12");
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-switch-to-24h']"
)
).toBeInTheDocument();
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-switch-to-12h']"
)
).not.toBeInTheDocument();
});
it("shows 'Switch to 12h' when pref is '24'", () => {
const { container } = renderWithHourFormatPref("24");
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-switch-to-12h']"
)
).toBeInTheDocument();
expect(
container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-switch-to-24h']"
)
).not.toBeInTheDocument();
});
it("flips the pref and fires WIDGETS_USER_EVENT on toggle click (12 -> 24)", () => {
const state = {
...mockState,
Prefs: {
...mockState.Prefs,
values: {
...mockState.Prefs.values,
"widgets.clocks.hourFormat": "12",
},
},
};
const { container, dispatch } = renderClocks("large", state);
const item = container.querySelector(
"panel-item[data-l10n-id='newtab-clock-widget-menu-switch-to-24h']"
);
fireEvent.click(item);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toMatchObject({
type: at.SET_PREF,
data: { name: "widgets.clocks.hourFormat", value: "24" },
});
expect(dispatch.mock.calls[1][0]).toMatchObject({
type: at.WIDGETS_USER_EVENT,
data: expect.objectContaining({
widget_name: "clocks",
widget_source: "context_menu",
user_action: "change_hour_format",
action_value: "24",
widget_size: "large",
}),
});
});
});
});