import { mount, shallowMount } from "@vue/test-utils"; import flushPromises from "flush-promises"; import { describe, expect, it } from "vitest"; import type { JobBaseModel } from "@/api/jobs"; import { statePlaceholders } from "@/composables/useInvocationGraph"; import { TEST_JOBS_BY_STATES } from "./test/jobStepUtils"; import TEST_JOBS_JSON from "./test/json/jobs.json"; import JobStep from "./JobStep.vue"; const TEST_INVOCATION_ID = "test-invocation-id"; const SELECTORS = { JOB_STATE_BUTTON_NAV: "nav", JOB_STATE_BUTTON: ".g-button", JOBS_TABLE: ".job-step-jobs", JOB_ROW: ".job-step-jobs > tbody > tr", STUBBED_JOB_DETAILS: "anonymous-stub", }; describe("Job Step", () => { it("shows jobs grouped by state in tables when multiple jobs", async () => { const wrapper = mount(JobStep as object, { propsData: { jobs: TEST_JOBS_JSON, invocationId: TEST_INVOCATION_ID, }, }); await flushPromises(); // verify that the job state buttons are rendered const nav = wrapper.find(SELECTORS.JOB_STATE_BUTTON_NAV); expect(nav.exists()).toBe(true); // there are as many buttons as there are unique job states in `jobs` const buttons = nav.findAll(SELECTORS.JOB_STATE_BUTTON); expect(buttons.length).toBe(Object.keys(TEST_JOBS_BY_STATES).length); const jobStates = Object.keys(TEST_JOBS_BY_STATES); // test each job state for (let i = 0; i < jobStates.length; i++) { const jobState = jobStates[i] as string; const expectedCount = TEST_JOBS_BY_STATES[jobState]?.length as number; // check that the button has the expected text expect(buttons.at(i).text()).toBe( `${expectedCount} job${expectedCount === 1 ? "" : "s"} ${statePlaceholders[jobState] || jobState}`, ); // click the button to switch to this state await buttons.at(i).trigger("click"); await flushPromises(); // the clicked button should be pressed expect(buttons.at(i).classes()).toContain("g-pressed"); // renders a table with jobs for the current state const tableRows = wrapper.find(SELECTORS.JOBS_TABLE).findAll(SELECTORS.JOB_ROW); expect(tableRows.length).toBe(expectedCount); // each row has the expected state (as the last cell in the row) tableRows.wrappers.forEach((tr) => { const cells = tr.findAll("td"); expect(cells.at(cells.length - 1).text()).toBe(jobState); }); } }); it("reacts to job states changing when multiple jobs", async () => { const wrapper = mount(JobStep as object, { propsData: { jobs: TEST_JOBS_JSON, invocationId: TEST_INVOCATION_ID, }, }); await flushPromises(); // NOTE: This test assumes that the first job in TEST_JOBS_JSON has state 'new', and there is only one job with that state. // That is because we will update that job to 'running' and expect the active/pressed button to change to 'running', // as there are no more jobs in 'new' state. let buttons = wrapper.find(SELECTORS.JOB_STATE_BUTTON_NAV).findAll(SELECTORS.JOB_STATE_BUTTON); expect(buttons.length).toBe(Object.keys(TEST_JOBS_BY_STATES).length); let firstButton = buttons.at(0); // verify initial data is displayed for 'new' state' expect(firstButton.classes()).toContain("g-pressed"); expect(firstButton.text()).toBe("1 job new"); // the table should show one job in 'new' state expect(wrapper.findAll(SELECTORS.JOB_ROW).length).toBe(1); expect(wrapper.find(SELECTORS.JOB_ROW).find("td:last-child").text()).toBe("new"); // we have one running job already expect(buttons.at(1).text()).toBe("1 job running"); // we trigger the state change by updating the first job's state from 'new' to 'running' const updatedJob = { ...TEST_JOBS_JSON[0], state: "running" }; await wrapper.setProps({ jobs: [updatedJob, ...TEST_JOBS_JSON.slice(1)] }); await flushPromises(); // verify buttons have been updated buttons = wrapper.find(SELECTORS.JOB_STATE_BUTTON_NAV).findAll(SELECTORS.JOB_STATE_BUTTON); expect(buttons.length).toBe(Object.keys(TEST_JOBS_BY_STATES).length - 1); firstButton = buttons.at(0); // first button is now 'running' with 2 jobs expect(firstButton.classes()).toContain("g-pressed"); expect(firstButton.text()).toBe("2 jobs running"); // the table should now show 2 jobs in 'running' state expect(wrapper.findAll(SELECTORS.JOB_ROW).length).toBe(2); expect(wrapper.find(SELECTORS.JOB_ROW).find("td:last-child").text()).toBe("running"); }); it("just renders the job when only one job", async () => { const singleJob = TEST_JOBS_JSON.slice(0, 1)[0] as JobBaseModel; const wrapper = shallowMount(JobStep as object, { propsData: { jobs: [singleJob], invocationId: TEST_INVOCATION_ID, }, }); await flushPromises(); expect(wrapper.find(SELECTORS.JOBS_TABLE).exists()).toBe(false); expect(wrapper.find(SELECTORS.JOB_STATE_BUTTON_NAV).exists()).toBe(false); const jobDetails = wrapper.find(SELECTORS.STUBBED_JOB_DETAILS); expect(jobDetails.exists()).toBe(true); expect(jobDetails.attributes()["jobid"]).toBe(singleJob.id); expect(jobDetails.attributes()["invocationid"]).toBe(TEST_INVOCATION_ID); }); });