deer-flow/frontend/tests/unit/components/workspace/scheduled-task-schedule-input.dom.test.tsx
wutongyuonce 48a8978b7b
feat(scheduler): add interval schedule type (#5291)
* feat(scheduler): add interval schedule type

Allow scheduled tasks to fire every N seconds from last dispatch, not
only wall-clock cron or a single run_at. Cadence is UTC now+N with no
missed-beat catch-up, bounded by min_once_delay_seconds and 30 days.

* fix(scheduler): let interval tasks create, edit, and keep next run

Create/edit now keep every_seconds. Unchanged interval spec no longer
resets next_run_at, including timezone-only PATCH.

* fix(scheduler): keep non-minute intervals on edit

Stop rounding every_seconds to whole minutes in the form. Values that
are not whole minutes or hours now use a seconds unit so edit/duplicate
round-trips the stored cadence instead of rewriting it and resetting
next_run_at. Document that min_once_delay_seconds is also the interval
floor.

* fix(scheduler): clamp interval seconds to the default 60s floor

The new seconds unit allowed 1–59, which the API rejects under the
default min_once_delay_seconds. Clamp the form to >= 60 and show the
floor next to the preview. Also mention interval in the scheduler
field_doc, matching config.example.yaml.

* fix(scheduler): do not clamp interval amount while typing

Keystroke clamp made 90 become 9 -> 60, then 600, and backspace could
not leave 60. Keep the raw field text and apply the 60s floor on blur
and emit only.

* test(scheduler): cover interval input editing

* fix(frontend): preserve saved interval cadence until edited

* style(tests): format scheduled task router tests

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-10 16:46:01 +08:00

80 lines
2.8 KiB
TypeScript

import { afterEach, describe, expect, test } from "@rstest/core";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import {
ScheduledTaskScheduleInput,
type ScheduleValue,
} from "@/components/workspace/scheduled-task-schedule-input";
import { I18nProvider } from "@/core/i18n/context";
afterEach(cleanup);
describe("ScheduledTaskScheduleInput", () => {
test.each([1, 30, 59])(
"preserves an existing %s-second interval until explicitly edited",
(everySeconds) => {
const emitted: ScheduleValue[] = [];
render(
<I18nProvider initialLocale="en-US">
<ScheduledTaskScheduleInput
initial={{
schedule_type: "interval",
schedule_spec: { every_seconds: everySeconds },
timezone: "UTC",
}}
onChange={(value) => emitted.push(value)}
/>
</I18nProvider>,
);
const amountInput = screen.getByRole<HTMLInputElement>("spinbutton");
expect(amountInput.value).toBe(String(everySeconds));
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(everySeconds);
fireEvent.focus(amountInput);
fireEvent.blur(amountInput);
expect(amountInput.value).toBe(String(everySeconds));
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(everySeconds);
fireEvent.change(amountInput, { target: { value: "9" } });
expect(amountInput.value).toBe("9");
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(60);
fireEvent.blur(amountInput);
expect(amountInput.value).toBe("60");
},
);
test("keeps interval text editable until blur applies the floor", () => {
const emitted: ScheduleValue[] = [];
render(
<I18nProvider initialLocale="en-US">
<ScheduledTaskScheduleInput
initial={{
schedule_type: "interval",
schedule_spec: { every_seconds: 90 },
timezone: "UTC",
}}
onChange={(value) => emitted.push(value)}
/>
</I18nProvider>,
);
const amountInput = screen.getByRole("spinbutton");
expect((amountInput as HTMLInputElement).value).toBe("90");
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(90);
fireEvent.change(amountInput, { target: { value: "9" } });
expect((amountInput as HTMLInputElement).value).toBe("9");
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(60);
fireEvent.change(amountInput, { target: { value: "" } });
expect((amountInput as HTMLInputElement).value).toBe("");
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(60);
fireEvent.blur(amountInput);
expect((amountInput as HTMLInputElement).value).toBe("60");
expect(emitted.at(-1)?.schedule_spec.every_seconds).toBe(60);
});
});