Workflow Editor Terminal Tests: Comprehensive Research Report
Overview
client/src/components/Workflow/Editor/modules/terminals.test.ts is a unit test suite for the Galaxy workflow editor’s terminal connection logic. Terminals represent the input/output ports on workflow steps — the connectable endpoints users drag wires between. The test file validates the rules governing which outputs can connect to which inputs, including collection type mapping (“map over”), datatype compatibility, and parameter type matching.
The file is ~928 lines, organized into 4 describe blocks containing ~50 individual test cases. It exercises the core terminalFactory function and 6 of the 8 concrete terminal classes it can produce: InputTerminal, InputCollectionTerminal, InputParameterTerminal, OutputTerminal, OutputCollectionTerminal, and OutputParameterTerminal. (InvalidInputTerminal and InvalidOutputTerminal are not directly constructed by the factory test, though InvalidOutputTerminal is exercised indirectly via corrupted connections.)
File Locations
| File | Purpose |
|---|---|
client/src/components/Workflow/Editor/modules/terminals.test.ts | Test file under analysis |
client/src/components/Workflow/Editor/modules/terminals.ts | Production code (1038 lines) |
client/src/components/Workflow/Editor/modules/collectionTypeDescription.ts | Collection type algebra (canMatch, canMapOver, effectiveMapOver, etc.) |
client/src/components/Workflow/Editor/test-data/parameter_steps.json | ”Advanced” fixture: 29 workflow steps (ids 0-28) |
client/src/components/Workflow/Editor/test-data/simple_steps.json | ”Simple” fixture: 2 steps with a pre-existing connection |
client/src/components/Datatypes/test_fixtures.ts | Shared testDatatypesMapper from real Galaxy datatype JSON |
client/src/components/Datatypes/model.ts | DatatypesMapperModel class for subtype checking |
client/src/stores/workflowStepStore.ts | Pinia store for step data, mapOver state |
client/src/stores/workflowConnectionStore.ts | Pinia store for connections between steps |
client/src/composables/workflowStores.ts | Composable bundling all workflow stores |
client/vitest.config.mts | Vitest configuration |
client/tests/vitest/setup.ts | Global test setup (Pinia mocks, console fail-on-error) |
Test Structure and Organization
Describe Block 1: terminalFactory (lines 108-150)
Setup: beforeEach creates a fresh Pinia, calls setupAdvanced() to build terminals from all 29 advanced steps.
Tests (2):
- “constructs correct class instances” — Verifies
terminalFactoryreturns the right subclass for each step’s inputs/outputs. Covers 6 concrete classes:OutputTerminal,InputTerminal,OutputCollectionTerminal(including filter_failed’scollection_type_sourcevariant),InputCollectionTerminal,OutputParameterTerminal, andInputParameterTerminal. Does not exerciseInvalidInputTerminalorInvalidOutputTerminalconstruction. - “throws error on invalid terminalSource” — Confirms passing
{}throws an error.
Describe Block 2: canAccept (lines 152-840)
The largest block (~690 lines, ~42 test cases). This is the heart of the test suite.
Setup: beforeEach creates a fresh Pinia, builds terminals, AND adds all advanced steps to the step store. This is critical because canAccept and connect/disconnect need actual step state to track connections, map-over, and constraints.
Test categories within this block:
Basic data connections (lines 166-204)
- Simple data output -> data input (connect, reject when filled, disconnect)
- Collection (list) output -> data input (map over set/cleared)
- Paired collection -> data input (map over set/cleared)
Mapped-over data connections (lines 205-211)
- Second input on same step accepts a mapped-over output after first input already mapped
Collection type variations (lines 212-266)
list:list-> data (rank 2 map over)paired_or_unpaired-> data (rank 1 map over)list:paired_or_unpaired-> data (rank 2 map over)list:paired_or_unpaired->list:paired_or_unpairedcollection input (direct match)paired_or_unpaired->paired_or_unpairedcollection input (direct match, no map over)
Collection-to-collection connections (lines 267-386)
list:paired->pairedcollection input (maps over as list)list:paired->paired_or_unpairedcollection input (maps over as list)list->paired_or_unpairedcollection input (maps over as list)list:list:paired->paired_or_unpairedcollection input (maps over as list:list)list:list:paired->list:paired_or_unpairedcollection input (maps over as list)list:list->paired_or_unpairedcollection input (maps over as list:list)list:list->list:paired_or_unpairedcollection input (maps over as list)paired->paired_or_unpaired(direct match, no map over)paired->paired(direct match)list->list(direct match)list:paired->list:paired(direct match)- Rejection:
paired:paired->list:paired(incompatible) - Rejection:
paired:paired->list:paired_or_unpaired(incompatible)
Rejection of paired_or_unpaired outputs to strict paired inputs (lines 387-410)
paired_or_unpaired->pairedrejected with helpful message about “Split Paired and Unpaired” toollist:paired_or_unpaired->pairedrejected similarlylist:paired_or_unpaired->listrejected
Multi-data input behavior (lines 411-494)
- List collection treated as direct match (no map over on multi-data)
- Multiple simple data outputs connect to same multi-data input (tracks
input_connectionsarray) - Removing a connected step cleans up
input_connections - Separate
list:listinputs on separate multi-data inputs of same tool - Rejection: paired on multi-data
- Rejection: paired_or_unpaired on multi-data
- Rejection: list:paired on multi-data
- Rejection: list:paired_or_unpaired on multi-data
- Rejection: collection on multi-data after non-collection already connected
Map-over with list:list on multi-data (lines 505-521)
list:listmaps over multi-data as list- Rejects attaching second collection to same multi-data input
Cross-type rejections (lines 522-573)
- data -> collection input rejected
- optional data -> required data rejected
- parameter -> data rejected
- integer parameter -> integer parameter accepted (regression test for #15417)
- text parameter -> integer parameter rejected
- optional integer parameter -> required integer parameter rejected
- data -> integer parameter rejected
- Same-step connection rejected
Map-over constraint propagation (lines 574-662)
- Increasing map over rejected if output already connected to data input
- Increasing map over from list to list:list rejected if constrained by output
- Non-collection output rejected on mapped-over input; rebuild terminal clears stale mapOver
- Dataset accepted on non-mapped-over input of mapped-over step
- Deeper nesting rejected on second input of mapped-over step
Map-over reset and maintenance (lines 663-731)
- Map over resets when output constraint is lifted (disconnect)
- Step map over state maintained when disconnecting output (filter_failed test)
- Collection input acceptance when other input has map-over
- Rejection of mapping collection input when outputs constrain to incompatible type
Transitive map-over (lines 732-796)
- Tracks map over through intermediate steps (list:list -> simple data -> multi data)
- Tracks map over through collection inputs (list:list -> list collection -> another list collection)
- Rejects connections to input collection constrained by output connection
- Rejects connections to input collection constrained by other input
Connection validation (lines 797-839)
destroyInvalidConnectionson output terminal (changes output datatype, verifies disconnection)destroyInvalidConnectionson input terminal (changes input datatype, verifies disconnection)resolves collection type source— filter_failed output collection type resolves from connected input
Describe Block 3: Input terminal (lines 842-907)
Setup: Uses simpleSteps fixture (2 steps with pre-existing connection via input_connections). Adds steps to store to materialize connections.
Tests (4):
- “has step” — Basic sanity: step store contains the step
- “infers correct state” — Comprehensive check of terminal properties:
connections.length,mapOver,isMappedOver(),hasConnectedMappedInputTerminals(),hasMappedOverInputTerminals(),hasConnectedOutputTerminals(),canAccept,attachable,connected(),_collectionAttached(),_producesAcceptableDatatype() - “can accept new connection” — Disconnect existing, verify canAccept flips, reconnect, verify flips back. Also tests
validInputTerminals()on the output terminal. - “will maintain invalid connections” — Corrupts a connection’s output name, verifies
getConnectedTerminals()returns anInvalidOutputTerminal
Describe Block 4: producesAcceptableDatatype (lines 909-927)
Tests the standalone exported function directly.
Tests (3):
- Input type “input” accepts everything
- Unknown output datatype produces specific error message
- Incompatible datatypes produce specific error message
Mocking and Fixture Strategies
Pinia Store Isolation
Every describe block uses beforeEach with setActivePinia(createPinia()) to ensure complete store isolation between tests. No test leaks state to another.
Real Stores, No Mocking
The tests use real Pinia stores — useWorkflowStepStore, useConnectionStore, etc. — with a scoped workflow ID "mock-workflow". This means the tests exercise actual store logic (adding connections, tracking mapOver state, removing steps, etc.) rather than mocking it. This is an integration-test-like approach within a unit test framework.
useStores Helper (lines 42-61)
A local helper mirrors the production useWorkflowStores composable but without Vue’s inject/provide (since there’s no component tree). It instantiates all 7 stores with the same "mock-workflow" ID.
setupAdvanced Helper (lines 74-92)
Iterates all steps in parameter_steps.json, builds terminals for every input/output using terminalFactory. Steps are keyed by their label for readable test access like terminals["list input"]["output"].
Important distinction: In the terminalFactory block, steps are NOT added to the store (only terminals are built). In the canAccept block, steps ARE added to the store, which is necessary for connection tracking.
rebuildTerminal Helper (lines 94-106)
Re-creates a terminal from the current store state. This simulates what happens in the production useTerminal.ts composable when a terminal is rebuilt after state changes. Used to test that stale mapOver state is cleared when constraints change.
Console Debug Suppression (lines 66-72)
A vi.spyOn(console, "debug") mock suppresses the NO_COLLECTION_TYPE_INFORMATION_MESSAGE debug log that fires for steps without collection_type or collection_type_source. Other debug messages pass through.
testDatatypesMapper
From client/src/components/Datatypes/test_fixtures.ts. Loads real Galaxy datatype JSON (datatypes.json and datatypes.mapping.json from tests/test-data/json/) and constructs a real DatatypesMapperModel. This ensures isSubType checks work against the actual type hierarchy.
Test Data Fixtures
parameter_steps.json (advanced): 29 steps (ids 0-28) representing a comprehensive set of workflow inputs and tools:
- Data inputs: regular (
id:0), optional (id:9) - Collection inputs: list (
id:2), list:list (id:3), paired (id:4), list:paired (id:18), list:list:list (id:16), list:list:paired (id:26), paired:paired (id:28), paired_or_unpaired (id:21), list:paired_or_unpaired (id:22) - Parameter inputs: integer (
id:5), text (id:10), optional text (id:19), optional integer (id:20) - Tools: simple Cut tool (
id:1,id:6), multi_data_param (id:15), Apply Rules / any collection (id:7), Extract dataset / list or paired (id:8), count_list / list collection (id:11,id:13), two_list_inputs (id:12), concatenate / multiple simple data (id:14), filter_failed with collection_type_source (id:17), paired_or_unpaired collection input (id:23), list:paired_or_unpaired collection input (id:24), paired collection input (id:25), list:paired collection input (id:27)
simple_steps.json (simple): 2 steps — a data input (id:0) connected to a Cut tool (id:1) via input_connections. This fixture tests the pre-existing connection state path.
Key Production Code Under Test
Class Hierarchy (terminals.ts)
EventEmitter (from "events"; browser-polyfilled in Vite)
|
Terminal (base: stores, stepId, name, mapOver, connect/disconnect)
|-- BaseInputTerminal (datatypes, optional, multiple, canAccept, _inputFilled, etc.)
| |-- InvalidInputTerminal (placeholder for broken inputs; always rejects)
| |-- InputTerminal (dataset inputs, multiple=true support)
| |-- InputParameterTerminal (parameter inputs: text, integer, etc.)
| |-- InputCollectionTerminal (dataset_collection inputs)
|
|-- BaseOutputTerminal (datatypes, optional, validInputTerminals, destroyInvalidConnections)
|-- OutputTerminal (plain dataset outputs; empty subclass)
|-- OutputCollectionTerminal (collection outputs with collection_type/collection_type_source)
|-- OutputParameterTerminal (parameter outputs: text, integer, etc.)
|-- InvalidOutputTerminal (placeholder for broken outputs; always rejects)
terminalFactory Function
Dispatches on terminalSource shape:
- Has
input_type-> input terminal (dataset / dataset_collection / parameter) - Has
name+extensions-> output terminal - Has
name+collection-> output collection terminal - Has
name+parameter-> output parameter terminal - Has
valid: false-> invalid terminal
producesAcceptableDatatype Function
Standalone function checking datatype compatibility via the DatatypesMapperModel.isSubType method. Handles special “input” and “sniff” wildcards.
ConnectionAcceptable Class
Simple value object with {canAccept: boolean, reason: string | null} returned by all acceptance checks.
Coverage Analysis
Well-Covered Areas
-
terminalFactorydispatch logic: All 7 terminal classes are tested for correct construction. Invalid input throws. -
canAccept/attachableonInputTerminal: Extensively tested across:- Simple data connections
- Collection -> data mapping (all collection type combinations)
- Multi-data (multiple=true) inputs
- Optional/required checking
- Same-step rejection
- Map-over constraint propagation and rejection
-
canAccept/attachableonInputCollectionTerminal: Extensively tested:- Direct collection matches (list->list, paired->paired, etc.)
- Map-over through collections (list:list->list, etc.)
- Incompatible collection types (paired->list, list->paired)
paired_or_unpairedmatching semantics- Constraint propagation via outputs
-
canAccept/attachableonInputParameterTerminal: Tested for:- Type matching (integer -> integer)
- Type rejection (text -> integer)
- Optional -> required rejection
- Data -> parameter rejection
-
connect/disconnectstate management: Many tests verify that connecting and disconnecting properly updates mapOver, localMapOver, and input_connections. -
destroyInvalidConnections: Tested from both input and output terminal sides. -
getConnectedTerminalswith invalid state: Tested via corrupted connection name. -
validInputTerminals: Tested (returns all inputs across all steps that can accept this output). -
producesAcceptableDatatype: Standalone tests for wildcard, unknown, and incompatible types. -
OutputCollectionTerminal.getCollectionTypeFromInput: Tested indirectly viaresolves collection type source(filter_failed withcollection_type_source).
Gaps and Uncovered Areas
-
InvalidInputTerminal.attachable: Not directly tested (always returns rejection). Covered only throughgetConnectedTerminalsreturningInvalidOutputTerminal. -
InvalidOutputTerminal.canAcceptandvalidInputTerminals: Not tested. -
InputParameterTerminal.effectiveType: Theselect->textanddata_column->integermappings are not tested. Onlyintegerandtexttypes appear in fixtures. -
OutputParameterTerminal.multiplerejection: TheInputParameterTerminal.attachablecheck for!this.multiple && other.multiple(single value input rejecting multi-value output) is not tested. No fixture hasmultiple: trueon a parameter output. -
BaseOutputTerminal.optionalfromwhen: The conditionalthis.optional = attr.optional || Boolean(this.stores.stepStore.getStep(this.stepId)?.when)— in the test fixtures, onlyfilter_failedhas awhenfield (set tonull); no fixture step has a truthywhenvalue. -
getInvalidConnectedTerminals: Tested indirectly throughdestroyInvalidConnectionsbut not directly asserted. -
BaseOutputTerminal.getConnectedTerminalswithChangeDatatypeActionpost_job_actions: The production code checks forChangeDatatypeActionto override output extensions. No test exercises this path. -
Terminal.setMapOverinternal branching: The multi-input tracking instepInputMapOveris exercised indirectly but not unit-tested in isolation. -
resetMappingcascading through output steps: The complex logic inBaseInputTerminal.resetMappingthat iterates connected output steps, drops their mapping, and re-establishes via inputs is exercised implicitly but the cascading behavior could use more targeted coverage. -
_collectionAttachedwithextensions.indexOf("input") > 0: This specific check (output extension containing “input” at non-zero index) is not tested. -
No tests for
collectionTypeDescription.ts: There is no separate test file forCollectionTypeDescription,NULL_COLLECTION_TYPE_DESCRIPTION, orANY_COLLECTION_TYPE_DESCRIPTION. Their behavior is tested only indirectly through terminal tests.
Patterns and Idioms
Terminal Lookup by Label
Tests use terminals["step label"]["terminal name"] for readable access. The setupAdvanced function keys terminals by step labels like "data input", "simple data", "list input", etc.
Type Assertions for Terminal Subclasses
Tests consistently cast terminals to specific subclasses:
const dataOut = terminals["data input"]!["output"] as OutputTerminal;
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
This is necessary because terminalFactory returns a union type.
Connect-Assert-Disconnect-Assert Pattern
Most canAccept tests follow this pattern:
- Verify initial
canAcceptis true connectthe terminals- Verify
canAcceptbecomes false (with expected reason) disconnectthe terminals- Verify
canAcceptreturns to true - Verify
mapOverreturns toNULL_COLLECTION_TYPE_DESCRIPTION
MapOver Assertions
Map-over state is verified via deep equality:
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
And null state via identity:
expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION);
Note the use of toBe (reference equality) for null and toEqual (deep equality) for collection types.
Reason String Verification
Every rejection test verifies the specific reason string. This is important because these messages are displayed to users in the workflow editor UI. Examples:
"Cannot connect output to input of same step.""Input already filled with another connection, delete it before connecting another output.""Cannot attach paired inputs to multiple data parameters, only lists may be treated this way."
Non-Reactive Terminals with rebuildTerminal
A comment at line 628-630 notes that terminal objects are not reactive. When state changes externally (e.g., disconnecting a constraining connection), the terminal must be rebuilt to pick up new state. The rebuildTerminal helper simulates what useTerminal.ts does in production.
Undo/Redo Integration
connect() and disconnect() on terminals go through undoRedoStore.action(), meaning the tests exercise the undo/redo action wrapping. However, no test actually invokes undo/redo.
Vitest Configuration
- Environment:
happy-dom - Globals:
false(explicit imports:describe,it,expect,vi,beforeEach) - Pool:
threads - Setup:
tests/vitest/setup.ts— Pinia-compatible (no Pinia mock; tests create their own viacreatePinia()),vitest-fail-on-consoleenabled for errors and warnings - Path aliases:
@->client/src,@tests->client/tests
Relationship to Other Test Files
This is the only test file in client/src/components/Workflow/Editor/modules/. There is no test for collectionTypeDescription.ts despite its complex matching logic. Other test files in the Editor directory (actions.test.ts, Node.test.ts, Index.test.ts, etc.) test higher-level components and may use the same parameter_steps.json fixture.
Summary
This test file is a thorough unit/integration test of Galaxy’s workflow connection logic. It covers the complex rules governing which workflow step outputs can connect to which inputs, with particular depth in collection type mapping (“map over”) scenarios. The tests use real Pinia stores rather than mocks, giving high confidence that the store interactions work correctly. The main gap is coverage of edge cases in parameter type mapping, ChangeDatatypeAction post-job-action handling, and the absence of standalone tests for CollectionTypeDescription.