From 81d6cf9c82bd6b0161306d02b2ac8aa47d3302b8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 18:25:16 +0100 Subject: [PATCH] test: merge cron view UI cases --- ui/src/ui/views/cron.test.ts | 207 +++++++++++------------------------ 1 file changed, 66 insertions(+), 141 deletions(-) diff --git a/ui/src/ui/views/cron.test.ts b/ui/src/ui/views/cron.test.ts index 1fdfd836488..46caf2d6496 100644 --- a/ui/src/ui/views/cron.test.ts +++ b/ui/src/ui/views/cron.test.ts @@ -77,6 +77,12 @@ function createProps(overrides: Partial = {}): 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", () => {