import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; import { useUndoRedoStore } from "@/stores/undoRedoStore"; import { useConnectionStore } from "@/stores/workflowConnectionStore"; import { useWorkflowCommentStore } from "@/stores/workflowEditorCommentStore"; import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore"; import { useWorkflowEditorToolbarStore } from "@/stores/workflowEditorToolbarStore"; import { useWorkflowSearchStore } from "@/stores/workflowSearchStore"; import { type DataOutput, type Step, type Steps, type TerminalSource, useWorkflowStepStore, } from "@/stores/workflowStepStore"; import advancedStepsJson from "../test-data/parameter_steps.json"; import simpleStepsJson from "../test-data/simple_steps.json"; import { ANY_COLLECTION_TYPE_DESCRIPTION, CollectionTypeDescription, NULL_COLLECTION_TYPE_DESCRIPTION, } from "./collectionTypeDescription"; import { InputCollectionTerminal, InputParameterTerminal, InputTerminal, InvalidOutputTerminal, NO_COLLECTION_TYPE_INFORMATION_MESSAGE, OutputCollectionTerminal, OutputParameterTerminal, OutputTerminal, producesAcceptableDatatype, terminalFactory, } from "./terminals"; const advancedSteps = advancedStepsJson as Steps; const simpleSteps = simpleStepsJson as Steps; function useStores(id = "mock-workflow") { const connectionStore = useConnectionStore(id); const stateStore = useWorkflowStateStore(id); const stepStore = useWorkflowStepStore(id); const commentStore = useWorkflowCommentStore(id); const toolbarStore = useWorkflowEditorToolbarStore(id); const undoRedoStore = useUndoRedoStore(id); const searchStore = useWorkflowSearchStore(id); return { workflowId: id, connectionStore, stateStore, stepStore, commentStore, toolbarStore, undoRedoStore, searchStore, }; } // Suppress debug messages about node configurations, we're testing esoteric things here - // we might want these messages at runtime to help debug complex things but we don't need it // during unit testing. vi.spyOn(console, "debug").mockImplementation( vi.fn((msg) => { if (msg != NO_COLLECTION_TYPE_INFORMATION_MESSAGE) { console.debug(msg); } }), ); function setupAdvanced() { const terminals: { [index: string]: { [index: string]: ReturnType } } = {}; const stores = useStores(); Object.values(advancedSteps).map((step) => { const stepLabel = step.label; if (stepLabel) { terminals[stepLabel] = {}; step.inputs?.map((input) => { terminals[stepLabel]![input.name] = terminalFactory(step.id, input, testDatatypesMapper, stores); }); step.outputs?.map((output) => { terminals[stepLabel]![output.name] = terminalFactory(step.id, output, testDatatypesMapper, stores); }); } }); return terminals; } function rebuildTerminal>(terminal: T): T { let terminalSource: TerminalSource; const step = terminal.stores.stepStore.getStep(terminal.stepId); const stores = useStores(); if (terminal.terminalType === "input") { terminalSource = step!.inputs.find((input) => input.name == terminal.name)!; } else { terminalSource = step!.outputs.find((output) => output.name == terminal.name)!; } return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper, stores) as T; } describe("terminalFactory", () => { let terminals: { [index: string]: { [index: string]: ReturnType } } = {}; beforeEach(() => { setActivePinia(createPinia()); terminals = setupAdvanced(); }); it("constructs correct class instances", () => { expect(terminals["data input"]?.["output"]).toBeInstanceOf(OutputTerminal); expect(terminals["simple data"]?.["input"]).toBeInstanceOf(InputTerminal); expect(terminals["simple data"]?.["out_file1"]).toBeInstanceOf(OutputTerminal); expect(terminals["simple data 2"]?.["input"]).toBeInstanceOf(InputTerminal); expect(terminals["simple data 2"]?.["out_file1"]).toBeInstanceOf(OutputTerminal); expect(terminals["multiple simple data"]?.["input1"]).toBeInstanceOf(InputTerminal); expect(terminals["multiple simple data"]?.["queries_0|input2"]).toBeInstanceOf(InputTerminal); expect(terminals["multiple simple data"]?.["out_file1"]).toBeInstanceOf(OutputTerminal); expect(terminals["optional data input"]?.["output"]).toBeInstanceOf(OutputTerminal); expect(terminals["list input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); expect(terminals["list:list input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); expect(terminals["list:list:paired input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); expect(terminals["paired input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); expect(terminals["multi data"]?.["f1"]).toBeInstanceOf(InputTerminal); expect(terminals["multi data"]?.["f2"]).toBeInstanceOf(InputTerminal); expect(terminals["multi data"]?.["out1"]).toBeInstanceOf(OutputTerminal); expect(terminals["multi data"]?.["out2"]).toBeInstanceOf(OutputTerminal); expect(terminals["integer parameter input"]?.["output"]).toBeInstanceOf(OutputParameterTerminal); expect(terminals["any collection"]?.["input"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["any collection"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); expect(terminals["multi data"]?.["advanced|advanced_threshold"]).toBeInstanceOf(InputParameterTerminal); expect(terminals["list collection input"]?.["input1"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["paired collection input"]?.["f1"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["paired_or_unpaired collection input"]?.["f1"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["list:paired_or_unpaired collection input"]?.["f1"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["filter_failed"]?.["input"]).toBeInstanceOf(InputCollectionTerminal); expect(terminals["filter_failed"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal); }); it("throws error on invalid terminalSource", () => { const stores = useStores(); const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper, stores); expect(invalidFactory).toThrow(); }); }); describe("canAccept", () => { let terminals: { [index: string]: { [index: string]: ReturnType } } = {}; let stepStore: ReturnType; let connectionStore: ReturnType; beforeEach(() => { setActivePinia(createPinia()); terminals = setupAdvanced(); stepStore = useWorkflowStepStore("mock-workflow"); connectionStore = useConnectionStore("mock-workflow"); Object.values(JSON.parse(JSON.stringify(advancedSteps)) as Steps).map((step) => { stepStore.addStep(step); }); }); it("accepts simple data -> data connections", () => { const dataOut = terminals["data input"]!["output"] as OutputTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.canAccept(dataOut).canAccept).toBe(true); dataIn.connect(dataOut); expect(dataIn.canAccept(dataOut).canAccept).toBe(false); dataIn.disconnect(dataOut); expect(dataIn.canAccept(dataOut).canAccept).toBe(true); }); it("accepts collection data -> data connection", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts paired data -> data connection", () => { const collectionOut = terminals["paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "paired", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts mapped over data output on mapped over data input", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["multiple simple data"]!["input1"] as InputTerminal; const dataInTwo = terminals["multiple simple data"]!["queries_0|input2"] as InputTerminal; dataIn.connect(collectionOut); expect(dataInTwo.canAccept(collectionOut).canAccept).toBe(true); }); it("accepts list:list data -> data connection", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list:list", isCollection: true, rank: 2 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts paired_or_unpaired data -> data connection", () => { const collectionOut = terminals["paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "paired_or_unpaired", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list:paired_or_unpaired data -> data connection", () => { const collectionOut = terminals["list:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list:paired_or_unpaired", isCollection: true, rank: 2 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list:paired_or_unpaired data -> list:paired_or_unpaired connection", () => { const collectionOut = terminals["list:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); }); it("accepts paired_or_unpaired data -> paired_or_unpaired connection", () => { const collectionOut = terminals["paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list:paired -> paired connection", () => { const collectionOut = terminals["list:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts list:paired -> paired_or_unpaired connection", () => { const collectionOut = terminals["list:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts list -> paired_or_unpaired connection", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list:list:paired -> paired_or_unpaired connection", () => { const collectionOut = terminals["list:list:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list:list", isCollection: true, rank: 2 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts list:list:paired -> list:paired_or_unpaired connection", () => { const collectionOut = terminals["list:list:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired_or_unpaired collection input"]!["f1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts list:list -> paired_or_unpaired connection", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list:list", isCollection: true, rank: 2 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts list:list -> list:paired_or_unpaired connection", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired_or_unpaired collection input"]!["f1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("accepts paired -> paired_or_unpaired connection", () => { const collectionOut = terminals["paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts paired -> paired connection", () => { const collectionOut = terminals["paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list -> list connection", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list collection input"]!["input1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts list:paired -> list:paired connection", () => { const collectionOut = terminals["list:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired collection input"]!["input1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("rejects paired:paired -> list:paired connection", () => { const collectionOut = terminals["paired:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired collection input"]!["input1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("rejects paired:paired -> list:paired_or_unpaired connection", () => { const collectionOut = terminals["paired:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired_or_unpaired collection input"]!["f1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("rejects paired_or_unpaired -> paired connection", () => { const collectionOut = terminals["paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Cannot attach optionally paired outputs to inputs requiring pairing, consider using the 'Split Paired and Unpaired' tool to extract just the pairs out from this output.", ); }); it("rejects list:paired_or_unpaired -> paired connection", () => { const collectionOut = terminals["list:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Cannot attach optionally paired outputs to inputs requiring pairing, consider using the 'Split Paired and Unpaired' tool to extract just the pairs out from this output.", ); }); it("rejects list:paired_or_unpaired -> list connection", () => { const collectionOut = terminals["list:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list collection input"]!["input1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); }); it("treats multi data input as list input", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(true); multiDataIn.connect(collectionOut); expect(multiDataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts multiple simple data inputs on multi-data input", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const dataOutOne = terminals["data input"]!["output"] as OutputTerminal; const dataOutTwo = terminals["simple data"]!["out_file1"] as OutputTerminal; let step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toBe(undefined); multiDataIn.connect(dataOutOne); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([{ id: dataOutOne.stepId, output_name: dataOutOne.name }]); multiDataIn.connect(dataOutTwo); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([ { id: dataOutOne.stepId, output_name: dataOutOne.name }, { id: dataOutTwo.stepId, output_name: dataOutTwo.name }, ]); multiDataIn.disconnect(dataOutTwo); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([{ id: dataOutOne.stepId, output_name: dataOutOne.name }]); multiDataIn.disconnect(dataOutOne); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([]); multiDataIn.connect(dataOutOne); multiDataIn.connect(dataOutTwo); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([ { id: dataOutOne.stepId, output_name: dataOutOne.name }, { id: dataOutTwo.stepId, output_name: dataOutTwo.name }, ]); stepStore.removeStep(dataOutTwo.stepId); step = stepStore.getStep(multiDataIn.stepId)!; expect(step.input_connections["f1"]).toStrictEqual([{ id: dataOutOne.stepId, output_name: dataOutOne.name }]); }); it("accepts separate list:list inputs on separate multi-data inputs of same tool", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const multiDataInOne = terminals["multi data"]!["f1"] as InputTerminal; const multiDataInTwo = terminals["multi data"]!["f2"] as InputTerminal; multiDataInOne.connect(collectionOut); expect(multiDataInTwo.canAccept(collectionOut).canAccept).toBe(true); }); it("rejects connecting output to input of same step", () => { const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.canAccept(dataOut).canAccept).toBe(false); expect(dataIn.canAccept(dataOut).reason).toBe("Cannot connect output to input of same step."); }); it("rejects paired input on multi-data input", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const pairedOut = terminals["paired input"]!["output"] as OutputCollectionTerminal; expect(multiDataIn.canAccept(pairedOut).canAccept).toBe(false); expect(multiDataIn.canAccept(pairedOut).reason).toBe( "Cannot attach paired inputs to multiple data parameters, only lists may be treated this way.", ); }); it("rejects paired_or_unpaired input on multi-data input", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const pairedOut = terminals["paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; expect(multiDataIn.canAccept(pairedOut).canAccept).toBe(false); expect(multiDataIn.canAccept(pairedOut).reason).toBe( "Cannot attach paired inputs to multiple data parameters, only lists may be treated this way.", ); }); it("rejects list:paired input on multi-data input", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const pairedOut = terminals["list:paired input"]!["output"] as OutputCollectionTerminal; expect(multiDataIn.canAccept(pairedOut).canAccept).toBe(false); expect(multiDataIn.canAccept(pairedOut).reason).toBe( "Cannot attach paired inputs to multiple data parameters, only lists may be treated this way.", ); }); it("rejects list:paired_or_unpaired input on multi-data input", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const pairedOut = terminals["list:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; expect(multiDataIn.canAccept(pairedOut).canAccept).toBe(false); expect(multiDataIn.canAccept(pairedOut).reason).toBe( "Cannot attach paired inputs to multiple data parameters, only lists may be treated this way.", ); }); it("rejects collections on multi data inputs if non-collection already connected", () => { const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; const dataOut = terminals["data input"]!["output"] as OutputTerminal; const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; multiDataIn.connect(dataOut); expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(false); expect(multiDataIn.canAccept(collectionOut).reason).toBe( "Cannot attach collections to data parameters with individual data inputs already attached.", ); }); it("maps list:list over multi data input", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(true); multiDataIn.connect(collectionOut); expect(multiDataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); }); it("rejects attaching multiple collections to a single multi data input", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const otherCollectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal; multiDataIn.connect(collectionOut); expect(multiDataIn.canAccept(otherCollectionOut).canAccept).toBe(false); expect(multiDataIn.canAccept(otherCollectionOut).reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); }); it("rejects data -> collection connection", () => { const dataOut = terminals["data input"]!["output"] as OutputTerminal; const collectionInput = terminals["any collection"]!["input"] as InputCollectionTerminal; expect(collectionInput.canAccept(dataOut).canAccept).toBe(false); expect(collectionInput.canAccept(dataOut).reason).toBe("Cannot attach a data output to a collection input."); }); it("rejects optional data -> required data", () => { const optionalDataOut = terminals["optional data input"]!["output"] as OutputTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.canAccept(optionalDataOut).canAccept).toBe(false); expect(dataIn.canAccept(optionalDataOut).reason).toBe( "Cannot connect an optional output to a non-optional input", ); }); it("rejects parameter to data connection", () => { const dataIn = terminals["simple data"]!["input"] as InputTerminal; // # type system would reject this, but test runtime too const integerParam = terminals["integer parameter input"]!["output"] as any; expect(dataIn.canAccept(integerParam).canAccept).toBe(false); expect(dataIn.canAccept(integerParam).reason).toBe("Cannot connect workflow parameter to data input."); }); it("accepts integer parameter to integer parameter connection", () => { const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal; const integerOutputParam = terminals["integer parameter input"]!["output"] as OutputParameterTerminal; expect(integerInputParam.canAccept(integerOutputParam).canAccept).toBe(true); // regression test for https://github.com/galaxyproject/galaxy/issues/15417 expect(integerInputParam.canAccept(integerOutputParam).reason).toBe(null); }); it("rejects text to integer parameter connection", () => { const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal; const textOutputParam = terminals["text parameter input"]!["output"] as OutputParameterTerminal; expect(integerInputParam.canAccept(textOutputParam).canAccept).toBe(false); expect(integerInputParam.canAccept(textOutputParam).reason).toBe( "Cannot attach a text parameter to a integer input", ); }); it("rejects optional integer to required parameter connection", () => { const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal; const optionalIntegerOutputParam = terminals["optional integer parameter input"]![ "output" ] as OutputParameterTerminal; expect(integerInputParam.canAccept(optionalIntegerOutputParam).canAccept).toBe(false); expect(integerInputParam.canAccept(optionalIntegerOutputParam).reason).toBe( "Cannot attach an optional output to a required parameter", ); }); it("rejects data to parameter connection", () => { const dataOut = terminals["data input"]!["output"] as OutputTerminal; const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal; expect(integerInputParam.canAccept(dataOut).canAccept).toBe(false); expect(integerInputParam.canAccept(dataOut).reason).toBe("Cannot attach a data parameter to a integer input"); }); it("rejects increasing map over if output connected to data input", () => { const dataIn = terminals["simple data"]!["input"] as InputTerminal; const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const constrainingDataIn = terminals["simple data 2"]!["input"] as InputTerminal; const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; // connect simple data to simple data 2 constrainingDataIn.connect(dataOut); // now we can't connect a collection out to the data input of simple data expect(dataIn.canAccept(collectionOut).canAccept).toBe(false); expect(dataIn.canAccept(collectionOut).reason).toBe( "Can't map over this input with output collection type - an output of this tool is mapped over constraining this input. Disconnect output(s) and retry.", ); }); it("rejects increasing map over to list:list if data is mapped over a list input", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const dataInTwo = terminals["simple data 2"]!["input"] as InputTerminal; dataIn.connect(collectionOut); dataInTwo.connect(dataOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); // dataIn.disconnect(collectionOut); // this is weird and not particularly robust, if you save and reload this will most likely not be constrained // TODO: avoid this if possible ... expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.canAccept(listListOut).canAccept).toBe(false); expect(dataIn.canAccept(listListOut).reason).toBe( "Can't map over this input with output collection type - this step has outputs defined constraining the mapping of this tool. Disconnect outputs and retry.", ); }); it("rejects attaching non-collection outputs to mapped-over inputs", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const simpleDataOut = terminals["data input"]!["output"] as OutputTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const dataInTwo = terminals["simple data 2"]!["input"] as InputTerminal; dataIn.connect(collectionOut); dataInTwo.connect(dataOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); // dataIn.disconnect(collectionOut); // this is weird and not particularly robust, if you save and reload this will most likely not be constrained // TODO: avoid this if possible ... expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); expect(dataIn.localMapOver.isCollection).toBe(true); expect(dataIn.canAccept(simpleDataOut).canAccept).toBe(false); expect(dataIn.canAccept(simpleDataOut).reason).toBe( "Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping.", ); dataInTwo.disconnect(dataOut); // terminal isn't a reactive class in any way (worth a thought doing, but difficult!), the following happens when a new terminal is built // in useTerminal.ts const rebuiltDataIn = rebuildTerminal(dataIn); expect(rebuiltDataIn.localMapOver.isCollection).toBe(false); expect(rebuiltDataIn.canAccept(simpleDataOut).canAccept).toBe(true); }); it("accepts attaching dataset to non-mapped over input for a mapped over step", () => { const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal; const simpleDataOut = terminals["data input"]?.["output"] as OutputTerminal; const otherDataIn = terminals["simple data"]?.["input"] as InputTerminal; const multiSimpleInputOne = terminals["multiple simple data"]?.["input1"] as InputTerminal; const multiSimpleInputTwo = terminals["multiple simple data"]?.["queries_0|input2"] as InputTerminal; const multiSimpleOut = terminals["multiple simple data"]?.["out_file1"] as OutputTerminal; // map over simple input multiSimpleInputOne.connect(collectionOut); // constrain input by connecting an output to same step otherDataIn.connect(multiSimpleOut); // can connect a simple input to the non mapped-over input of the mapped-over step expect(multiSimpleInputTwo.canAccept(simpleDataOut).canAccept).toBe(true); // can also connect a connection of the same map over state expect(multiSimpleInputTwo.canAccept(collectionOut).canAccept).toBe(true); // cannot connect connection of deeper nesting const listListOut = terminals["list:list input"]?.["output"] as OutputCollectionTerminal; expect(multiSimpleInputTwo.canAccept(listListOut).canAccept).toBe(false); expect(multiSimpleInputTwo.canAccept(listListOut).reason).toBe( "Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry.", ); // disconnect, map over should be maintained multiSimpleInputOne.disconnect(collectionOut); expect(multiSimpleInputOne.localMapOver.isCollection).toBe(true); expect(multiSimpleInputOne.canAccept(simpleDataOut).canAccept).toBe(false); expect(multiSimpleInputOne.canAccept(simpleDataOut).reason).toBe( "Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping.", ); }); // TODO: test mapOver reset when constraint removed it("resets mapOver when constraint is lifted", () => { const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]?.["input"] as InputTerminal; const dataOut = terminals["simple data"]?.["out_file1"] as OutputTerminal; const dataInTwo = terminals["simple data 2"]?.["input"] as InputTerminal; dataIn.connect(collectionOut); dataInTwo.connect(dataOut); expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); // dataIn.disconnect(collectionOut); // this is weird and not particularly robust, if you save and reload this will most likely not be constrained // TODO: avoid this if possible ... expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); dataInTwo.disconnect(dataOut); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("maintains step map over state when disconnecting output", () => { const listListOut = terminals["list:list input"]?.["output"] as OutputCollectionTerminal; const filterFailedInput = terminals["filter_failed"]?.["input"] as InputCollectionTerminal; const filterFailedOutput = terminals["filter_failed"]?.["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]?.["input"] as InputTerminal; filterFailedInput.connect(listListOut); dataIn.connect(filterFailedOutput); expect(filterFailedInput.isMappedOver()).toBe(true); expect(stepStore.stepMapOver[filterFailedOutput.stepId]?.isCollection).toBe(true); dataIn.disconnect(filterFailedOutput); expect(stepStore.stepMapOver[filterFailedOutput.stepId]?.isCollection).toBe(true); }); it("accepts sample_sheet data -> data connection (maps like list)", () => { const collectionOut = terminals["sample_sheet input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "sample_sheet", isCollection: true, rank: 1 }); dataIn.disconnect(collectionOut); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts sample_sheet -> list connection (canMatch)", () => { const collectionOut = terminals["sample_sheet input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts sample_sheet:paired -> paired connection (maps over like list:paired)", () => { const collectionOut = terminals["sample_sheet:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "sample_sheet", isCollection: true, rank: 1 }); }); it("accepts sample_sheet:paired -> list:paired connection (canMatch)", () => { const collectionOut = terminals["sample_sheet:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired collection input"]!["input1"] as InputCollectionTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("accepts sample_sheet -> paired_or_unpaired connection", () => { const collectionOut = terminals["sample_sheet input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "sample_sheet", isCollection: true, rank: 1 }); }); it("accepts sample_sheet:paired -> paired_or_unpaired connection", () => { const collectionOut = terminals["sample_sheet:paired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "sample_sheet", isCollection: true, rank: 1 }); }); it("accepts sample_sheet:paired_or_unpaired -> list:paired_or_unpaired connection (canMatch)", () => { const collectionOut = terminals["sample_sheet:paired_or_unpaired input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["list:paired_or_unpaired collection input"]!["f1"] as InputTerminal; expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(dataIn.canAccept(collectionOut).canAccept).toBe(true); dataIn.connect(collectionOut); expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("rejects list -> sample_sheet connection (asymmetry)", () => { const listOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["sample_sheet collection input"]!["input1"] as InputCollectionTerminal; expect(collectionIn.canAccept(listOut).canAccept).toBe(false); }); it("rejects list:paired -> sample_sheet:paired connection (asymmetry)", () => { const listPairedOut = terminals["list:paired input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["sample_sheet:paired collection input"]!["input1"] as InputCollectionTerminal; expect(collectionIn.canAccept(listPairedOut).canAccept).toBe(false); }); it("accepts sample_sheet -> sample_sheet connection", () => { const sampleSheetOut = terminals["sample_sheet input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["sample_sheet collection input"]!["input1"] as InputCollectionTerminal; expect(collectionIn.canAccept(sampleSheetOut).canAccept).toBe(true); }); it("rejects connecting paired -> list", () => { const pairedOut = terminals["paired input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal; expect(collectionIn.canAccept(pairedOut).canAccept).toBe(false); expect(collectionIn.canAccept(pairedOut).reason).toBe("Incompatible collection type(s) for attachment."); }); it("rejects connecting list -> paired", () => { const pairedOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["paired collection input"]!["f1"] as InputCollectionTerminal; expect(collectionIn.canAccept(pairedOut).canAccept).toBe(false); expect(collectionIn.canAccept(pairedOut).reason).toBe("Incompatible collection type(s) for attachment."); }); it("accepts a collection input if other input has a map over collection type", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal; const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal; listOneIn.connect(listListOut); expect(listTwoIn.canAccept(collectionOut).canAccept).toBe(true); }); it("rejects mapping over collection input if outputs constrain input to incompatible collection type", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal; const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal; const mapOverOut = terminals["two list inputs"]!["out1"] as OutputTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; listOneIn.connect(listListOut); // two list inputs constrained to a list map over dataIn.connect(mapOverOut); listOneIn.disconnect(listListOut); // still constrained to list map over via output connection // can accept other input expect(listTwoIn.canAccept(collectionOut).canAccept).toBe(true); // but cannot accept on the constrained input expect(listOneIn.canAccept(collectionOut).canAccept).toBe(false); expect(listOneIn.canAccept(collectionOut).reason).toBe( "Can't map over this input with output collection type - this step has outputs defined constraining the mapping of this tool. Disconnect outputs and retry.", ); }); it("tracks transitive map over", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const listListListOut = terminals["list:list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; const simpleDataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; dataIn.connect(collectionOut); expect(dataIn.mapOver).toEqual({ collectionType: "list:list", isCollection: true, rank: 2 }); const otherDataIn = terminals["multi data"]!["f1"] as InputTerminal; expect(otherDataIn.canAccept(simpleDataOut).canAccept).toBe(true); otherDataIn.connect(simpleDataOut); expect(otherDataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); const otherDataInTwo = terminals["multi data"]!["f2"] as InputTerminal; expect(otherDataInTwo.canAccept(collectionOut).canAccept).toBe(true); expect(otherDataInTwo.canAccept(listListListOut).canAccept).toBe(false); expect(otherDataInTwo.canAccept(listListListOut).reason).toBe( "Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry.", ); }); it("tracks transitive map over through collection inputs", () => { const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal; expect(collectionIn.canAccept(collectionOut).canAccept).toBe(true); expect(collectionIn.isMappedOver()).toBe(false); collectionIn.connect(collectionOut); expect(collectionIn.isMappedOver()).toBe(true); expect(collectionIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 }); const intermediateOut = terminals["list collection input"]!["out_file1"] as OutputCollectionTerminal; const otherListIn = terminals["list collection input 2"]!["input1"] as InputCollectionTerminal; expect(otherListIn.canAccept(intermediateOut).canAccept).toBe(true); otherListIn.connect(intermediateOut); expect(otherListIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION); }); it("rejects connections to input collection constrained by output connection", () => { const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal; const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; const mappedOverListOut = terminals["list collection input"]!["out_file1"] as OutputCollectionTerminal; const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; // This constrains collectionIn to list because it's output is mapped over dataIn.connect(mappedOverListOut); collectionIn.connect(collectionOut); // Constraint survives disconnect collectionIn.disconnect(collectionOut); // Can't connect list:list because output acts like "list"" expect(collectionIn.canAccept(listListOut).canAccept).toBe(false); expect(collectionIn.canAccept(listListOut).reason).toBe( "Can't map over this input with output collection type - this step has outputs defined constraining the mapping of this tool. Disconnect outputs and retry.", ); }); it("rejects connections to input collection constrained by other input", () => { const listListListOut = terminals["list:list:list input"]!["output"] as OutputCollectionTerminal; const dataIn = terminals["simple data"]!["input"] as InputTerminal; const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal; const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal; const mapOverOut = terminals["two list inputs"]!["out1"] as OutputTerminal; const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal; // This constrains "two list inputs" to list:list because it's output is mapped over listOneIn.connect(listListOut); dataIn.connect(mapOverOut); // Can't connect list as output acts like "list:list" expect(listTwoIn.canAccept(listListListOut).canAccept).toBe(false); expect(listTwoIn.canAccept(listListListOut).reason).toBe( "Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry.", ); }); it("disconnects invalid input terminals", () => { const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const dataIn = terminals["simple data 2"]!["input"] as InputTerminal; dataIn.connect(dataOut); const oldStep = stepStore.getStep(dataOut.stepId) as Step; const stepOutput = oldStep.outputs[0] as DataOutput; stepOutput["extensions"] = ["ab1"]; dataOut.datatypes = ["ab1"]; dataOut.destroyInvalidConnections(); expect(connectionStore.connections).toEqual([]); // disconnects and connection will be invalid expect(dataIn.canAccept(dataOut).canAccept).toBe(false); expect(dataIn.canAccept(dataOut).reason).toBe( "Effective output data type(s) [ab1] do not appear to match input type(s) [txt].", ); }); it("disconnects invalid output terminals", () => { const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal; const dataIn = terminals["simple data 2"]!["input"] as InputTerminal; dataIn.connect(dataOut); dataIn.datatypes = ["ab1"]; dataIn.destroyInvalidConnections(); expect(connectionStore.connections).toEqual([]); // disconnects and connection will be invalid expect(dataIn.canAccept(dataOut).canAccept).toBe(false); expect(dataIn.canAccept(dataOut).reason).toBe( "Effective output data type(s) [tabular] do not appear to match input type(s) [ab1].", ); }); it("resolves collection type source", () => { const filterFailedInput = terminals["filter_failed"]?.["input"] as InputCollectionTerminal; const filterFailedOutput = terminals["filter_failed"]?.["output"] as OutputCollectionTerminal; expect(filterFailedOutput.collectionType).toBe(ANY_COLLECTION_TYPE_DESCRIPTION); const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal; filterFailedInput.connect(collectionOut); expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(new CollectionTypeDescription("list")); filterFailedInput.disconnect(collectionOut); const listPairedOutput = terminals["list:paired input"]?.["output"] as OutputCollectionTerminal; filterFailedInput.connect(listPairedOutput); expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual( new CollectionTypeDescription("list:paired"), ); }); }); describe("Input terminal", () => { let stores: ReturnType; let terminals: { [index: number]: { [index: string]: ReturnType } }; beforeEach(() => { setActivePinia(createPinia()); stores = useStores(); terminals = {}; Object.values(simpleSteps).map((step) => { stores.stepStore.addStep(step); terminals[step.id] = {}; const stepTerminals = terminals[step.id]!; step.inputs?.map((input) => { stepTerminals[input.name] = terminalFactory(step.id, input, testDatatypesMapper, stores); }); step.outputs?.map((output) => { stepTerminals[output.name] = terminalFactory(step.id, output, testDatatypesMapper, stores); }); }); }); it("has step", () => { expect(stores.stepStore.getStep(1)).toEqual(simpleSteps["1"]); }); it("infers correct state", () => { const firstInputTerminal = terminals[1]!["input"] as InputTerminal; expect(firstInputTerminal).toBeInstanceOf(InputTerminal); const dataInputOutputTerminal = terminals[0]!["output"] as OutputTerminal; expect(dataInputOutputTerminal).toBeInstanceOf(OutputTerminal); expect(firstInputTerminal.connections.length).toBe(1); expect(firstInputTerminal.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION); expect(firstInputTerminal.isMappedOver()).toBe(false); expect(firstInputTerminal.hasConnectedMappedInputTerminals()).toBe(false); expect(firstInputTerminal.hasMappedOverInputTerminals()).toBe(false); expect(firstInputTerminal.hasConnectedOutputTerminals()).toBe(false); const canAccept = firstInputTerminal.canAccept(dataInputOutputTerminal); expect(canAccept.canAccept).toBe(false); expect(canAccept.reason).toBe( "Input already filled with another connection, delete it before connecting another output.", ); // bypasses _inputFilled check expect(firstInputTerminal.attachable(dataInputOutputTerminal).canAccept).toBe(true); expect(firstInputTerminal.connected()).toBe(true); expect(firstInputTerminal._collectionAttached()).toBe(false); expect(firstInputTerminal._producesAcceptableDatatype(dataInputOutputTerminal).canAccept).toBe(true); }); it("can accept new connection", () => { const firstInputTerminal = terminals[1]!["input"] as InputTerminal; const dataInputOutputTerminal = terminals[0]!["output"] as OutputTerminal; const connection = firstInputTerminal.connections[0]!; expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false); expect(dataInputOutputTerminal.validInputTerminals().length).toBe(0); firstInputTerminal.disconnect(connection); expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(true); expect(dataInputOutputTerminal.validInputTerminals().length).toBe(1); stores.connectionStore.addConnection(connection); expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false); }); it("will maintain invalid connections", () => { const connection = stores.connectionStore.connections[0]!; connection.output.name = "I don't exist"; const firstInputTerminal = terminals[1]?.["input"] as InputTerminal; const invalidTerminals = firstInputTerminal.getConnectedTerminals(); expect(invalidTerminals.length).toBe(1); expect(invalidTerminals[0]).toBeInstanceOf(InvalidOutputTerminal); }); }); describe("producesAcceptableDatatype", () => { it("accepts everything if datatypes includes input", () => { expect(producesAcceptableDatatype(testDatatypesMapper, ["input"], ["whatever"]).canAccept).toBe(true); }); it("rejects connections for unknown output datatypes", () => { expect(producesAcceptableDatatype(testDatatypesMapper, ["txt"], ["i am not an extension"]).canAccept).toBe( false, ); expect(producesAcceptableDatatype(testDatatypesMapper, ["txt"], ["i am not an extension"]).reason).toBe( "Effective output data type(s) [i am not an extension] unknown. This tool cannot be run on this Galaxy Server at this moment, please contact the Administrator.", ); }); it("rejects incompatible datatypes", () => { expect(producesAcceptableDatatype(testDatatypesMapper, ["txt"], ["ab1"]).canAccept).toBe(false); expect(producesAcceptableDatatype(testDatatypesMapper, ["txt"], ["ab1"]).reason).toBe( "Effective output data type(s) [ab1] do not appear to match input type(s) [txt].", ); }); });