Skip to content

Commit

Permalink
add a jest each for WorkflowAnnotation and WorkflowNavigationTitle
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedhamidawan committed Nov 9, 2024
1 parent 8dceade commit 936d0b2
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 43 deletions.
2 changes: 1 addition & 1 deletion client/src/components/History/SwitchToHistoryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function viewHistoryInNewTab(history: HistorySummary) {
<template>
<div>
<LoadingSpan v-if="!history" />
<div v-else class="history-link">
<div v-else class="history-link" data-description="switch to history link">
<BLink
v-b-tooltip.hover.top.noninteractive.html
class="truncate"
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Workflow/List/WorkflowIndicators.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function onViewUserPublished() {
<BBadge
v-if="publishedView && workflow.published"
v-b-tooltip.noninteractive.hover
data-description="published owner badge"
class="outline-badge cursor-pointer mx-1"
:title="publishedTitle"
@click="onViewUserPublished">
Expand Down
168 changes: 168 additions & 0 deletions client/src/components/Workflow/WorkflowAnnotation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { createTestingPinia } from "@pinia/testing";
import { getFakeRegisteredUser } from "@tests/test-data";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "tests/jest/helpers";

import { useServerMock } from "@/api/client/__mocks__";
import { useHistoryStore } from "@/stores/historyStore";
import { useUserStore } from "@/stores/userStore";

import WorkflowAnnotation from "./WorkflowAnnotation.vue";

// Constants
const WORKFLOW_OWNER = "test-user";
const OTHER_USER = "other-user";
const WORKFLOW_UPDATE_TIME = "2023-01-01T00:00:00.000Z";
const INVOCATION_TIME = "2024-01-01T00:00:00.000Z";
const SAMPLE_WORKFLOW = {
id: "workflow-id",
name: "workflow-name",
owner: WORKFLOW_OWNER,
version: 1,
update_time: WORKFLOW_UPDATE_TIME,
};
const OTHER_USER_WORKFLOW_ID = "other-user-workflow-id";
const SAMPLE_RUN_COUNT = 100;
const TEST_HISTORY_ID = "test-history-id";
const TEST_HISTORY = {
id: TEST_HISTORY_ID,
name: "fake-history-name",
};

const SELECTORS = {
RUN_COUNT: ".workflow-invocations-count",
INDICATORS_LINK: '[data-description="published owner badge"]',
SWITCH_TO_HISTORY_LINK: "[data-description='switch to history link']",
TIME_INFO: '[data-description="workflow annotation time info"]',
DATE: '[data-description="workflow annotation date"]',
};

// 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((id: string) => {
if (id === OTHER_USER_WORKFLOW_ID) {
return { ...SAMPLE_WORKFLOW, id: OTHER_USER_WORKFLOW_ID, published: true };
}
return SAMPLE_WORKFLOW;
}),
}),
};
});

jest.mock("@/stores/historyStore"),
() => {
const originalModule = jest.requireActual("@/stores/historyStore");
return {
...originalModule,
useHistoryStore: () => ({
...originalModule.useHistoryStore(),
getHistoryById: jest.fn().mockImplementation(() => TEST_HISTORY),
}),
};
};

const localVue = getLocalVue();
const { server, http } = useServerMock();

