// @vitest-environment jsdom // // Interaction contract of the shared PriorityStepper (channel manager cell + detail header). i18n is // mocked to a key-passthrough so aria-label lookups read stable keys, not translations. The visual // size/border split (`sm` vs `md`) is CSS-only, so we assert behaviour, not styling. import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { PriorityStepper } from "./PriorityStepper"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); afterEach(() => { cleanup(); vi.useRealTimers(); // any fake-timer test resets here so it can't leak into the next }); const raise = () => screen.getByRole("button", { name: "common.priority.raise" }); const lower = () => screen.getByRole("button", { name: "common.priority.lower" }); const valueBtn = () => screen.getByRole("button", { name: "common.priority.edit" }); describe("PriorityStepper", () => { it("hands the focus back to the value after Enter or Escape, not to ", () => { const onSet = vi.fn(); render( , ); for (const key of ["Escape", "Enter"]) { fireEvent.click(valueBtn()); const field = screen.getByRole("spinbutton"); field.focus(); fireEvent.keyDown(field, { key }); expect(document.activeElement).toBe(valueBtn()); } }); it("shows the current priority and both step buttons", () => { render( , ); expect(valueBtn().textContent).toBe("12"); expect(raise()).toBeTruthy(); expect(lower()).toBeTruthy(); }); it("tapping + / − nudges by ±1", () => { const onPriority = vi.fn(); render( , ); fireEvent.click(raise()); fireEvent.click(lower()); expect(onPriority.mock.calls).toEqual([[1], [-1]]); }); it("clicking the value opens an inline editor seeded with the current value", () => { render( , ); fireEvent.click(valueBtn()); const input = screen.getByRole("spinbutton") as HTMLInputElement; expect(input.value).toBe("7"); }); it("typing a new value and pressing Enter commits it via onSet (negatives allowed)", () => { const onSet = vi.fn(); render( , ); fireEvent.click(valueBtn()); const input = screen.getByRole("spinbutton"); fireEvent.change(input, { target: { value: "-3" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onSet).toHaveBeenCalledWith(-3); }); it("Escape cancels the edit without committing", () => { const onSet = vi.fn(); render( , ); fireEvent.click(valueBtn()); const input = screen.getByRole("spinbutton"); fireEvent.change(input, { target: { value: "99" } }); fireEvent.keyDown(input, { key: "Escape" }); expect(onSet).not.toHaveBeenCalled(); // editor closed → the value button is back expect(valueBtn().textContent).toBe("7"); }); it("committing an unchanged value does not call onSet", () => { const onSet = vi.fn(); render( , ); fireEvent.click(valueBtn()); fireEvent.keyDown(screen.getByRole("spinbutton"), { key: "Enter" }); expect(onSet).not.toHaveBeenCalled(); }); it("renders the same interactive contract at size=md", () => { const onPriority = vi.fn(); render( , ); expect(valueBtn().textContent).toBe("3"); fireEvent.click(raise()); expect(onPriority).toHaveBeenCalledWith(1); }); it("holding a step button auto-repeats (accelerating) and suppresses the trailing click", () => { vi.useFakeTimers(); const onPriority = vi.fn(); render( , ); fireEvent.pointerDown(raise()); vi.advanceTimersByTime(349); expect(onPriority).not.toHaveBeenCalled(); // a tap below the 350ms threshold doesn't repeat vi.advanceTimersByTime(1); // cross the threshold → first auto-repeat tick expect(onPriority).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(1000); // several accelerating repeats while held const heldCount = onPriority.mock.calls.length; expect(heldCount).toBeGreaterThan(3); expect(onPriority.mock.calls.every(([d]) => d === 1)).toBe(true); fireEvent.pointerUp(raise()); vi.advanceTimersByTime(1000); expect(onPriority.mock.calls.length).toBe(heldCount); // stops repeating on release fireEvent.click(raise()); // the click that trails a hold is swallowed (no double-count) expect(onPriority.mock.calls.length).toBe(heldCount); }); it("a quick tap (released before the hold threshold) still nudges exactly once", () => { vi.useFakeTimers(); const onPriority = vi.fn(); render( , ); fireEvent.pointerDown(lower()); vi.advanceTimersByTime(100); // below the threshold → no auto-repeat armed fireEvent.pointerUp(lower()); fireEvent.click(lower()); // the tap's click is NOT suppressed expect(onPriority.mock.calls).toEqual([[-1]]); }); it("clears the auto-repeat timer on unmount (no fire after the row is gone)", () => { vi.useFakeTimers(); const onPriority = vi.fn(); const { unmount } = render( , ); fireEvent.pointerDown(raise()); vi.advanceTimersByTime(350); // repeat armed + first tick const before = onPriority.mock.calls.length; expect(before).toBeGreaterThan(0); unmount(); vi.advanceTimersByTime(1000); expect(onPriority.mock.calls.length).toBe(before); // no post-unmount timer fire }); });