diff --git a/client/src/components/Form/Elements/FormData/FormData.test.js b/client/src/components/Form/Elements/FormData/FormData.test.js index 709f2910a791..0453772e6f28 100644 --- a/client/src/components/Form/Elements/FormData/FormData.test.js +++ b/client/src/components/Form/Elements/FormData/FormData.test.js @@ -1,7 +1,7 @@ import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; import { PiniaVuePlugin } from "pinia"; -import { dispatchEvent, getLocalVue } from "tests/jest/helpers"; +import { dispatchEvent, getLocalVue, stubHelpPopovers } from "tests/jest/helpers"; import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; @@ -51,6 +51,8 @@ const defaultOptions = { const SELECT_OPTIONS = ".multiselect__element"; const SELECTED_VALUE = ".multiselect__option--selected span"; +stubHelpPopovers(); + describe("FormData", () => { it("regular data", async () => { const wrapper = createTarget({ diff --git a/client/src/components/History/CurrentHistory/SelectPreferredStore.test.ts b/client/src/components/History/CurrentHistory/SelectPreferredStore.test.ts index 7638ef43b918..c500afa2af64 100644 --- a/client/src/components/History/CurrentHistory/SelectPreferredStore.test.ts +++ b/client/src/components/History/CurrentHistory/SelectPreferredStore.test.ts @@ -3,6 +3,7 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import flushPromises from "flush-promises"; import { getLocalVue } from "tests/jest/helpers"; +import { h } from "vue"; import { useServerMock } from "@/api/client/__mocks__"; import { ROOT_COMPONENT } from "@/utils/navigation/schema"; @@ -42,6 +43,13 @@ async function mountComponent() { const PREFERENCES = ROOT_COMPONENT.preferences; +// bootstrap vue will try to match targets to actual HTML elements in DOM but there +// may be no DOM for jest tests, just stub out an alternative minimal implementation. +jest.mock("@/components/ObjectStore/ObjectStoreSelectButtonPopover.vue", () => ({ + name: "ObjectStoreSelectButtonPopover", + render: () => h("div", "Mocked Popover"), +})); + describe("SelectPreferredStore.vue", () => { let axiosMock: MockAdapter; diff --git a/client/src/components/History/HistoryView.test.js b/client/src/components/History/HistoryView.test.js index 85dc95106ede..b00e2647f7a9 100644 --- a/client/src/components/History/HistoryView.test.js +++ b/client/src/components/History/HistoryView.test.js @@ -17,6 +17,11 @@ jest.mock("stores/services/history.services"); const { server, http } = useServerMock(); +jest.mock("vue-router/composables", () => ({ + useRoute: jest.fn(() => ({})), + useRouter: jest.fn(() => ({})), +})); + function create_history(historyId, userId, purged = false, archived = false) { const historyName = `${userId}'s History ${historyId}`; return { diff --git a/client/src/components/JobInformation/JobInformation.test.js b/client/src/components/JobInformation/JobInformation.test.js index 68602a1b6ffd..b356fed1d75b 100644 --- a/client/src/components/JobInformation/JobInformation.test.js +++ b/client/src/components/JobInformation/JobInformation.test.js @@ -1,6 +1,6 @@ import { mount } from "@vue/test-utils"; import flushPromises from "flush-promises"; -import { getLocalVue } from "tests/jest/helpers"; +import { getLocalVue, stubHelpPopovers } from "tests/jest/helpers"; import { useServerMock } from "@/api/client/__mocks__"; @@ -16,6 +16,8 @@ const localVue = getLocalVue(); const { server, http } = useServerMock(); +stubHelpPopovers(); + describe("JobInformation/JobInformation.vue", () => { let wrapper; let jobInfoTable; diff --git a/client/src/components/Toolshed/RepositoryDetails/Index.test.ts b/client/src/components/Toolshed/RepositoryDetails/Index.test.ts index b2a8cc0b19a7..e733ed052b9b 100644 --- a/client/src/components/Toolshed/RepositoryDetails/Index.test.ts +++ b/client/src/components/Toolshed/RepositoryDetails/Index.test.ts @@ -1,7 +1,7 @@ import { shallowMount } from "@vue/test-utils"; import flushPromises from "flush-promises"; import { createPinia } from "pinia"; -import { getLocalVue } from "tests/jest/helpers"; +import { getLocalVue, suppressDebugConsole } from "tests/jest/helpers"; import { HttpResponse, useServerMock } from "@/api/client/__mocks__"; @@ -10,6 +10,8 @@ import Index from "./Index.vue"; const { server, http } = useServerMock(); describe("RepositoryDetails", () => { + suppressDebugConsole(); // we issue a debug warning when a repo has no revisions + it("test repository details index", async () => { server.use( http.get("/api/configuration", ({ response }) => { diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.test.js b/client/src/components/Workflow/Editor/Forms/FormTool.test.js index 72ce85fddec6..56c65045504d 100644 --- a/client/src/components/Workflow/Editor/Forms/FormTool.test.js +++ b/client/src/components/Workflow/Editor/Forms/FormTool.test.js @@ -2,8 +2,11 @@ import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; +import flushPromises from "flush-promises"; import { getLocalVue } from "tests/jest/helpers"; +import { useServerMock } from "@/api/client/__mocks__"; + import FormTool from "./FormTool"; jest.mock("@/api/schema"); @@ -17,10 +20,20 @@ jest.mock("@/composables/config", () => ({ const localVue = getLocalVue(); +const { server, http } = useServerMock(); + describe("FormTool", () => { const axiosMock = new MockAdapter(axios); axiosMock.onGet(`/api/webhooks`).reply(200, []); + beforeEach(() => { + server.use( + http.get("/api/configuration", ({ response }) => { + return response(200).json({}); + }) + ); + }); + function mountTarget() { return mount(FormTool, { propsData: { @@ -35,6 +48,7 @@ describe("FormTool", () => { description: "description", inputs: [{ name: "input", label: "input", type: "text", value: "value" }], help: "help_text", + help_format: "restructuredtext", versions: ["1.0", "2.0", "3.0"], citations: false, }, @@ -71,5 +85,6 @@ describe("FormTool", () => { state = wrapper.emitted().onSetData[1][1]; expect(state.tool_version).toEqual("3.0"); expect(state.tool_id).toEqual("tool_id+3.0"); + await flushPromises(); }); }); diff --git a/client/src/components/Workflow/Editor/Index.test.ts b/client/src/components/Workflow/Editor/Index.test.ts index 62e9673f3673..7262d2d72956 100644 --- a/client/src/components/Workflow/Editor/Index.test.ts +++ b/client/src/components/Workflow/Editor/Index.test.ts @@ -38,7 +38,7 @@ describe("Index", () => { const datatypesStore = useDatatypesMapperStore(); datatypesStore.datatypesMapper = testDatatypesMapper; mockLoadWorkflow.mockResolvedValue({ steps: {} }); - MockGetVersions.mockResolvedValue(() => []); + MockGetVersions.mockResolvedValue([]); mockGetStateUpgradeMessages.mockImplementation(() => []); mockGetAppRoot.mockImplementation(() => "prefix/"); Object.defineProperty(window, "onbeforeunload", { diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index 99087e09d500..1943de38e215 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -190,7 +190,7 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faArrowLeft, faArrowRight, faHistory } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { useMagicKeys, whenever } from "@vueuse/core"; +import { whenever } from "@vueuse/core"; import { logicAnd, logicNot, logicOr } from "@vueuse/math"; import { Toast } from "composables/toast"; import { storeToRefs } from "pinia"; @@ -199,6 +199,7 @@ import Vue, { computed, nextTick, onUnmounted, ref, unref, watch } from "vue"; import { getUntypedWorkflowParameters } from "@/components/Workflow/Editor/modules/parameters"; import { ConfirmDialog } from "@/composables/confirmDialog"; import { useDatatypesMapper } from "@/composables/datatypesMapper"; +import { useMagicKeys } from "@/composables/useMagicKeys"; import { useUid } from "@/composables/utils/uid"; import { provideScopedWorkflowStores } from "@/composables/workflowStores"; import { hide_modal } from "@/layout/modal"; diff --git a/client/src/components/Workflow/Editor/NodeOutput.test.ts b/client/src/components/Workflow/Editor/NodeOutput.test.ts index 414ebd7e42f3..db7f46221b85 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.test.ts +++ b/client/src/components/Workflow/Editor/NodeOutput.test.ts @@ -37,6 +37,9 @@ function propsForStep(step: Step) { scroll: { x: ref(0), y: ref(0) }, scale: 1, datatypesMapper: testDatatypesMapper, + parentNode: null, + readonly: true, + blank: false, }; } diff --git a/client/src/composables/useMagicKeys.js b/client/src/composables/useMagicKeys.js new file mode 100644 index 000000000000..1d2338ce6c00 --- /dev/null +++ b/client/src/composables/useMagicKeys.js @@ -0,0 +1,15 @@ +import { useMagicKeys as wrappedUseMagicKeys } from "@vueuse/core"; +import Vue from "vue"; + +export function useMagicKeys() { + // a version of useMagicKeys from vueuse/core that doesn't console.error the + // the message [Vue warn]: Vue 2 does not support reactive collection types such as Map or Set. + // in all our tests. This can be dropped after the migration to Vue3. + const oldSlientConfig = Vue.config.silent; + try { + Vue.config.silent = true; + return wrappedUseMagicKeys(); + } finally { + Vue.config.silent = oldSlientConfig; + } +} diff --git a/client/src/stores/workflowEditorToolbarStore.ts b/client/src/stores/workflowEditorToolbarStore.ts index 3bb36ea75cda..901a823943b8 100644 --- a/client/src/stores/workflowEditorToolbarStore.ts +++ b/client/src/stores/workflowEditorToolbarStore.ts @@ -1,7 +1,7 @@ -import { useMagicKeys } from "@vueuse/core"; import { computed, onScopeDispose, reactive, ref, watch } from "vue"; import { type Rectangle } from "@/components/Workflow/Editor/modules/geometry"; +import { useMagicKeys } from "@/composables/useMagicKeys"; import { useUserLocalStorage } from "@/composables/userLocalStorage"; import { defineScopedStore } from "./scopedStore"; diff --git a/client/src/utils/upload-queue.test.js b/client/src/utils/upload-queue.test.js index 3e5a72588132..ce38d572ea30 100644 --- a/client/src/utils/upload-queue.test.js +++ b/client/src/utils/upload-queue.test.js @@ -9,52 +9,66 @@ function StubFile(name = null, size = 0, mode = "local") { return { name, size, mode }; } +function instrumentedUploadQueue(options = {}) { + const uploadQueue = new UploadQueue(options); + uploadQueue.encountedErrors = false; + uploadQueue.opts.error = function (d, m) { + uploadQueue.encountedErrors = true; + }; + return uploadQueue; +} + describe("UploadQueue", () => { test("a queue is initialized to correct state", () => { - const q = new UploadQueue({ foo: 1 }); + const q = instrumentedUploadQueue({ foo: 1 }); expect(q.size).toEqual(0); expect(q.isRunning).toBe(false); expect(q.opts.foo).toEqual(1); // passed as options expect(q.opts.multiple).toBe(true); // default value + expect(q.encountedErrors).toBeFalsy(); }); test("resetting the queue removes all files from it", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); q.add([StubFile("a"), StubFile("b")]); expect(q.size).toEqual(2); q.reset(); expect(q.size).toEqual(0); + expect(q.encountedErrors).toBeFalsy(); }); test("calling configure updates options", () => { - const q = new UploadQueue({ foo: 1 }); + const q = instrumentedUploadQueue({ foo: 1 }); expect(q.opts.foo).toEqual(1); expect(q.opts.bar).toBeUndefined(); q.configure({ bar: 2 }); // overwrite bar expect(q.opts.foo).toEqual(1); // value unchangee expect(q.opts.bar).toEqual(2); // value overwritten + expect(q.encountedErrors).toBeFalsy(); }); test("calling start sets isRunning to true", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); q._process = jest.fn(); // mock this, otherwise it'll reset isRunning after it's done. expect(q.isRunning).toBe(false); q.start(); expect(q.isRunning).toBe(true); + expect(q.encountedErrors).toBeFalsy(); }); test("calling start is a noop if queue is running", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); const mockedProcess = jest.fn(); q._process = mockedProcess(); q.isRunning = true; q.start(); expect(mockedProcess.mock.calls.length === 0); // function was not called + expect(q.encountedErrors).toBeFalsy(); }); test("calling start processes all files in queue", () => { const fileEntries = {}; - const q = new UploadQueue({ + const q = instrumentedUploadQueue({ get: (index) => fileEntries[index], announce: (index, file) => { fileEntries[index] = { @@ -62,6 +76,7 @@ describe("UploadQueue", () => { fileName: file.name, fileSize: file.size, fileContent: "fileContent", + targetHistoryId: "mockhistoryid", }; }, }); @@ -71,12 +86,13 @@ describe("UploadQueue", () => { q.add([StubFile("a"), StubFile("b")]); q.start(); expect(q.size).toEqual(0); + expect(q.encountedErrors).toBeFalsy(); expect(spy.mock.calls.length).toEqual(3); // called for 2, 1, 0 files. spy.mockRestore(); // not necessary, but safer, in case we later modify implementation. }); test("calling stop sets isPaused to true", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); q.start(); expect(q.isPaused).toBe(false); q.stop(); @@ -84,7 +100,7 @@ describe("UploadQueue", () => { }); test("adding files increases the queue size by the number of files", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); expect(q.size).toEqual(0); q.add([StubFile("a"), StubFile("b")]); expect(q.nextIndex).toEqual(2); @@ -94,14 +110,14 @@ describe("UploadQueue", () => { }); test("adding files increases the next index by the number of files", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); expect(q.nextIndex).toEqual(0); q.add([StubFile("a"), StubFile("b")]); expect(q.nextIndex).toEqual(2); }); test("duplicate files are not added to the queue, unless the mode is set to 'new'", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); const file1 = StubFile("a", 1); const file2 = StubFile("a", 1); const file3 = StubFile("a", 1, "new"); @@ -115,7 +131,7 @@ describe("UploadQueue", () => { test("adding a file calls opts.announce with correct arguments", () => { const mockAnnounce = jest.fn(); - const q = new UploadQueue({ announce: mockAnnounce }); + const q = instrumentedUploadQueue({ announce: mockAnnounce }); const file = StubFile("a"); expect(mockAnnounce.mock.calls.length).toBe(0); q.add([file]); @@ -126,7 +142,7 @@ describe("UploadQueue", () => { test("removing a file reduces the queue size by 1", () => { const fileEntries = {}; - const q = new UploadQueue({ + const q = instrumentedUploadQueue({ announce: (index, file) => { fileEntries[index] = file; }, @@ -138,7 +154,7 @@ describe("UploadQueue", () => { }); test("removing a file by index out of sequence is allowed", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); const file1 = StubFile("a"); const file2 = StubFile("b"); const file3 = StubFile("c"); @@ -149,10 +165,11 @@ describe("UploadQueue", () => { expect(q.queue.get("0")).toBe(file1); expect(q.queue.get("1")).toBeUndefined(); expect(q.queue.get("2")).toBe(file3); + expect(q.encountedErrors).toBeFalsy(); }); test("removing a file via _processIndex, obeys FIFO protocol", () => { - const q = new UploadQueue(); + const q = instrumentedUploadQueue(); q.add([StubFile("a"), StubFile("b")]); let nextIndex = q._processIndex(); expect(nextIndex).toEqual("0"); @@ -161,11 +178,12 @@ describe("UploadQueue", () => { expect(nextIndex).toEqual("1"); q.remove(nextIndex); expect(q._processIndex()).toBeUndefined(); + expect(q.encountedErrors).toBeFalsy(); }); test("remote file batch", () => { const fileEntries = {}; - const q = new UploadQueue({ + const q = instrumentedUploadQueue({ historyId: "historyId", announce: (index, file) => { fileEntries[index] = { @@ -227,5 +245,6 @@ describe("UploadQueue", () => { }, ], }); + expect(q.encountedErrors).toBeFalsy(); }); }); diff --git a/client/tests/jest/helpers.js b/client/tests/jest/helpers.js index ff5f1e716919..bb5b2a943f1a 100644 --- a/client/tests/jest/helpers.js +++ b/client/tests/jest/helpers.js @@ -273,3 +273,16 @@ export function injectTestRouter(localVue) { const router = new VueRouter(); return router; } + +export function suppressDebugConsole() { + jest.spyOn(console, "debug").mockImplementation(jest.fn()); +} + +export function stubHelpPopovers() { + // bootstrap vue will try to match targets to actual HTML elements in DOM but there + // may be no DOM for jest tests, just stub out an alternative minimal implementation. + jest.mock("@/components/Help/HelpPopover.vue", () => ({ + name: "HelpPopover", + render: (h) => h("div", "Mocked Popover"), + })); +} diff --git a/client/tests/jest/jest.setup.js b/client/tests/jest/jest.setup.js index 505e186fd517..bf547952dc9f 100644 --- a/client/tests/jest/jest.setup.js +++ b/client/tests/jest/jest.setup.js @@ -4,6 +4,9 @@ import "fake-indexeddb/auto"; import Vue from "vue"; +// not available in jsdom, mock it out +Element.prototype.scrollIntoView = jest.fn(); + // Set Vue to suppress production / devtools / etc. warnings Vue.config.productionTip = false; Vue.config.devtools = false;