diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.test.ts b/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.test.ts new file mode 100644 index 000000000000..f6308952b587 --- /dev/null +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.test.ts @@ -0,0 +1,160 @@ +import { createTestingPinia } from "@pinia/testing"; +import { shallowMount } from "@vue/test-utils"; +import flushPromises from "flush-promises"; +import { getLocalVue } from "tests/jest/helpers"; + +import sampleInvocation from "@/components/Workflow/test/json/invocation.json"; +import { useUserStore } from "@/stores/userStore"; + +import WorkflowInvocationHeader from "./WorkflowInvocationHeader.vue"; + +// Constants +const WORKFLOW_OWNER = "test-user"; +const OTHER_USER = "other-user"; +const UNIMPORTABLE_WORKFLOW_ID = "invalid-workflow-id"; +const UNIMPORTABLE_WORKFLOW_INSTANCE_ID = "invalid-instance-id"; +const SAMPLE_WORKFLOW = { + id: "workflow-id", + name: "workflow-name", + owner: WORKFLOW_OWNER, + version: 1, +}; +const IMPORT_ERROR_MESSAGE = "Failed to import workflow"; + +const SELECTORS = { + INVOKED_WORKFLOW_HEADING: "anonymous-stub[h1='true']", + RETURN_TO_INVOCATIONS_LIST_BUTTON: "bbutton-stub[title='Return to Invocations List']", + ACTIONS_BUTTON_GROUP: "bbuttongroup-stub", + EDIT_WORKFLOW_BUTTON: `bbutton-stub[title='Edit
${SAMPLE_WORKFLOW.name}']`, + IMPORT_WORKFLOW_BUTTON: "anonymous-stub[title='Import this workflow']", + RUN_WORKFLOW_BUTTON: `anonymous-stub[id='${SAMPLE_WORKFLOW.id}']`, + ALERT_MESSAGE: "balert-stub", +}; + +// Mock the copyWorkflow function for importing a workflow +jest.mock("components/Workflow/workflows.services", () => ({ + copyWorkflow: jest.fn().mockImplementation((workflowId: string) => { + if (workflowId === UNIMPORTABLE_WORKFLOW_ID) { + throw new Error(IMPORT_ERROR_MESSAGE); + } + return SAMPLE_WORKFLOW; + }), +})); + +// Mock the workflow store to return the sample workflow +jest.mock("@/stores/workflowStore", () => { + const originalModule = jest.requireActual("@/stores/workflowStore"); + return { + ...originalModule, + useWorkflowStore: () => ({ + ...originalModule.useWorkflowStore(), + getStoredWorkflowByInstanceId: jest.fn().mockImplementation((instanceId: string) => { + if (instanceId === UNIMPORTABLE_WORKFLOW_INSTANCE_ID) { + return { ...SAMPLE_WORKFLOW, id: UNIMPORTABLE_WORKFLOW_ID }; + } + return SAMPLE_WORKFLOW; + }), + }), + }; +}); + +const localVue = getLocalVue(); + +/** + * Mounts the WorkflowInvocationHeader component with props/stores adjusted given the parameters + * @param ownsWorkflow Whether the user owns the workflow associated with the invocation + * @param hasReturnBtn Whether the component should have a return to invocations list button + * @param unimportableWorkflow Whether the workflow import should fail + * @returns The wrapper object + */ +async function mountWorkflowInvocationHeader(ownsWorkflow = true, hasReturnBtn = false, unimportableWorkflow = false) { + const wrapper = shallowMount(WorkflowInvocationHeader as object, { + propsData: { + invocation: { + ...sampleInvocation, + workflow_id: !unimportableWorkflow ? sampleInvocation.workflow_id : UNIMPORTABLE_WORKFLOW_INSTANCE_ID, + }, + fromPanel: !hasReturnBtn, + }, + localVue, + pinia: createTestingPinia(), + }); + + const userStore = useUserStore(); + userStore.currentUser = { + id: "1", + email: "test@mail.test", + tags_used: [], + isAnonymous: false, + total_disk_usage: 0, + username: ownsWorkflow ? WORKFLOW_OWNER : OTHER_USER, + }; + + return { wrapper }; +} + +describe("WorkflowInvocationHeader renders", () => { + // Included both cases in one test because these are always constant + it("(always) the workflow name in header and run button in actions", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(); + + const heading = wrapper.find(SELECTORS.INVOKED_WORKFLOW_HEADING); + expect(heading.text()).toBe(`Invoked Workflow: "${SAMPLE_WORKFLOW.name}"`); + + const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP); + const runButton = actionsGroup.find(SELECTORS.RUN_WORKFLOW_BUTTON); + expect(runButton.attributes("title")).toContain(SAMPLE_WORKFLOW.name); + }); + + it("return to invocations list button if not from panel", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(false, true); + const returnButton = wrapper.find(SELECTORS.RETURN_TO_INVOCATIONS_LIST_BUTTON); + expect(returnButton.text()).toBe("Invocations List"); + }); + + it("edit button if user owns the workflow", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(); + const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP); + const editButton = actionsGroup.find(SELECTORS.EDIT_WORKFLOW_BUTTON); + expect(editButton.attributes("to")).toBe( + `/workflows/edit?id=${SAMPLE_WORKFLOW.id}&version=${SAMPLE_WORKFLOW.version}` + ); + }); + + it("import button instead if user does not own the workflow", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(false); + const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP); + const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON); + expect(importButton.exists()).toBe(true); + }); +}); + +describe("Importing a workflow in WorkflowInvocationHeader", () => { + it("should show a confirmation dialog when the import is successful", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(false); + const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP); + const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON); + + // Cannot `.trigger("click")` on `AsyncButton` because it is a stubbed custom component + await importButton.props().action(); + await flushPromises(); + + const alert = wrapper.find(SELECTORS.ALERT_MESSAGE); + expect(alert.attributes("variant")).toBe("info"); + expect(alert.text()).toContain(`Workflow ${SAMPLE_WORKFLOW.name} imported successfully`); + }); + + it("should show an error dialog when the import fails", async () => { + const { wrapper } = await mountWorkflowInvocationHeader(false, false, true); + const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP); + const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON); + + // Cannot `.trigger("click")` on `AsyncButton` because it is a stubbed custom component + await importButton.props().action(); + await flushPromises(); + + const alert = wrapper.find(SELECTORS.ALERT_MESSAGE); + expect(alert.attributes("variant")).toBe("danger"); + expect(alert.text()).toContain(IMPORT_ERROR_MESSAGE); + }); +}); diff --git a/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.vue b/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.vue index f01618d86582..d6f0142d5040 100644 --- a/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.vue +++ b/client/src/components/WorkflowInvocationState/WorkflowInvocationHeader.vue @@ -1,13 +1,21 @@