/**
* Mounts the WorkflowAnnotation component with props/stores adjusted given the parameters
* @param version The version of the component to mount (`run_form` or `invocation` view)
* @param ownsWorkflow Whether the user owns the workflow
* @returns The wrapper object
*/
async function mountWorkflowAnnotation(version: "run_form" | "invocation", ownsWorkflow = true) {
server.use(
http.get("/api/histories/{history_id}", ({ response }) => {
return response(200).json(TEST_HISTORY);
})
);
server.use(
http.get("/api/workflows/{workflow_id}/counts", ({ response }) => {
return response(200).json({ scheduled: SAMPLE_RUN_COUNT });
})
);

const wrapper = mount(WorkflowAnnotation as object, {
propsData: {
workflowId: ownsWorkflow ? SAMPLE_WORKFLOW.id : OTHER_USER_WORKFLOW_ID,
historyId: TEST_HISTORY_ID,
invocationUpdateTime: version === "invocation" ? INVOCATION_TIME : undefined,
showDetails: version === "run_form",
},
localVue,
pinia: createTestingPinia(),
stubs: {
FontAwesomeIcon: true,
},
});

const historyStore = useHistoryStore();
historyStore.setCurrentHistoryId(TEST_HISTORY_ID);

const userStore = useUserStore();
userStore.currentUser = getFakeRegisteredUser({
username: ownsWorkflow ? WORKFLOW_OWNER : OTHER_USER,
});

await flushPromises();

return { wrapper };
}

