/* 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 { fireEvent, render } from "@testing-library/react"; import { EditClocksPanel } from "content-src/components/Widgets/Clocks/EditClocksPanel"; const DEFAULT_CLOCKS = [ { timeZone: "Europe/Berlin", city: "Berlin", label: "Home", labelColor: "cyan", }, { timeZone: "America/New_York", city: "New York", label: null, labelColor: null, }, ]; function renderPanel(overrides = {}) { const props = { clockZones: DEFAULT_CLOCKS, canAddClock: true, onShowAddClock: jest.fn(), onEditClock: jest.fn(), onRemoveClock: jest.fn(), onClose: jest.fn(), ...overrides, }; const result = render(); return { ...result, props }; } describe("", () => { describe("rendering", () => { it("renders the panel with the back button and title", () => { const { container } = renderPanel(); expect(container.querySelector(".clocks-edit-panel")).toBeInTheDocument(); expect( container.querySelector(".clocks-edit-back-button") ).toBeInTheDocument(); expect( container .querySelector(".clocks-edit-title") .getAttribute("data-l10n-id") ).toBe("newtab-clock-widget-label-your-clocks"); }); it("renders one item per clock with the city name", () => { const { container } = renderPanel(); const items = container.querySelectorAll(".clocks-edit-item"); expect(items.length).toBe(2); expect(items[0].querySelector(".clocks-edit-city").textContent).toBe( "Berlin" ); expect(items[1].querySelector(".clocks-edit-city").textContent).toBe( "New York" ); }); it("falls back to deriving the city from the timezone when clock.city is missing", () => { const { container } = renderPanel({ clockZones: [ { timeZone: "America/Los_Angeles", label: null, labelColor: null, }, ], }); expect(container.querySelector(".clocks-edit-city").textContent).toBe( "Los Angeles" ); }); it("renders the nickname subtitle when a label is set", () => { const { container } = renderPanel(); const subtitle = container .querySelectorAll(".clocks-edit-item")[0] .querySelector(".clocks-edit-subtitle"); expect(subtitle.getAttribute("data-l10n-id")).toBe( "newtab-clock-widget-label-nickname-with-value" ); expect(JSON.parse(subtitle.getAttribute("data-l10n-args"))).toEqual({ nickname: "Home", }); }); it("hides the nickname subtitle from AT when no label is set", () => { const { container } = renderPanel(); const subtitle = container .querySelectorAll(".clocks-edit-item")[1] .querySelector(".clocks-edit-subtitle"); expect(subtitle.getAttribute("aria-hidden")).toBe("true"); expect(subtitle.hasAttribute("data-l10n-id")).toBe(false); }); it("makes each clock item focusable for keyboard hover-reveal", () => { const { container } = renderPanel(); const items = container.querySelectorAll(".clocks-edit-item"); items.forEach(item => { expect(item.getAttribute("tabIndex")).toBe("0"); }); }); }); describe("add affordance", () => { it("renders the add button when canAddClock is true", () => { const { container } = renderPanel({ canAddClock: true }); expect( container.querySelector(".clocks-edit-add-button") ).toBeInTheDocument(); }); it("hides the add button when canAddClock is false", () => { const { container } = renderPanel({ canAddClock: false }); expect(container.querySelector(".clocks-edit-add-button")).toBeNull(); }); it("calls onShowAddClock when the add button is clicked", () => { const { container, props } = renderPanel(); fireEvent.click(container.querySelector(".clocks-edit-add-button")); expect(props.onShowAddClock).toHaveBeenCalledTimes(1); }); }); describe("row actions", () => { it("calls onEditClock with the row index when the edit button is clicked", () => { const { container, props } = renderPanel(); const editButtons = container.querySelectorAll( ".clocks-edit-item-edit-button" ); fireEvent.click(editButtons[1]); expect(props.onEditClock).toHaveBeenCalledWith(1); }); it("calls onRemoveClock with the row index when the remove button is clicked", () => { const { container, props } = renderPanel(); const removeButtons = container.querySelectorAll( ".clocks-edit-item-remove-button" ); fireEvent.click(removeButtons[0]); expect(props.onRemoveClock).toHaveBeenCalledWith(0); }); it("hides the remove button when only one clock remains", () => { const { container } = renderPanel({ clockZones: [DEFAULT_CLOCKS[0]], }); expect( container.querySelector(".clocks-edit-item-remove-button") ).toBeNull(); // The edit button is still rendered. expect( container.querySelector(".clocks-edit-item-edit-button") ).toBeInTheDocument(); }); }); describe("close", () => { it("calls onClose when the back button is clicked", () => { const { container, props } = renderPanel(); fireEvent.click(container.querySelector(".clocks-edit-back-button")); expect(props.onClose).toHaveBeenCalled(); }); it("calls onClose on Escape inside the panel", () => { const { container, props } = renderPanel(); fireEvent.keyDown(container.querySelector(".clocks-edit-panel"), { key: "Escape", }); expect(props.onClose).toHaveBeenCalled(); }); it("does not call onClose for other keys", () => { const { container, props } = renderPanel(); fireEvent.keyDown(container.querySelector(".clocks-edit-panel"), { key: "Enter", }); expect(props.onClose).not.toHaveBeenCalled(); }); }); describe("focus management", () => { it("focuses the back button (and only the back button) after a double rAF on mount", () => { jest.useFakeTimers(); const { container } = renderPanel(); const backButton = container.querySelector(".clocks-edit-back-button"); const backFocusSpy = jest.spyOn(backButton, "focus"); // Spy on the rest of the focusable elements in the panel so this // test fails if focus accidentally lands somewhere else. const otherFocusables = container.querySelectorAll( ".clocks-edit-add-button, .clocks-edit-item, .clocks-edit-item-button" ); const otherSpies = Array.from(otherFocusables).map(el => jest.spyOn(el, "focus") ); try { // Two rAFs are scheduled; advance both. jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers(); expect(backFocusSpy).toHaveBeenCalled(); otherSpies.forEach(spy => expect(spy).not.toHaveBeenCalled()); } finally { backFocusSpy.mockRestore(); otherSpies.forEach(spy => spy.mockRestore()); jest.useRealTimers(); } }); }); });