import { afterEach, expect, rs, test } from "@rstest/core"; import { cleanup, fireEvent, render } from "@testing-library/react"; import { useState, type ReactNode } from "react"; import { ScheduledTaskScheduleInput, type ScheduleValue, } from "@/components/workspace/scheduled-task-schedule-input"; import { enUS } from "@/core/i18n/locales/en-US"; rs.mock("@/core/i18n/hooks", () => ({ useI18n: () => ({ locale: "en-US", t: enUS }), })); // Exercise schedule state with native selects; the page E2E uses real Radix UI. rs.mock("@/components/ui/select", () => ({ Select: ({ value, onValueChange, children, }: { value: string; onValueChange: (value: string) => void; children: ReactNode; }) => ( ), SelectTrigger: () => null, SelectValue: () => null, SelectContent: ({ children }: { children: ReactNode }) => <>{children}, SelectItem: ({ value, children }: { value: string; children: ReactNode }) => ( ), })); afterEach(cleanup); function once(runAt: string, timezone = "America/New_York"): ScheduleValue { return { schedule_type: "once", schedule_spec: { run_at: runAt }, timezone }; } test.each([ "2026-11-01T06:30:00Z", "2027-06-01T12:30:45Z", "2027-06-01T12:30:45.123Z", "2027-06-01T08:30:00-04:00", ])("preserves the original one-time timestamp on mount: %s", (runAt) => { const initial = once(runAt); const onChange = rs.fn(); render( , ); expect(onChange).toHaveBeenLastCalledWith(initial); }); test("parent feedback and title-only rerenders retain the original instant", () => { const original = once("2026-11-01T06:30:00Z"); function Editor() { const [schedule, setSchedule] = useState(original); const [title, setTitle] = useState("Original"); return ( <> setTitle(event.target.value)} /> setSchedule(next)} scheduleTypeLocked /> {schedule.schedule_spec.run_at} ); } const ui = render(); fireEvent.change(ui.getByLabelText("Title"), { target: { value: "Renamed" }, }); expect(ui.getByRole("status").textContent).toBe( original.schedule_spec.run_at, ); fireEvent.change(ui.getByLabelText("Run at"), { target: { value: "2026-11-01T03:30" }, }); expect(ui.getByRole("status").textContent).toBe("2026-11-01T08:30:00+00:00"); // Returning to the original fields must use the original snapshot, not the // latest value passed back through initial by the parent. fireEvent.change(ui.getByLabelText("Run at"), { target: { value: "2026-11-01T01:30" }, }); expect(ui.getByRole("status").textContent).toBe( original.schedule_spec.run_at, ); }); test("changing timezone recomputes the instant and reverting restores seconds", () => { const initial = once("2027-06-01T12:30:45Z"); const onChange = rs.fn(); const ui = render( , ); fireEvent.change(ui.getByRole("combobox"), { target: { value: "Asia/Shanghai" }, }); expect(onChange).toHaveBeenLastCalledWith( once("2027-06-01T00:30:00+00:00", "Asia/Shanghai"), ); fireEvent.change(ui.getByRole("combobox"), { target: { value: "America/New_York" }, }); expect(onChange).toHaveBeenLastCalledWith(initial); }); test("clearing the date removes run_at", () => { const onChange = rs.fn(); const ui = render( , ); fireEvent.change(ui.getByLabelText("Run at"), { target: { value: "" } }); expect(onChange).toHaveBeenLastCalledWith({ schedule_type: "once", schedule_spec: {}, timezone: "America/New_York", }); }); test("empty create form converts a newly entered date", () => { const onChange = rs.fn(); const ui = render( , ); expect(onChange).toHaveBeenLastCalledWith({ schedule_type: "once", schedule_spec: {}, timezone: "Asia/Shanghai", }); fireEvent.change(ui.getByLabelText("Run at"), { target: { value: "2027-06-01T09:30" }, }); expect(onChange).toHaveBeenLastCalledWith( once("2027-06-01T01:30:00+00:00", "Asia/Shanghai"), ); }); test("switching schedule type preserves cron behavior and an unchanged once value", () => { const initial = once("2027-06-01T12:30:45Z"); const onChange = rs.fn(); const ui = render( , ); fireEvent.click(ui.getByRole("button", { name: "Recurring" })); expect(onChange).toHaveBeenLastCalledWith({ schedule_type: "cron", schedule_spec: { cron: "0 9 * * *" }, timezone: "America/New_York", }); fireEvent.click(ui.getByRole("button", { name: "One-time" })); expect(onChange).toHaveBeenLastCalledWith(initial); }); test("a keyed task switch captures the next task's instant", () => { const onChange = rs.fn(); const ui = render( , ); const next = once("2027-06-01T12:30:45Z"); ui.rerender( , ); expect(onChange).toHaveBeenLastCalledWith(next); }); test("empty timezone uses a non-UTC browser zone without changing the instant", () => { const timezone = "Asia/Shanghai"; const browserOptions = Intl.DateTimeFormat().resolvedOptions(); const detectedZone = rs .spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions") .mockReturnValue({ ...browserOptions, timeZone: timezone }); try { const initial = once("2026-11-01T06:30:00Z", ""); const onChange = rs.fn(); const ui = render( , ); expect(detectedZone).toHaveBeenCalled(); expect(onChange).toHaveBeenLastCalledWith({ ...initial, timezone }); expect((ui.getByLabelText("Run at") as HTMLInputElement).value).toBe( "2026-11-01T14:30", ); } finally { detectedZone.mockRestore(); } });