import React from "react"; import { screen, waitFor } from "@testing-library/react"; import { UserEvent } from "@testing-library/user-event"; import { IScript } from "interfaces/script"; import { createCustomRenderer } from "test/test-utils"; import { http, HttpResponse } from "msw"; import mockServer from "test/mock-server"; import { format } from "date-fns"; import RunScriptBatchModal from "./RunScriptBatchModal"; const baseUrl = (path: string) => { return `/api/latest/fleet${path}`; }; const windowsScript: IScript = { id: 123, team_id: 1, name: "winscript.ps1", created_at: "2023-01-01T00:00:00Z", updated_at: "2023-01-01T00:00:00Z", }; const linuxScript: IScript = { id: 456, team_id: 1, name: "linuxscript.sh", created_at: "2023-01-01T00:00:00Z", updated_at: "2023-01-01T00:00:00Z", }; jest.mock("../RunScriptBatchPaginatedList", () => { return { __esModule: true, default: ({ onRunScript }: { onRunScript: (script: IScript) => void }) => { return (
onRunScript(windowsScript)}>windows script
onRunScript(linuxScript)}>linux script
); }, }; }); // Utility to validate that the script list is rendered, select a script, // and return the run and cancel buttons. const selectScript = async (user: UserEvent, platform: string) => { let el; await waitFor(async () => { el = screen.getByText(`${platform} script`); expect(el).toBeInTheDocument(); }); if (!el) { throw new Error("Script element not found"); } await user.click(el); let runButton; let cancelButton; await waitFor(() => { runButton = screen.getByRole("button", { name: "Run" }); expect(runButton).toBeInTheDocument(); cancelButton = screen.getByRole("button", { name: "Cancel" }); expect(cancelButton).toBeInTheDocument(); }); if (!runButton || !cancelButton) { throw new Error("Run or Cancel button not found"); } return { runButton, cancelButton }; }; // Utility to validate that the "schedule for later" and "run now" buttons are present // and return them. const getScheduleSelector = async () => { let scheduleButton; let runNowButton; await waitFor(() => { scheduleButton = screen.getByLabelText("Schedule for later"); expect(scheduleButton).toBeInTheDocument(); runNowButton = screen.getByLabelText("Run now"); expect(runNowButton).toBeInTheDocument(); }); if (!scheduleButton || !runNowButton) { throw new Error("Schedule or Run Now button not found"); } return { scheduleButton, runNowButton }; }; // Utility to validate that the scheduling UI is present and return the date and time inputs. const getScheduleUI = async () => { let dateInput; let timeInput; await waitFor(() => { dateInput = screen.getByLabelText("Date (UTC)"); expect(dateInput).toBeInTheDocument(); timeInput = screen.getByLabelText("Time (UTC)"); expect(timeInput).toBeInTheDocument(); }); if (!dateInput || !timeInput) { throw new Error("Date or Time input not found"); } return { dateInput, timeInput }; }; describe("RunScriptBatchModal", () => { // Mock the scripts endpoint to return our two test scripts. const scriptsHandler = http.get(baseUrl("/scripts"), () => { return HttpResponse.json({ scripts: [windowsScript, linuxScript], }); }); // Mock the run batch endpoint to simulate running a batch script, // and provide a mock function we can use to validate the API call. const runBatchFn = jest.fn(async () => { return HttpResponse.json({}); }); const runBatchHandler = http.post(baseUrl("/scripts/run/batch"), runBatchFn); beforeEach(() => { mockServer.use(scriptsHandler); runBatchFn.mockReset(); mockServer.use(runBatchHandler); }); const render = createCustomRenderer({ withBackendMock: true, }); // Standard props to use for most tests. const defaultProps = { runByFilters: false, filters: { team_id: 1, status: "" }, teamId: 1, isFreeTier: false, totalFilteredHostsCount: 2, selectedHostIds: [1, 2], onCancel: () => null, }; it("lists the scripts available for batch running", () => { render(); waitFor(() => { const windowsScriptElement = screen.getByText("windows script"); const linuxScriptElement = screen.getByText("linux script"); expect(windowsScriptElement).toBeInTheDocument(); expect(linuxScriptElement).toBeInTheDocument(); }); }); describe("after clicking run script", () => { it("shows the correct heading for linux/macos scripts", async () => { const { user } = render(); await selectScript(user, "linux"); await waitFor(() => { expect(screen.getByText("linuxscript.sh")).toBeInTheDocument(); expect(screen.getByText(/macOS and Linux/)).toBeInTheDocument(); }); }); it("shows the correct heading for windows", async () => { const { user } = render(); await selectScript(user, "windows"); await waitFor(() => { expect(screen.getByText("winscript.ps1")).toBeInTheDocument(); expect(screen.getByText(/Windows/)).toBeInTheDocument(); }); }); it("does not show the scheduling UI if 'run now' is selected", async () => { const { user } = render(); await selectScript(user, "windows"); const { runNowButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); expect(screen.queryByLabelText(/Date/)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/Time/)).not.toBeInTheDocument(); }); it("shows the scheduling UI if 'schedule for later' is selected", async () => { const { user } = render(); await selectScript(user, "windows"); const { runNowButton, scheduleButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); await user.click(scheduleButton); await getScheduleUI(); }); describe("run now", () => { it("should call the API with no not_before param", async () => { const { user } = render(); const { runButton } = await selectScript(user, "windows"); await user.click(runButton); expect(runBatchFn.mock.calls.length).toBe(1); // @ts-ignore const body = await runBatchFn.mock.calls[0][0].request.json(); expect(body).toEqual({ script_id: windowsScript.id, host_ids: defaultProps.selectedHostIds, }); }); it("should call the API with filters if supplied", async () => { const props = { ...defaultProps, runByFilters: true, filters: { query: "hi", label_id: 16, status: "" }, }; props.selectedHostIds = []; const { user } = render(); const { runButton } = await selectScript(user, "windows"); await user.click(runButton); expect(runBatchFn.mock.calls.length).toBe(1); // @ts-ignore const body = await runBatchFn.mock.calls[0][0].request.json(); expect(body).toEqual({ script_id: windowsScript.id, filters: { query: "hi", label_id: 16, fleet_id: 1, status: "", }, }); }); }); describe("schedule for later", () => { it("requires a valid date", async () => { const { user } = render(); await selectScript(user, "windows"); const { runNowButton, scheduleButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); await user.click(scheduleButton); const { dateInput } = await getScheduleUI(); // Add a wildly invalid date await user.type(dateInput, "u up?"); expect(dateInput).toHaveValue("u up?"); expect( screen.getByText("Date (UTC) must have valid format") ).toBeInTheDocument(); // Add a less wild, but still invalid date await user.clear(dateInput); await user.type(dateInput, "2023-99-99"); expect(dateInput).toHaveValue("2023-99-99"); expect( screen.getByText("Date (UTC) must have valid format") ).toBeInTheDocument(); // Add a valid date, but in the past await user.clear(dateInput); await user.type(dateInput, "2023-01-01"); expect(dateInput).toHaveValue("2023-01-01"); expect( screen.getByText("Date (UTC) cannot be in the past") ).toBeInTheDocument(); // Add a valid date in the future await user.clear(dateInput); await user.type(dateInput, "2099-12-31"); expect(dateInput).toHaveValue("2099-12-31"); expect( screen.queryByText("Date (UTC) must have valid format") ).not.toBeInTheDocument(); expect( screen.queryByText("Date (UTC) cannot be in the past") ).not.toBeInTheDocument(); }); it("requires a valid time", async () => { const { user } = render(); await selectScript(user, "windows"); const { runNowButton, scheduleButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); await user.click(scheduleButton); const { dateInput, timeInput } = await getScheduleUI(); // Add a wildly invalid time await user.type(timeInput, "professor churro"); expect(timeInput).toHaveValue("professor churro"); expect( screen.getByText("Time (UTC) must have valid format") ).toBeInTheDocument(); // Add a less wild, but still invalid time await user.clear(timeInput); await user.type(timeInput, "99:99"); expect(timeInput).toHaveValue("99:99"); expect( screen.getByText("Time (UTC) must have valid format") ).toBeInTheDocument(); // Add a valid time in the past (no date selected) await user.clear(timeInput); await user.type(timeInput, "00:00"); expect(timeInput).toHaveValue("00:00"); expect( screen.queryByText("Date (UTC) must have valid format") ).not.toBeInTheDocument(); expect( screen.queryByText("Date cannot be in the past") ).not.toBeInTheDocument(); // Add a valid time in the past (future date selected) await user.clear(timeInput); await user.type(timeInput, "00:00"); await user.clear(dateInput); await user.type(dateInput, "2099-12-31"); expect(timeInput).toHaveValue("00:00"); expect( screen.queryByText("Date (UTC) must have valid format") ).not.toBeInTheDocument(); expect( screen.queryByText("Date (UTC) cannot be in the past") ).not.toBeInTheDocument(); // Add a valid time in the past (today selected) await user.clear(dateInput); await user.type(dateInput, format(new Date(), "yyyy-MM-dd")); await user.clear(timeInput); await user.type(timeInput, "00:00"); expect(timeInput).toHaveValue("00:00"); expect( screen.getByText("Time (UTC) cannot be in the past") ).toBeInTheDocument(); }); it("should call the API with a correct not_before param", async () => { const { user } = render(); const { runButton } = await selectScript(user, "windows"); const { runNowButton, scheduleButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); await user.click(scheduleButton); expect(scheduleButton).toBeChecked(); const { dateInput, timeInput } = await getScheduleUI(); await user.type(dateInput, "2099-12-31"); await user.type(timeInput, "23:59"); await user.click(runButton); expect(runBatchFn.mock.calls.length).toBe(1); // @ts-ignore const body = await runBatchFn.mock.calls[0][0].request.json(); expect(body).toEqual({ script_id: windowsScript.id, host_ids: defaultProps.selectedHostIds, not_before: "2099-12-31T23:59:00.000Z", }); }); it("should call the API with a correct not_before param and filters if provided", async () => { const props = { ...defaultProps, runByFilters: true, filters: { query: "hi", label_id: 16, status: "" }, }; props.selectedHostIds = []; const { user } = render(); const { runButton } = await selectScript(user, "windows"); const { runNowButton, scheduleButton } = await getScheduleSelector(); expect(runNowButton).toBeChecked(); await user.click(scheduleButton); expect(scheduleButton).toBeChecked(); const { dateInput, timeInput } = await getScheduleUI(); await user.type(dateInput, "2099-12-31"); await user.type(timeInput, "23:59"); await user.click(runButton); expect(runBatchFn.mock.calls.length).toBe(1); // @ts-ignore const body = await runBatchFn.mock.calls[0][0].request.json(); expect(body).toEqual({ script_id: windowsScript.id, not_before: "2099-12-31T23:59:00.000Z", filters: { query: "hi", label_id: 16, status: "", fleet_id: 1 }, }); }); }); }); });