import { getLocalVue } from "@tests/vitest/helpers"; import { mount, type Wrapper } from "@vue/test-utils"; import flushPromises from "flush-promises"; import { createPinia, defineStore, setActivePinia } from "pinia"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ref } from "vue"; import { useServerMock } from "@/api/client/__mocks__"; import type { State } from "@/components/History/Content/model/states"; import TEST_TOUR from "@/components/Tour/sampleTour.json"; import ToolTourGeneratorItem from "./ToolTourGeneratorItem.vue"; const TEST_TOOL_ID = "test-tool-id"; const TEST_TOOL_VERSION = "1.0"; const TEST_HISTORY_ID = "test-history-id"; const { server, http } = useServerMock(); // Mock the 3 stores used in the component vi.mock("@/stores/historyStore", () => { return { useHistoryStore: () => defineStore("history", { state: () => ({ currentHistoryId: TEST_HISTORY_ID, }), })(), }; }); let mockedHistoryItemsStore: ReturnType>; vi.mock("@/stores/historyItemsStore", () => { return { useHistoryItemsStore: () => mockedHistoryItemsStore, }; }); const setTourMock = vi.fn(); vi.mock("@/stores/tourStore", () => { return { useTourStore: () => defineStore("tour", { state: () => ({ toolGeneratedTours: {}, }), actions: { setTour: setTourMock, }, })(), }; }); // Mock the toast composable to track the messages const toastMock = vi.fn((message, type: "success" | "info" | "error") => { return { message, type }; }); vi.mock("@/composables/toast", () => ({ Toast: { success: vi.fn().mockImplementation((message) => { toastMock(message, "success"); }), info: vi.fn().mockImplementation((message) => { toastMock(message, "info"); }), error: vi.fn().mockImplementation((message) => { toastMock(message, "error"); }), }, })); describe("Tool Generated Tour Dropdown Item", () => { let wrapper: Wrapper; /** This is used to trigger a change in what `historyItemsStore.getStatesForHids` returns */ const currentItemState = ref(null); beforeEach(async () => { setActivePinia(createPinia()); mockedHistoryItemsStore = defineStore("historyItems", { getters: { // If `hids` are provided, return the `currentItemState` for each hid getStatesForHids: () => (_: string, hids: number[]) => { if (currentItemState.value === null) { return {}; } const entries = hids.map((hid) => [hid, currentItemState.value]); return Object.fromEntries(entries) as Record; }, }, })(); wrapper = mount(ToolTourGeneratorItem as object, { propsData: { toolId: TEST_TOOL_ID, toolVersion: TEST_TOOL_VERSION, }, localVue: getLocalVue(), stubs: { FontAwesomeIcon: true, }, }); }); afterEach(() => { wrapper.destroy(); server.resetHandlers(); currentItemState.value = null; setTourMock.mockClear(); toastMock.mockClear(); }); it("generates a basic tour (that doesn't wait on datasets) on click", async () => { server.use( http.get("/api/tours/generate", ({ response }) => { return response(200).json({ tour: TEST_TOUR, uploaded_hids: [], use_datasets: false, }); }), ); const dropdownItem = await clickDropdownItem(); // Since there is nothing to wait for the tour is ready and in the store tourHasGenerated(dropdownItem); // Only a singular toast confirming the tour is ready expect(toastMock).toHaveBeenCalledTimes(1); }); it("generates a tour that that waits for datasets to be ok", async () => { server.use( http.get("/api/tours/generate", ({ response }) => { return response(200).json({ tour: TEST_TOUR, uploaded_hids: [1, 2, 3], use_datasets: true, }); }), ); const dropdownItem = await clickDropdownItem(); // Unlike the tour without datasets, the tour is still generating after local state update tourIsGenerating(dropdownItem); // Confirm that there is a toast expect(toastMock).toHaveBeenCalledWith("This tour waits for history datasets to be ready.", "info"); // Now we mock history items going through states, and the tour generation completing only when all are ok await mockItemsState("new"); tourIsGenerating(dropdownItem); await mockItemsState("running"); tourIsGenerating(dropdownItem); await mockItemsState("ok"); tourHasGenerated(dropdownItem); // We know by now this is the 2nd toast expect(toastMock).toHaveBeenCalledTimes(2); }); it("generates a tour that that uploads datasets but they become invalid", async () => { server.use( http.get("/api/tours/generate", ({ response }) => { return response(200).json({ tour: TEST_TOUR, uploaded_hids: [1, 2, 3], use_datasets: true, }); }), ); const dropdownItem = await clickDropdownItem(); // Now we mock history items going through states, and the tour generation failing when one is invalid await mockItemsState("new"); tourIsGenerating(dropdownItem); await mockItemsState("running"); tourIsGenerating(dropdownItem); await mockItemsState("error"); tourGenerationFailedWith( dropdownItem, "This tour uploads datasets that failed to be created. You can try generating the tour again.", ); // We know by now this is the 2nd toast expect(toastMock).toHaveBeenCalledTimes(2); }); // LOCAL METHODS: ---------------------------------------------------------------------- /** Finds and confirms the tool generated tour dropdown item exists, and clicks it. */ async function clickDropdownItem() { const dropdownItem = wrapper.find("[data-description='click to generate tour']"); expect(dropdownItem.exists()).toBe(true); await dropdownItem.trigger("click"); tourIsGenerating(dropdownItem); // Flush after tour generation API call, which updates the local state await flushPromises(); return dropdownItem; } /** By simply setting the local `currentItemState` ref, we mock the `historyItemsStore` getter to * return the desired state for `uploaded_hids` as returned by the tour generation API. */ async function mockItemsState(state: State) { currentItemState.value = state; await flushPromises(); } /** Confirms the tour is _(still)_ generating, given that the dropdown item is disabled * and the tour store not yet updated. */ function tourIsGenerating(dropdownItem: Wrapper) { expect(dropdownItem.attributes("aria-disabled")).toBe("true"); expect(setTourMock).toHaveBeenCalledTimes(0); } /** Confirms the tour has been generated and the `tourStore` updated with it. */ function tourHasGenerated(dropdownItem: Wrapper) { // The second toast confirms the tour is ready expect(toastMock).toHaveBeenCalledWith("You can now start the tour", "success"); expect(dropdownItem.attributes("aria-disabled")).toBeUndefined(); // The tour is now in the store, with the expected key expect(setTourMock).toHaveBeenCalledWith(`tool-generated-${TEST_TOOL_ID}-${TEST_TOOL_VERSION}`); } /** Confirms the tour generation failed, the dropdown item is enabled, the tour store not updated * and the expected error is message shown in a toast. */ function tourGenerationFailedWith(dropdownItem: Wrapper, message: string) { // The second toast confirms the tour generation failed expect(toastMock).toHaveBeenCalledWith(message, "error"); expect(dropdownItem.attributes("aria-disabled")).toBeUndefined(); expect(setTourMock).toHaveBeenCalledTimes(0); } });