Skip to content

Commit

Permalink
refactor WorkflowInvocationState and improve its jest
Browse files Browse the repository at this point in the history
Added a mock implementation for the store to detect fetches and use those to check if invocation or job states are terminal
  • Loading branch information
ahmedhamidawan committed Apr 17, 2024
1 parent ccc6ba1 commit a3691ce
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 156 deletions.
9 changes: 8 additions & 1 deletion client/src/components/Workflow/InvocationsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
<div class="invocations-list" aria-labelledby="invocations-title">
<div class="grid-header d-flex justify-content-between pb-2 flex-column">
<div class="d-flex">
<Heading h1 separator inline size="xl" class="flex-grow-1 m-0" data-description="grid title">
<Heading
id="invocations-title"
h1
separator
inline
size="xl"
class="flex-grow-1 m-0"
data-description="grid title">
<span v-localize>{{ title }}</span>
</Heading>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,91 @@
import { createTestingPinia } from "@pinia/testing";
import { shallowMount, Wrapper } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { setActivePinia } from "pinia";
import { PiniaVuePlugin, setActivePinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";

import type { WorkflowInvocation } from "@/api/invocations";

import invocationData from "../Workflow/test/json/invocation.json";

import WorkflowInvocationState from "./WorkflowInvocationState.vue";

const localVue = getLocalVue();
localVue.use(PiniaVuePlugin);

jest.mock("vue-router/composables", () => ({
useRoute: jest.fn().mockReturnValue({ path: "" }),
}));

const selectors = {
invocationSummary: ".invocation-summary",
};

const invocationJobsSummaryById = {
id: "d9833097445452b0",
/** Invocation data to be expected in the store */
const invocationById = {
[invocationData.id]: invocationData,
"not-fetched-invocation": null,
"non-terminal-id": {
...invocationData,
id: "non-terminal-id",
state: "new",
},
"non-terminal-jobs": {
...invocationData,
id: "non-terminal-jobs",
},
};

// Jobs summary constants
const invocationDataJobsSummary = {
model: "WorkflowInvocation",
states: {},
populated_state: "ok",
};
const invocationJobsSummaryById = {
[invocationData.id]: invocationDataJobsSummary,
"not-fetched-invocation": null,
"non-terminal-invocation": invocationDataJobsSummary,
"non-terminal-jobs": {
...invocationDataJobsSummary,
states: {
running: 1,
},
},
};

async function mountWorkflowInvocationState(invocation: WorkflowInvocation | null) {
// Mock the invocation store to return the expected invocation data given the invocation ID
jest.mock("@/stores/invocationStore", () => {
const originalModule = jest.requireActual("@/stores/invocationStore");
const mockFetchInvocationForId = jest.fn();
const mockFetchInvocationJobsSummaryForId = jest.fn();
return {
...originalModule,
useInvocationStore: () => ({
...originalModule.useInvocationStore(),
getInvocationById: jest.fn().mockImplementation((invocationId) => {
return invocationById[invocationId];
}),
getInvocationJobsSummaryById: jest.fn().mockImplementation((invocationId) => {
return invocationJobsSummaryById[invocationId];
}),
fetchInvocationForId: mockFetchInvocationForId,
fetchInvocationJobsSummaryForId: mockFetchInvocationJobsSummaryForId,
}),
mockFetchInvocationForId,
mockFetchInvocationJobsSummaryForId,
};
});

/** Mount the WorkflowInvocationState component with the given invocation ID
* @param invocationId The invocation ID to be passed as a prop
* @returns The mounted wrapper
*/
async function mountWorkflowInvocationState(invocationId: string) {
const pinia = createTestingPinia();
setActivePinia(pinia);

const wrapper = shallowMount(WorkflowInvocationState, {
const wrapper = shallowMount(WorkflowInvocationState as object, {
propsData: {
invocationId: invocationData.id,
},
computed: {
invocation: () => invocation,
jobStatesSummary: () => invocationJobsSummaryById,
invocationId,
},
pinia,
localVue,
Expand All @@ -44,29 +96,60 @@ async function mountWorkflowInvocationState(invocation: WorkflowInvocation | nul

describe("WorkflowInvocationState.vue", () => {
it("determines that invocation and job states are terminal with terminal invocation", async () => {
const wrapper = await mountWorkflowInvocationState(invocationData as WorkflowInvocation);
const wrapper = await mountWorkflowInvocationState(invocationData.id);
expect(isInvocationAndJobTerminal(wrapper)).toBe(true);

// Neither the invocation nor the jobs summary should be fetched for terminal invocations
assertInvocationFetched(0);
assertJobsSummaryFetched(0);
});

it("determines that invocation and job states are not terminal with no invocation", async () => {
const wrapper = await mountWorkflowInvocationState(null);
it("determines that invocation and job states are not terminal with no fetched invocation", async () => {
const wrapper = await mountWorkflowInvocationState("not-fetched-invocation");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Both, the invocation and jobs summary should be fetched once if the invocation is not in the store
assertInvocationFetched(1);
assertJobsSummaryFetched(1);
});

it("determines that invocation and job states are not terminal with non-terminal invocation", async () => {
const invocation = {
...invocationData,
state: "new",
} as WorkflowInvocation;
const wrapper = await mountWorkflowInvocationState(invocation);
it("determines that invocation is not terminal with non-terminal state", async () => {
const wrapper = await mountWorkflowInvocationState("non-terminal-invocation");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Only the invocation should be fetched for non-terminal invocations
assertInvocationFetched(1);
assertJobsSummaryFetched(0);
});

it("determines that job states are not terminal with non-terminal jobs but scheduled invocation", async () => {
const wrapper = await mountWorkflowInvocationState("non-terminal-jobs");
expect(isInvocationAndJobTerminal(wrapper)).toBe(false);

// Only the jobs summary should be fetched, not the invocation since it is in scheduled/terminal state
assertInvocationFetched(0);
assertJobsSummaryFetched(1);
});
});

/**
* This is a somewhat hacky way to determine if the invocation and job states are terminal without
* exposing the internals of the component. This is just to restore the previous behavior of the test
* and it only uses the wrapper to check the props of the invocation summary component.
*/
function isInvocationAndJobTerminal(wrapper: Wrapper<Vue>): boolean {
const invocationSummary = wrapper.find(selectors.invocationSummary);
// This is a somewhat hacky way to determine if the invocation and job states are terminal without
// exposing the internals of the component. This is just to restore the previous behavior of the test
// but it would be better to test this in a more appropriate way.
return invocationSummary.exists() && invocationSummary.html().includes('invocationandjobterminal="true"');
}

/** Asserts that the invocation was fetched in the store the given number of times */
function assertInvocationFetched(count = 1) {
const { mockFetchInvocationForId } = jest.requireMock("@/stores/invocationStore");
expect(mockFetchInvocationForId).toHaveBeenCalledTimes(count);
}

/** Asserts that the jobs summary was fetched in the store the given number of times */
function assertJobsSummaryFetched(count = 1) {
const { mockFetchInvocationJobsSummaryForId } = jest.requireMock("@/stores/invocationStore");
expect(mockFetchInvocationJobsSummaryForId).toHaveBeenCalledTimes(count);
}
Loading

0 comments on commit a3691ce

Please sign in to comment.