diff --git a/client/galaxy/scripts/mvc/workflow/workflow-connector.js b/client/galaxy/scripts/mvc/workflow/workflow-connector.js
index c2863b937932..5eb1a6fd6188 100644
--- a/client/galaxy/scripts/mvc/workflow/workflow-connector.js
+++ b/client/galaxy/scripts/mvc/workflow/workflow-connector.js
@@ -35,6 +35,9 @@ $.extend(Connector.prototype, {
redraw: function() {
const handle1 = this.handle1;
const handle2 = this.handle2;
+ const startRibbon = handle1 && handle1.isMappedOver();
+ const endRibbon = handle2 && handle2.isMappedOver();
+ const canvasClass = `${startRibbon ? "start-ribbon" : ""} ${endRibbon ? "end-ribbon" : ""}`;
var canvas_container = $("#canvas-container");
if (!this.canvas) {
this.canvas = document.createElement("canvas");
@@ -71,6 +74,7 @@ $.extend(Connector.prototype, {
this.canvas.style.top = `${canvas_top}px`;
this.canvas.setAttribute("width", canvas_width);
this.canvas.setAttribute("height", canvas_height);
+ this.canvas.setAttribute("class", canvasClass);
// Adjust points to be relative to the canvas
start_x -= canvas_left;
start_y -= canvas_top;
@@ -84,13 +88,13 @@ $.extend(Connector.prototype, {
var start_offsets = null;
var end_offsets = null;
var num_offsets = 1;
- if (handle1 && handle1.isMappedOver()) {
+ if (startRibbon) {
var start_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
var start_offsets = [0];
}
- if (handle2 && handle2.isMappedOver()) {
+ if (endRibbon) {
var end_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
diff --git a/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js b/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js
index eaba7e851315..fc67953b47d6 100644
--- a/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js
+++ b/client/galaxy/scripts/mvc/workflow/workflow-view-terminals.js
@@ -72,7 +72,7 @@ var BaseInputTerminalView = TerminalView.extend({
const node = options.node;
const input = options.input;
const name = input.name;
- const id = `node-${node.id}-input-${name}`;
+ const id = `node-${node.cid}-input-${name}`;
const terminal = this.terminalForInput(input);
if (!terminal.multiple) {
this.setupMappingView(terminal);
@@ -179,7 +179,7 @@ var BaseOutputTerminalView = TerminalView.extend({
const node = options.node;
const output = options.output;
const name = output.name;
- const id = `node-${node.id}-output-${name}`;
+ const id = `node-${node.cid}-output-${name}`;
const terminal = this.terminalForOutput(output);
this.setupMappingView(terminal);
this.el.terminal = terminal;
diff --git a/templates/base/base_panels.mako b/templates/base/base_panels.mako
index c55af74cb2e1..9bc101a334e0 100644
--- a/templates/base/base_panels.mako
+++ b/templates/base/base_panels.mako
@@ -223,8 +223,8 @@
${self.left_panel()}
%endif
@@ -235,8 +235,8 @@
${self.right_panel()}
%endif
diff --git a/templates/webapps/galaxy/galaxy.panels.mako b/templates/webapps/galaxy/galaxy.panels.mako
index 856cbaae62d1..3936dfe1aa90 100644
--- a/templates/webapps/galaxy/galaxy.panels.mako
+++ b/templates/webapps/galaxy/galaxy.panels.mako
@@ -209,8 +209,8 @@
%endif
@@ -237,8 +237,8 @@
%endif
diff --git a/test/api/test_workflows.py b/test/api/test_workflows.py
index e06ed6317a8f..726b84145b09 100644
--- a/test/api/test_workflows.py
+++ b/test/api/test_workflows.py
@@ -18,54 +18,15 @@
wait_on,
WorkflowPopulator
)
+from base.workflow_fixtures import ( # noqa: I100
+ WORKFLOW_NESTED_SIMPLE,
+ WORKFLOW_WITH_OUTPUT_COLLECTION,
+ WORKFLOW_WITH_OUTPUT_COLLECTION_MAPPING,
+)
from galaxy.exceptions import error_codes # noqa: I201
from galaxy.tools.verify.test_data import TestDataResolver
-SIMPLE_NESTED_WORKFLOW_YAML = """
-class: GalaxyWorkflow
-inputs:
- - id: outer_input
-outputs:
- - id: outer_output
- source: second_cat#out_file1
-steps:
- - tool_id: cat1
- label: first_cat
- state:
- input1:
- $link: outer_input
- - run:
- class: GalaxyWorkflow
- inputs:
- - id: inner_input
- outputs:
- - id: workflow_output
- source: random_lines#out_file1
- steps:
- - tool_id: random_lines1
- label: random_lines
- state:
- num_lines: 1
- input:
- $link: inner_input
- seed_source:
- seed_source_selector: set_seed
- seed: asdf
- label: nested_workflow
- connect:
- inner_input: first_cat#out_file1
- - tool_id: cat1
- label: second_cat
- state:
- input1:
- $link: nested_workflow#workflow_output
- queries:
- - input2:
- $link: nested_workflow#workflow_output
-"""
-
-
class BaseWorkflowsApiTestCase(api.ApiTestCase):
# TODO: Find a new file for this class.
@@ -474,7 +435,7 @@ def get_subworkflow_content_id(workflow_id):
subworkflow_step = next(s for s in steps.values() if s["type"] == "subworkflow")
return subworkflow_step['content_id']
- workflow_id = self._upload_yaml_workflow(SIMPLE_NESTED_WORKFLOW_YAML, publish=True)
+ workflow_id = self._upload_yaml_workflow(WORKFLOW_NESTED_SIMPLE, publish=True)
subworkflow_content_id = get_subworkflow_content_id(workflow_id)
with self._different_user():
other_import_response = self.__import_workflow(workflow_id)
@@ -633,21 +594,7 @@ def __run_cat_workflow(self, inputs_by):
@skip_without_tool("collection_creates_pair")
def test_workflow_run_output_collections(self):
- workflow_id = self._upload_yaml_workflow("""
-class: GalaxyWorkflow
-steps:
- - label: text_input
- type: input
- - label: split_up
- tool_id: collection_creates_pair
- state:
- input1:
- $link: text_input
- - tool_id: collection_paired_test
- state:
- f1:
- $link: split_up#paired_output
-""")
+ workflow_id = self._upload_yaml_workflow(WORKFLOW_WITH_OUTPUT_COLLECTION)
with self.dataset_populator.test_history() as history_id:
hda1 = self.dataset_populator.new_dataset(history_id, content="a\nb\nc\nd\n")
inputs = {
@@ -779,23 +726,7 @@ def test_workflow_resume_with_mapped_over_input(self):
@skip_without_tool("collection_creates_pair")
def test_workflow_run_output_collection_mapping(self):
- workflow_id = self._upload_yaml_workflow("""
-class: GalaxyWorkflow
-steps:
- - type: input_collection
- - tool_id: collection_creates_pair
- state:
- input1:
- $link: 0
- - tool_id: collection_paired_test
- state:
- f1:
- $link: 1#paired_output
- - tool_id: cat_list
- state:
- input1:
- $link: 2#out1
-""")
+ workflow_id = self._upload_yaml_workflow(WORKFLOW_WITH_OUTPUT_COLLECTION_MAPPING)
with self.dataset_populator.test_history() as history_id:
hdca1 = self.dataset_collection_populator.create_list_in_history(history_id, contents=["a\nb\nc\nd\n", "e\nf\ng\nh\n"]).json()
self.dataset_populator.wait_for_history(history_id, assert_ok=True)
@@ -990,7 +921,7 @@ def test_run_subworkflow_simple(self):
outer_input:
value: 1.bed
type: File
-""" % SIMPLE_NESTED_WORKFLOW_YAML
+""" % WORKFLOW_NESTED_SIMPLE
self._run_jobs(workflow_run_description, history_id=history_id)
content = self.dataset_populator.get_history_dataset_content(history_id)
@@ -1266,7 +1197,7 @@ def test_workflow_run_input_mapping_with_subworkflows(self):
- identifier: el2
value: 1.fastq
type: File
-""" % SIMPLE_NESTED_WORKFLOW_YAML, history_id=history_id)
+""" % WORKFLOW_NESTED_SIMPLE, history_id=history_id)
workflow_id = summary.workflow_id
invocation_id = summary.invocation_id
invocation_response = self._get("workflows/%s/invocations/%s" % (workflow_id, invocation_id))
@@ -1856,7 +1787,7 @@ def test_nested_workflow_rerun_with_use_cached_job(self):
outer_input:
value: 1.bed
type: File
-""" % SIMPLE_NESTED_WORKFLOW_YAML
+""" % WORKFLOW_NESTED_SIMPLE
run_jobs_summary = self._run_jobs(workflow_run_description, history_id=history_id_one)
self.dataset_populator.wait_for_history(history_id_one, assert_ok=True)
workflow_request = run_jobs_summary.workflow_request
diff --git a/test/base/workflow_fixtures.py b/test/base/workflow_fixtures.py
new file mode 100644
index 000000000000..2d3491478b13
--- /dev/null
+++ b/test/base/workflow_fixtures.py
@@ -0,0 +1,134 @@
+
+WORKFLOW_SIMPLE_CAT_TWICE = """
+class: GalaxyWorkflow
+inputs:
+ - id: input1
+steps:
+ - tool_id: cat
+ label: first_cat
+ state:
+ input1:
+ $link: input1
+ queries:
+ - input2:
+ $link: input1
+"""
+
+
+WORKFLOW_WITH_OLD_TOOL_VERSION = """
+class: GalaxyWorkflow
+inputs:
+ - id: input1
+steps:
+ - tool_id: multiple_versions
+ tool_version: "0.0.1"
+ state:
+ inttest: 8
+"""
+
+
+WORKFLOW_WITH_INVALID_STATE = """
+class: GalaxyWorkflow
+inputs:
+ - id: input1
+steps:
+ - tool_id: multiple_versions
+ tool_version: "0.0.1"
+ state:
+ inttest: "moocow"
+"""
+
+
+WORKFLOW_WITH_OUTPUT_COLLECTION = """
+class: GalaxyWorkflow
+steps:
+ - label: text_input
+ type: input
+ - label: split_up
+ tool_id: collection_creates_pair
+ state:
+ input1:
+ $link: text_input
+ - tool_id: collection_paired_test
+ state:
+ f1:
+ $link: split_up#paired_output
+"""
+
+
+WORKFLOW_SIMPLE_MAPPING = """
+class: GalaxyWorkflow
+inputs:
+ - id: input1
+ type: data_collection_input
+ collection_type: list
+steps:
+ - tool_id: cat
+ label: cat
+ state:
+ input1:
+ $link: input1
+"""
+
+
+WORKFLOW_WITH_OUTPUT_COLLECTION_MAPPING = """
+class: GalaxyWorkflow
+steps:
+ - type: input_collection
+ - tool_id: collection_creates_pair
+ state:
+ input1:
+ $link: 0
+ - tool_id: collection_paired_test
+ state:
+ f1:
+ $link: 1#paired_output
+ - tool_id: cat_list
+ state:
+ input1:
+ $link: 2#out1
+"""
+
+
+WORKFLOW_NESTED_SIMPLE = """
+class: GalaxyWorkflow
+inputs:
+ - id: outer_input
+outputs:
+ - id: outer_output
+ source: second_cat#out_file1
+steps:
+ - tool_id: cat1
+ label: first_cat
+ state:
+ input1:
+ $link: outer_input
+ - run:
+ class: GalaxyWorkflow
+ inputs:
+ - id: inner_input
+ outputs:
+ - id: workflow_output
+ source: random_lines#out_file1
+ steps:
+ - tool_id: random_lines1
+ label: random_lines
+ state:
+ num_lines: 1
+ input:
+ $link: inner_input
+ seed_source:
+ seed_source_selector: set_seed
+ seed: asdf
+ label: nested_workflow
+ connect:
+ inner_input: first_cat#out_file1
+ - tool_id: cat1
+ label: second_cat
+ state:
+ input1:
+ $link: nested_workflow#workflow_output
+ queries:
+ - input2:
+ $link: nested_workflow#workflow_output
+"""
diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml
index 904d927621a0..af2de65a3a8a 100644
--- a/test/galaxy_selenium/navigation.yml
+++ b/test/galaxy_selenium/navigation.yml
@@ -11,6 +11,10 @@ _: # global stuff
selectors:
editable_text: '.editable-text'
tooltip_balloon: '.tooltip'
+ left_panel_drag: '#left-panel-drag'
+ left_panel_collapse: '#left-panel-collapse'
+ right_panel_drag: '#right-panel-drag'
+ right_panel_collapse: '#right-panel-collapse'
messages:
selectors:
@@ -233,6 +237,8 @@ workflow_editor:
output_terminal: "${_} [output-name='${name}']"
input_terminal: "${_} [input-name='${name}']"
+ input_mapping_icon: "${_} [name='${name}'] .fa-folder-o"
+
selectors:
canvas_body: '#workflow-canvas-body'
canvas_title: '#workflow-canvas-title'
@@ -247,6 +253,8 @@ workflow_editor:
connector_for: "canvas[handle1-id='${source_id}'][handle2-id='${sink_id}']"
+ connector_destroy_callout: '.callout .fa-times'
+
tour:
popover:
selectors:
diff --git a/test/selenium_tests/_workflow_fixtures.py b/test/selenium_tests/_workflow_fixtures.py
deleted file mode 100644
index 37a4d844e864..000000000000
--- a/test/selenium_tests/_workflow_fixtures.py
+++ /dev/null
@@ -1,38 +0,0 @@
-
-WORKFLOW_SIMPLE_CAT_TWICE = """
-class: GalaxyWorkflow
-inputs:
- - id: input1
-steps:
- - tool_id: cat
- label: first_cat
- state:
- input1:
- $link: input1
- queries:
- - input2:
- $link: input1
-"""
-
-WORKFLOW_WITH_OLD_TOOL_VERSION = """
-class: GalaxyWorkflow
-inputs:
- - id: input1
-steps:
- - tool_id: multiple_versions
- tool_version: "0.0.1"
- state:
- inttest: 8
-"""
-
-
-WORKFLOW_WITH_INVALID_STATE = """
-class: GalaxyWorkflow
-inputs:
- - id: input1
-steps:
- - tool_id: multiple_versions
- tool_version: "0.0.1"
- state:
- inttest: "moocow"
-"""
diff --git a/test/selenium_tests/test_workflow_editor.py b/test/selenium_tests/test_workflow_editor.py
index e9badc4f4718..cd8b78313dfe 100644
--- a/test/selenium_tests/test_workflow_editor.py
+++ b/test/selenium_tests/test_workflow_editor.py
@@ -1,8 +1,12 @@
-from ._workflow_fixtures import (
+from base.workflow_fixtures import (
+ WORKFLOW_NESTED_SIMPLE,
WORKFLOW_SIMPLE_CAT_TWICE,
+ WORKFLOW_SIMPLE_MAPPING,
WORKFLOW_WITH_INVALID_STATE,
WORKFLOW_WITH_OLD_TOOL_VERSION,
+ WORKFLOW_WITH_OUTPUT_COLLECTION,
)
+
from .framework import (
retry_assertion_during_transitions,
selenium_test,
@@ -15,11 +19,32 @@ class WorkflowEditorTestCase(SeleniumTestCase):
ensure_registered = True
@selenium_test
- def test_build_workflow(self):
+ def test_basics(self):
+ editor = self.components.workflow_editor
+
name = self.workflow_create_new()
edit_name_element = self.components.workflow_editor.edit_name.wait_for_visible()
assert name in edit_name_element.text, edit_name_element.text
+ editor.canvas_body.wait_for_visible()
+ editor.tool_menu.wait_for_visible()
+
+ self.screenshot("workflow_editor_blank")
+
+ self.components._.left_panel_drag.wait_for_visible()
+ self.components._.left_panel_collapse.wait_for_and_click()
+
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ self.screenshot("workflow_editor_left_collapsed")
+
+ self.components._.right_panel_drag.wait_for_visible()
+ self.components._.right_panel_collapse.wait_for_and_click()
+
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ self.screenshot("workflow_editor_left_and_right_collapsed")
+
@selenium_test
def test_data_input(self):
editor = self.components.workflow_editor
@@ -76,12 +101,49 @@ def test_collection_input(self):
@selenium_test
def test_existing_connections(self):
- name = self.workflow_upload_yaml_with_random_name(WORKFLOW_SIMPLE_CAT_TWICE)
- self.workflow_index_open()
- self.workflow_index_open_with_name(name)
- self.workflow_editor_click_option("Auto Re-layout")
+ self.open_in_workflow_editor(WORKFLOW_SIMPLE_CAT_TWICE)
+
+ editor = self.components.workflow_editor
+ self.assert_connected("input1#output", "first_cat#input1")
+ self.screenshot("workflow_editor_connection_simple")
+
+ cat_node = editor.node._(label="first_cat")
+ cat_input = cat_node.input_terminal(name="input1")
+ cat_input.wait_for_and_click()
+ editor.connector_destroy_callout.wait_for_visible()
+ self.screenshot("workflow_editor_connection_callout")
+ editor.connector_destroy_callout.wait_for_and_click()
+ self.assert_not_connected("input1#output", "first_cat#input1")
+ self.screenshot("workflow_editor_connection_destroyed")
+
+ self.workflow_editor_connect("input1#output", "first_cat#input1", screenshot_partial="workflow_editor_connection_dragging")
self.assert_connected("input1#output", "first_cat#input1")
+ @selenium_test
+ def test_rendering_output_collection_connections(self):
+ self.open_in_workflow_editor(WORKFLOW_WITH_OUTPUT_COLLECTION)
+ self.workflow_editor_maximize_center_pane()
+ self.screenshot("workflow_editor_output_collections")
+
+ @selenium_test
+ def test_simple_mapping_connections(self):
+ self.open_in_workflow_editor(WORKFLOW_SIMPLE_MAPPING)
+ self.workflow_editor_maximize_center_pane()
+ self.screenshot("workflow_editor_simple_mapping")
+ self.assert_connected("input1#output", "cat#input1")
+ self.assert_input_mapped("cat#input1")
+ self.workflow_editor_destroy_connection("cat#input1")
+ self.assert_input_not_mapped("cat#input1")
+ self.assert_not_connected("input1#output", "cat#input1")
+ self.workflow_editor_connect("input1#output", "cat#input1")
+ self.assert_input_mapped("cat#input1")
+
+ @selenium_test
+ def test_rendering_simple_nested_workflow(self):
+ self.open_in_workflow_editor(WORKFLOW_NESTED_SIMPLE)
+ self.workflow_editor_maximize_center_pane()
+ self.screenshot("workflow_editor_simple_nested")
+
@selenium_test
def test_save_as(self):
name = self.workflow_upload_yaml_with_random_name(WORKFLOW_SIMPLE_CAT_TWICE)
@@ -132,7 +194,42 @@ def workflow_editor_save_and_close(self):
self.workflow_editor_click_option("Save")
self.workflow_editor_click_option("Close")
+ def workflow_editor_maximize_center_pane(self):
+ self.components._.left_panel_collapse.wait_for_and_click()
+ self.components._.right_panel_collapse.wait_for_and_click()
+ self.sleep_for(self.wait_types.UX_RENDER)
+
+ def workflow_editor_connect(self, source, sink, screenshot_partial=None):
+ source_id, sink_id = self.workflow_editor_source_sink_terminal_ids(source, sink)
+ source_element = self.driver.find_element_by_css_selector("#" + source_id)
+ sink_element = self.driver.find_element_by_css_selector("#" + sink_id)
+
+ ac = self.action_chains()
+ ac = ac.move_to_element(source_element).click_and_hold()
+ if screenshot_partial:
+ ac = ac.move_to_element_with_offset(sink_element, -5, 0)
+ ac.perform()
+ self.sleep_for(self.wait_types.UX_RENDER)
+ self.screenshot(screenshot_partial)
+ ac = self.action_chains()
+
+ ac = ac.move_to_element(sink_element).release().perform()
+
def assert_connected(self, source, sink):
+ source_id, sink_id = self.workflow_editor_source_sink_terminal_ids(source, sink)
+ self.components.workflow_editor.connector_for(source_id=source_id, sink_id=sink_id).wait_for_visible()
+
+ def assert_not_connected(self, source, sink):
+ source_id, sink_id = self.workflow_editor_source_sink_terminal_ids(source, sink)
+ self.components.workflow_editor.connector_for(source_id=source_id, sink_id=sink_id).wait_for_absent()
+
+ def open_in_workflow_editor(self, yaml_content):
+ name = self.workflow_upload_yaml_with_random_name(yaml_content)
+ self.workflow_index_open()
+ self.workflow_index_open_with_name(name)
+ self.workflow_editor_click_option("Auto Re-layout")
+
+ def workflow_editor_source_sink_terminal_ids(self, source, sink):
editor = self.components.workflow_editor
source_node_label, source_output = source.split("#", 1)
@@ -153,7 +250,30 @@ def assert_connected(self, source, sink):
source_id = output_element.get_attribute("id")
sink_id = input_element.get_attribute("id")
- editor.connector_for(source_id=source_id, sink_id=sink_id).wait_for_visible()
+ return source_id, sink_id
+
+ def workflow_editor_destroy_connection(self, sink):
+ editor = self.components.workflow_editor
+
+ sink_node_label, sink_input_name = sink.split("#", 1)
+ sink_node = editor.node._(label=sink_node_label)
+ sink_input = sink_node.input_terminal(name=sink_input_name)
+ sink_input.wait_for_and_click()
+ editor.connector_destroy_callout.wait_for_and_click()
+
+ def assert_input_mapped(self, sink):
+ editor = self.components.workflow_editor
+ sink_node_label, sink_input_name = sink.split("#", 1)
+ sink_node = editor.node._(label=sink_node_label)
+ sink_mapping_icon = sink_node.input_mapping_icon(name=sink_input_name)
+ sink_mapping_icon.wait_for_visible()
+
+ def assert_input_not_mapped(self, sink):
+ editor = self.components.workflow_editor
+ sink_node_label, sink_input_name = sink.split("#", 1)
+ sink_node = editor.node._(label=sink_node_label)
+ sink_mapping_icon = sink_node.input_mapping_icon(name=sink_input_name)
+ sink_mapping_icon.wait_for_absent_or_hidden()
def workflow_index_open_with_name(self, name):
self.workflow_index_search_for(name)
diff --git a/test/selenium_tests/test_workflow_run.py b/test/selenium_tests/test_workflow_run.py
index da9414ee71d4..143fb5e4e7c1 100644
--- a/test/selenium_tests/test_workflow_run.py
+++ b/test/selenium_tests/test_workflow_run.py
@@ -1,7 +1,8 @@
-from ._workflow_fixtures import (
+from base.workflow_fixtures import (
WORKFLOW_SIMPLE_CAT_TWICE,
WORKFLOW_WITH_OLD_TOOL_VERSION,
)
+
from .framework import (
selenium_test,
SeleniumTestCase,