describe("WorkflowAnnotation renders", () => {
it("(always) the run count and history, not indicators if owned not published", async () => {
async function checkHasRunCount(version: "run_form" | "invocation") {
const { wrapper } = await mountWorkflowAnnotation(version);

const runCount = wrapper.find(SELECTORS.RUN_COUNT);
expect(runCount.text()).toContain("workflow runs:");
expect(runCount.text()).toContain(SAMPLE_RUN_COUNT.toString());

expect(wrapper.find(SELECTORS.SWITCH_TO_HISTORY_LINK).text()).toBe(TEST_HISTORY.name);

// Since this is the user's own workflow, the indicators link
// (to view all published workflows by the owner) should not be present
expect(wrapper.find(SELECTORS.INDICATORS_LINK).exists()).toBe(false);
}
await checkHasRunCount("run_form");
await checkHasRunCount("invocation");
});

it("workflow indicators if the user does not own the workflow", async () => {
// we assume it is another user's published workflow in this case

async function checkHasIndicators(version: "run_form" | "invocation") {
const { wrapper } = await mountWorkflowAnnotation(version, false);

const indicatorsLink = wrapper.find(SELECTORS.INDICATORS_LINK);
expect(indicatorsLink.text()).toBe(WORKFLOW_OWNER);
expect(indicatorsLink.attributes("title")).toContain(
`Click to view all published workflows by '${WORKFLOW_OWNER}'`
);
}
await checkHasIndicators("run_form");
await checkHasIndicators("invocation");
});

it("renders time since edit if run form view", async () => {
const { wrapper } = await mountWorkflowAnnotation("run_form");

const timeInfo = wrapper.find(SELECTORS.TIME_INFO);
expect(timeInfo.text()).toContain("edited");
expect(timeInfo.find(SELECTORS.DATE).attributes("title")).toBe(WORKFLOW_UPDATE_TIME);
});

it("renders time since invocation if invocation view", async () => {
const { wrapper } = await mountWorkflowAnnotation("invocation");

const timeInfo = wrapper.find(SELECTORS.TIME_INFO);
expect(timeInfo.text()).toContain("invoked");
expect(timeInfo.find(SELECTORS.DATE).attributes("title")).toBe(INVOCATION_TIME);
});
});
17 changes: 10 additions & 7 deletions client/src/components/Workflow/WorkflowAnnotation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const description = computed(() => {
}
});
const timeElapsed = computed(() => {
return props.invocationUpdateTime || workflow.value?.update_time;
});
const workflowTags = computed(() => {
return workflow.value?.tags || [];
});
Expand All @@ -55,13 +59,12 @@ const workflowTags = computed(() => {
<div v-if="workflow" class="pb-2 pl-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<i v-if="props.invocationUpdateTime">
<FontAwesomeIcon :icon="faClock" class="mr-1" />invoked
<UtcDate :date="props.invocationUpdateTime" mode="elapsed" />
</i>
<i v-else-if="workflow.update_time">
<FontAwesomeIcon :icon="faClock" class="mr-1" />edited
<UtcDate :date="workflow.update_time" mode="elapsed" />
<i v-if="timeElapsed" data-description="workflow annotation time info">
<FontAwesomeIcon :icon="faClock" class="mr-1" />
<span v-localize>
{{ props.invocationUpdateTime ? "invoked" : "edited" }}
</span>
<UtcDate :date="timeElapsed" mode="elapsed" data-description="workflow annotation date" />
</i>
<span class="d-flex flex-gapx-1 align-items-center">
<FontAwesomeIcon :icon="faHdd" />Input History:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getLocalVue } from "tests/jest/helpers";
import sampleInvocation from "@/components/Workflow/test/json/invocation.json";
import { useUserStore } from "@/stores/userStore";

import WorkflowNavigationTitle from "../Workflow/WorkflowNavigationTitle.vue";
import WorkflowNavigationTitle from "./WorkflowNavigationTitle.vue";

// Constants
const WORKFLOW_OWNER = "test-user";
Expand All @@ -23,12 +23,12 @@ const SAMPLE_WORKFLOW = {
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']",
WORKFLOW_HEADING: "[data-description='workflow heading']",
ACTIONS_BUTTON_GROUP: "bbuttongroup-stub",
EDIT_WORKFLOW_BUTTON: `bbutton-stub[title='<b>Edit</b><br>${SAMPLE_WORKFLOW.name}']`,
IMPORT_WORKFLOW_BUTTON: "anonymous-stub[title='Import this workflow']",
RUN_WORKFLOW_BUTTON: `anonymous-stub[id='${SAMPLE_WORKFLOW.id}']`,
IMPORT_WORKFLOW_BUTTON: "[data-description='import workflow button']",
EXECUTE_WORKFLOW_BUTTON: "[data-description='execute workflow button']",
ROUTE_TO_RUN_BUTTON: "[data-description='route to workflow run button']",
ALERT_MESSAGE: "balert-stub",
};

Expand Down Expand Up @@ -63,19 +63,42 @@ const localVue = getLocalVue();

/**
* Mounts the WorkflowNavigationTitle component with props/stores adjusted given the parameters
* @param version The version of the component to mount (`run_form` or `invocation` view)
* @param ownsWorkflow Whether the user owns the workflow associated with the invocation
* @param unimportableWorkflow Whether the workflow import should fail
* @returns The wrapper object
* @returns The wrapper object, and the mockRouter object
*/
async function mountWorkflowNavigationTitle(ownsWorkflow = true, unimportableWorkflow = false) {
async function mountWorkflowNavigationTitle(
version: "run_form" | "invocation",
ownsWorkflow = true,
unimportableWorkflow = false
) {
const mockRouter = {
push: jest.fn(),
};

let workflowId: string;
let invocation;
if (version === "invocation") {
workflowId = !unimportableWorkflow ? sampleInvocation.workflow_id : UNIMPORTABLE_WORKFLOW_INSTANCE_ID;
invocation = {
...sampleInvocation,
workflow_id: workflowId,
};
} else {
workflowId = !unimportableWorkflow ? SAMPLE_WORKFLOW.id : UNIMPORTABLE_WORKFLOW_INSTANCE_ID;
invocation = undefined;
}

const wrapper = shallowMount(WorkflowNavigationTitle as object, {
propsData: {
invocation: {
...sampleInvocation,
workflow_id: !unimportableWorkflow ? sampleInvocation.workflow_id : UNIMPORTABLE_WORKFLOW_INSTANCE_ID,
},
invocation,
workflowId,
},
localVue,
mocks: {
$router: mockRouter,
},
pinia: createTestingPinia(),
});

Expand All @@ -84,42 +107,70 @@ async function mountWorkflowNavigationTitle(ownsWorkflow = true, unimportableWor
username: ownsWorkflow ? WORKFLOW_OWNER : OTHER_USER,
});

return { wrapper };
return { wrapper, mockRouter };
}

describe("WorkflowNavigationTitle 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 mountWorkflowNavigationTitle();
it("the workflow name in header and run button in actions; invocation version", async () => {
const { wrapper } = await mountWorkflowNavigationTitle("invocation");

const heading = wrapper.find(SELECTORS.INVOKED_WORKFLOW_HEADING);
expect(heading.text()).toBe(`Invoked Workflow: "${SAMPLE_WORKFLOW.name}"`);
const heading = wrapper.find(SELECTORS.WORKFLOW_HEADING);
expect(heading.text()).toContain(`Invoked Workflow: ${SAMPLE_WORKFLOW.name}`);
expect(heading.text()).toContain(`(version: ${SAMPLE_WORKFLOW.version + 1})`);

const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
const runButton = actionsGroup.find(SELECTORS.RUN_WORKFLOW_BUTTON);
const runButton = actionsGroup.find(SELECTORS.ROUTE_TO_RUN_BUTTON);
expect(runButton.attributes("title")).toContain("Rerun");
expect(runButton.attributes("title")).toContain(SAMPLE_WORKFLOW.name);
});

it("edit button if user owns the workflow", async () => {
const { wrapper } = await mountWorkflowNavigationTitle();
it("the workflow name in header and run button in actions; run form version", async () => {
const { wrapper } = await mountWorkflowNavigationTitle("run_form");

const heading = wrapper.find(SELECTORS.WORKFLOW_HEADING);
expect(heading.text()).toContain(`Workflow: ${SAMPLE_WORKFLOW.name}`);
expect(heading.text()).toContain(`(version: ${SAMPLE_WORKFLOW.version + 1})`);

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}`
);
const runButton = actionsGroup.find(SELECTORS.EXECUTE_WORKFLOW_BUTTON);
expect(runButton.attributes("title")).toContain("Run");
});

it("edit button if user owns the workflow", async () => {
async function findAndClickEditButton(version: "invocation" | "run_form") {
const { wrapper, mockRouter } = await mountWorkflowNavigationTitle(version);
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);

const editButton = actionsGroup.find(SELECTORS.EDIT_WORKFLOW_BUTTON);
await editButton.trigger("click");
await flushPromises();

expect(mockRouter.push).toHaveBeenCalledTimes(1);
expect(mockRouter.push).toHaveBeenCalledWith(
`/workflows/edit?id=${SAMPLE_WORKFLOW.id}&version=${SAMPLE_WORKFLOW.version}`
);
}
await findAndClickEditButton("invocation");
await findAndClickEditButton("run_form");
});

it("import button instead if user does not own the workflow", async () => {
const { wrapper } = await mountWorkflowNavigationTitle(false);
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON);
expect(importButton.exists()).toBe(true);
async function findImportButton(version: "invocation" | "run_form") {
const { wrapper } = await mountWorkflowNavigationTitle(version, false);
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON);
expect(importButton.exists()).toBe(true);
}
await findImportButton("invocation");
await findImportButton("run_form");
});
});

describe("Importing a workflow in WorkflowNavigationTitle", () => {
// We only need to test the `invocation` version because the button is the same in both versions

it("should show a confirmation dialog when the import is successful", async () => {
const { wrapper } = await mountWorkflowNavigationTitle(false);
const { wrapper } = await mountWorkflowNavigationTitle("invocation", false);
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON);

Expand All @@ -133,7 +184,7 @@ describe("Importing a workflow in WorkflowNavigationTitle", () => {
});

it("should show an error dialog when the import fails", async () => {
const { wrapper } = await mountWorkflowNavigationTitle(false, true);
const { wrapper } = await mountWorkflowNavigationTitle("invocation", false, true);
const actionsGroup = wrapper.find(SELECTORS.ACTIONS_BUTTON_GROUP);
const importButton = actionsGroup.find(SELECTORS.IMPORT_WORKFLOW_BUTTON);

Expand Down
Loading

0 comments on commit 936d0b2

Please sign in to comment.