test: merge cron view UI cases

This commit is contained in:
Peter Steinberger
2026-04-17 18:25:16 +01:00
parent f513bae67e
commit 81d6cf9c82

View File

@@ -77,6 +77,12 @@ function createProps(overrides: Partial<CronProps> = {}): CronProps {
};
}
function getButtonByText(container: Element, text: string) {
return Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === text,
);
}
describe("cron view", () => {
it("shows all-job history mode by default", () => {
const container = document.createElement("div");
@@ -250,41 +256,36 @@ describe("cron view", () => {
expect(container.textContent).not.toContain("Next 13");
});
it("calls onJobsFiltersChange when schedule filter changes", () => {
it("wires jobs filter changes and reset", () => {
const container = document.createElement("div");
const onJobsFiltersChange = vi.fn();
const onJobsFiltersReset = vi.fn();
render(renderCron(createProps({ onJobsFiltersChange })), container);
const select = container.querySelector('select[data-test-id="cron-jobs-schedule-filter"]');
expect(select).not.toBeNull();
if (!(select instanceof HTMLSelectElement)) {
const scheduleSelect = container.querySelector(
'select[data-test-id="cron-jobs-schedule-filter"]',
);
expect(scheduleSelect).not.toBeNull();
if (!(scheduleSelect instanceof HTMLSelectElement)) {
return;
}
select.value = "cron";
select.dispatchEvent(new Event("change", { bubbles: true }));
scheduleSelect.value = "cron";
scheduleSelect.dispatchEvent(new Event("change", { bubbles: true }));
expect(onJobsFiltersChange).toHaveBeenCalledWith({ cronJobsScheduleKindFilter: "cron" });
});
it("calls onJobsFiltersChange when last-run filter changes", () => {
const container = document.createElement("div");
const onJobsFiltersChange = vi.fn();
render(renderCron(createProps({ onJobsFiltersChange })), container);
const select = container.querySelector('select[data-test-id="cron-jobs-last-status-filter"]');
expect(select).not.toBeNull();
if (!(select instanceof HTMLSelectElement)) {
const lastRunSelect = container.querySelector(
'select[data-test-id="cron-jobs-last-status-filter"]',
);
expect(lastRunSelect).not.toBeNull();
if (!(lastRunSelect instanceof HTMLSelectElement)) {
return;
}
select.value = "error";
select.dispatchEvent(new Event("change", { bubbles: true }));
lastRunSelect.value = "error";
lastRunSelect.dispatchEvent(new Event("change", { bubbles: true }));
expect(onJobsFiltersChange).toHaveBeenCalledWith({ cronJobsLastStatusFilter: "error" });
});
it("calls onJobsFiltersReset when reset button is clicked", () => {
const container = document.createElement("div");
const onJobsFiltersReset = vi.fn();
render(
renderCron(
createProps({
@@ -302,7 +303,7 @@ describe("cron view", () => {
expect(onJobsFiltersReset).toHaveBeenCalledTimes(1);
});
it("shows webhook delivery option in the form", () => {
it("renders supported delivery options and normalizes stale announce selection", () => {
const container = document.createElement("div");
render(
renderCron(
@@ -317,10 +318,7 @@ describe("cron view", () => {
(opt.textContent ?? "").trim(),
);
expect(options).toContain("Webhook POST");
});
it("normalizes stale announce selection in the form when unsupported", () => {
const container = document.createElement("div");
render(
renderCron(
createProps({
@@ -335,12 +333,12 @@ describe("cron view", () => {
container,
);
const options = Array.from(container.querySelectorAll("option")).map((opt) =>
const normalizedOptions = Array.from(container.querySelectorAll("option")).map((opt) =>
(opt.textContent ?? "").trim(),
);
expect(options).not.toContain("Announce summary (default)");
expect(options).toContain("Webhook POST");
expect(options).toContain("None (internal)");
expect(normalizedOptions).not.toContain("Announce summary (default)");
expect(normalizedOptions).toContain("Webhook POST");
expect(normalizedOptions).toContain("None (internal)");
expect(container.querySelector('input[placeholder="https://example.com/cron"]')).toBeNull();
});
@@ -386,9 +384,7 @@ describe("cron view", () => {
container,
);
const editButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Edit",
);
const editButton = getButtonByText(container, "Edit");
expect(editButton).not.toBeUndefined();
editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onEdit).toHaveBeenCalledWith(job);
@@ -397,15 +393,13 @@ describe("cron view", () => {
expect(container.textContent).toContain("Edit Job");
expect(container.textContent).toContain("Save changes");
const cancelButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Cancel",
);
const cancelButton = getButtonByText(container, "Cancel");
expect(cancelButton).not.toBeUndefined();
cancelButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onCancelEdit).toHaveBeenCalledTimes(1);
});
it("renders advanced controls for cron + agent payload + delivery", () => {
it("renders cron form sections and toggles advanced controls by schedule", () => {
const container = document.createElement("div");
render(
renderCron(
@@ -428,52 +422,29 @@ describe("cron view", () => {
expect(container.textContent).toContain("Model");
expect(container.textContent).toContain("Thinking");
expect(container.textContent).toContain("Best effort delivery");
});
it("groups stagger window and unit inside the same stagger row", () => {
const container = document.createElement("div");
render(
renderCron(
createProps({
form: {
...DEFAULT_CRON_FORM,
scheduleKind: "cron",
payloadKind: "agentTurn",
},
}),
),
container,
);
const staggerGroup = container.querySelector(".cron-stagger-group");
expect(staggerGroup).not.toBeNull();
expect(staggerGroup?.textContent).toContain("Stagger window");
expect(staggerGroup?.textContent).toContain("Stagger unit");
});
it("explains timeout blank behavior and shows cron jitter hint", () => {
const container = document.createElement("div");
render(
renderCron(
createProps({
form: {
...DEFAULT_CRON_FORM,
scheduleKind: "cron",
payloadKind: "agentTurn",
},
}),
),
container,
);
expect(container.textContent).toContain(
"Optional. Leave blank to use the gateway default timeout behavior for this run.",
);
expect(container.textContent).toContain("Need jitter? Use Advanced");
});
it("disables Agent ID when clear-agent is enabled", () => {
const container = document.createElement("div");
expect(container.textContent).toContain("Enabled");
expect(container.textContent).toContain("Jobs");
expect(container.textContent).toContain("Next wake");
expect(container.textContent).toContain("Basics");
expect(container.textContent).toContain("Schedule");
expect(container.textContent).toContain("Execution");
expect(container.textContent).toContain("Delivery");
const checkboxLabel = container.querySelector(".cron-checkbox");
expect(checkboxLabel).not.toBeNull();
const firstElement = checkboxLabel?.firstElementChild;
expect(firstElement?.tagName.toLowerCase()).toBe("input");
render(
renderCron(
createProps({
@@ -490,32 +461,7 @@ describe("cron view", () => {
expect(agentInput).not.toBeNull();
expect(agentInput instanceof HTMLInputElement).toBe(true);
expect(agentInput instanceof HTMLInputElement ? agentInput.disabled : false).toBe(true);
});
it("renders sectioned cron form layout", () => {
const container = document.createElement("div");
render(renderCron(createProps()), container);
expect(container.textContent).toContain("Enabled");
expect(container.textContent).toContain("Jobs");
expect(container.textContent).toContain("Next wake");
expect(container.textContent).toContain("Basics");
expect(container.textContent).toContain("Schedule");
expect(container.textContent).toContain("Execution");
expect(container.textContent).toContain("Delivery");
expect(container.textContent).toContain("Advanced");
});
it("renders checkbox fields with input first for alignment", () => {
const container = document.createElement("div");
render(renderCron(createProps()), container);
const checkboxLabel = container.querySelector(".cron-checkbox");
expect(checkboxLabel).not.toBeNull();
const firstElement = checkboxLabel?.firstElementChild;
expect(firstElement?.tagName.toLowerCase()).toBe("input");
});
it("hides cron-only advanced controls for non-cron schedules", () => {
const container = document.createElement("div");
render(
renderCron(
createProps({
@@ -535,7 +481,7 @@ describe("cron view", () => {
expect(container.textContent).not.toContain("Best effort delivery");
});
it("renders inline validation errors and disables submit when invalid", () => {
it("renders inline validation errors, disabled submit, and required aria bindings", () => {
const container = document.createElement("div");
render(
renderCron(
@@ -569,10 +515,7 @@ describe("cron view", () => {
);
expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(true);
});
it("shows required legend and aria bindings for invalid required fields", () => {
const container = document.createElement("div");
render(
renderCron(
createProps({
@@ -609,7 +552,7 @@ describe("cron view", () => {
);
});
it("wires the Clone action from job rows", () => {
it("wires job row actions and selects the row before acting", () => {
const container = document.createElement("div");
const onClone = vi.fn();
const onLoadRuns = vi.fn();
@@ -625,85 +568,67 @@ describe("cron view", () => {
container,
);
const cloneButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Clone",
);
const cloneButton = getButtonByText(container, "Clone");
expect(cloneButton).not.toBeUndefined();
cloneButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onClone).toHaveBeenCalledWith(job);
expect(onLoadRuns).toHaveBeenCalledWith("job-clone");
});
it("selects row when clicking Enable/Disable, Run, and Remove actions", () => {
const container = document.createElement("div");
const onToggle = vi.fn();
const onRun = vi.fn();
const onRemove = vi.fn();
const onLoadRuns = vi.fn();
const job = createJob("job-actions");
const actionLoadRuns = vi.fn();
const actionJob = createJob("job-actions");
render(
renderCron(
createProps({
jobs: [job],
jobs: [actionJob],
onToggle,
onRun,
onRemove,
onLoadRuns,
onLoadRuns: actionLoadRuns,
}),
),
container,
);
const enableButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Disable",
);
const enableButton = getButtonByText(container, "Disable");
expect(enableButton).not.toBeUndefined();
enableButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
const runButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Run",
);
const runButton = getButtonByText(container, "Run");
expect(runButton).not.toBeUndefined();
runButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
const removeButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Remove",
);
const removeButton = getButtonByText(container, "Remove");
expect(removeButton).not.toBeUndefined();
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onToggle).toHaveBeenCalledWith(job, false);
expect(onRun).toHaveBeenCalledWith(job, "force");
expect(onRemove).toHaveBeenCalledWith(job);
expect(onLoadRuns).toHaveBeenCalledTimes(3);
expect(onLoadRuns).toHaveBeenNthCalledWith(1, "job-actions");
expect(onLoadRuns).toHaveBeenNthCalledWith(2, "job-actions");
expect(onLoadRuns).toHaveBeenNthCalledWith(3, "job-actions");
});
expect(onToggle).toHaveBeenCalledWith(actionJob, false);
expect(onRun).toHaveBeenCalledWith(actionJob, "force");
expect(onRemove).toHaveBeenCalledWith(actionJob);
expect(actionLoadRuns).toHaveBeenCalledTimes(3);
expect(actionLoadRuns).toHaveBeenNthCalledWith(1, "job-actions");
expect(actionLoadRuns).toHaveBeenNthCalledWith(2, "job-actions");
expect(actionLoadRuns).toHaveBeenNthCalledWith(3, "job-actions");
it("wires Run if due action with due mode", () => {
const container = document.createElement("div");
const onRun = vi.fn();
const onLoadRuns = vi.fn();
const job = createJob("job-due");
const onRunDue = vi.fn();
const dueJob = createJob("job-due");
render(
renderCron(
createProps({
jobs: [job],
onRun,
onLoadRuns,
jobs: [dueJob],
onRun: onRunDue,
}),
),
container,
);
const runDueButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Run if due",
);
const runDueButton = getButtonByText(container, "Run if due");
expect(runDueButton).not.toBeUndefined();
runDueButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onRun).toHaveBeenCalledWith(job, "due");
expect(onRunDue).toHaveBeenCalledWith(dueJob, "due");
});
it("renders suggestion datalists for agent/model/thinking/timezone", () => {