From c63f562819c0ce6c00d0ee47cf820236ee1214e6 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 12 Apr 2018 09:07:15 -0400 Subject: [PATCH] More, better workflow editor Selenium tests - part 2. - More stuff tested: - Connecting and disconnecting simple terminals as well as mapping terminals (ribbons). - Rendering simple workflows, workflow with output collections, workflow with mapping, and workflow with a subworkflow. - Collapsing side panels in the workflow editor. - Small tweaks to editor DOM. - More screenshots. --- .../mvc/workflow/workflow-connector.js | 8 +- .../mvc/workflow/workflow-view-terminals.js | 4 +- templates/base/base_panels.mako | 8 +- templates/webapps/galaxy/galaxy.panels.mako | 8 +- test/api/test_workflows.py | 91 ++---------- test/base/workflow_fixtures.py | 134 +++++++++++++++++ test/galaxy_selenium/navigation.yml | 8 ++ test/selenium_tests/_workflow_fixtures.py | 38 ----- test/selenium_tests/test_workflow_editor.py | 135 +++++++++++++++++- test/selenium_tests/test_workflow_run.py | 3 +- 10 files changed, 299 insertions(+), 138 deletions(-) create mode 100644 test/base/workflow_fixtures.py delete mode 100644 test/selenium_tests/_workflow_fixtures.py 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 @@ %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..6ae1813f46df 100644 --- a/test/selenium_tests/test_workflow_editor.py +++ b/test/selenium_tests/test_workflow_editor.py @@ -1,8 +1,13 @@ -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, + WORKFLOW_WITH_RULES_1, ) + from .framework import ( retry_assertion_during_transitions, selenium_test, @@ -15,11 +20,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 +102,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 +195,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 +251,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,