Merge pull request #5876 from jmchilton/editor_tests

More, better workflow editor Selenium tests.
This commit is contained in:
Dannon
2018-04-11 20:59:18 -04:00
committed by GitHub
12 changed files with 182 additions and 49 deletions
@@ -33,6 +33,8 @@ $.extend(Connector.prototype, {
}
},
redraw: function() {
const handle1 = this.handle1;
const handle2 = this.handle2;
var canvas_container = $("#canvas-container");
if (!this.canvas) {
this.canvas = document.createElement("canvas");
@@ -41,16 +43,18 @@ $.extend(Connector.prototype, {
this.canvas.style.zIndex = "300";
}
}
this.canvas.setAttribute("handle1-id", handle1 ? handle1.element.getAttribute("id") : "");
this.canvas.setAttribute("handle2-id", handle2 ? handle2.element.getAttribute("id") : "");
var relativeLeft = e => $(e).offset().left - canvas_container.offset().left;
var relativeTop = e => $(e).offset().top - canvas_container.offset().top;
if (!this.handle1 || !this.handle2) {
if (!handle1 || !handle2) {
return;
}
// Find the position of each handle
var start_x = relativeLeft(this.handle1.element) + 5;
var start_y = relativeTop(this.handle1.element) + 5;
var end_x = relativeLeft(this.handle2.element) + 5;
var end_y = relativeTop(this.handle2.element) + 5;
var start_x = relativeLeft(handle1.element) + 5;
var start_y = relativeTop(handle1.element) + 5;
var end_x = relativeLeft(handle2.element) + 5;
var end_y = relativeTop(handle2.element) + 5;
// Calculate canvas area
var canvas_extra = 100;
var canvas_min_x = Math.min(start_x, end_x);
@@ -80,13 +84,13 @@ $.extend(Connector.prototype, {
var start_offsets = null;
var end_offsets = null;
var num_offsets = 1;
if (this.handle1 && this.handle1.isMappedOver()) {
if (handle1 && handle1.isMappedOver()) {
var start_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
var start_offsets = [0];
}
if (this.handle2 && this.handle2.isMappedOver()) {
if (handle2 && handle2.isMappedOver()) {
var end_offsets = [-6, -3, 0, 3, 6];
num_offsets = 5;
} else {
@@ -62,6 +62,7 @@ $.extend(Workflow.prototype, {
add_node: function(node) {
node.id = this.id_counter;
node.element.attr("id", `wf-node-step-${node.id}`);
node.element.attr("node-label", node.label);
this.id_counter++;
this.nodes[node.id] = node;
this.has_changes = true;
@@ -229,6 +230,8 @@ $.extend(Workflow.prototype, {
});
}
node.id = parseInt(step.id) + offset;
node.element.attr("id", `wf-node-step-${node.id}`);
node.element.attr("node-label", step.label);
wf.nodes[node.id] = node;
max_id = Math.max(max_id, parseInt(id) + offset);
// For older workflows, it's possible to have HideDataset PJAs, but not WorkflowOutputs.
@@ -20,6 +20,7 @@ export default Backbone.View.extend({
renderToolLabel: function() {
this.$(".nodeTitle").text(this.node.label || this.node.name);
this.$el.attr("node-label", this.node.label);
},
renderToolErrors: function() {
@@ -69,14 +69,19 @@ var TerminalView = Backbone.View.extend({
var BaseInputTerminalView = TerminalView.extend({
className: "terminal input-terminal",
initialize: function(options) {
var node = options.node;
var input = options.input;
var name = input.name;
var terminal = this.terminalForInput(input);
const node = options.node;
const input = options.input;
const name = input.name;
const id = `node-${node.id}-input-${name}`;
const terminal = this.terminalForInput(input);
if (!terminal.multiple) {
this.setupMappingView(terminal);
}
this.el.terminal = terminal;
this.$el.attr("input-name", name);
this.$el.attr("id", id);
this.id = id;
terminal.node = node;
terminal.name = name;
node.input_terminals[name] = terminal;
@@ -171,12 +176,15 @@ var InputCollectionTerminalView = BaseInputTerminalView.extend({
var BaseOutputTerminalView = TerminalView.extend({
className: "terminal output-terminal",
initialize: function(options) {
var node = options.node;
var output = options.output;
var name = output.name;
var terminal = this.terminalForOutput(output);
const node = options.node;
const output = options.output;
const name = output.name;
const id = `node-${node.id}-output-${name}`;
const terminal = this.terminalForOutput(output);
this.setupMappingView(terminal);
this.el.terminal = terminal;
this.$el.attr("output-name", name);
this.$el.attr("id", id);
terminal.node = node;
terminal.name = name;
node.output_terminals[name] = terminal;
@@ -763,7 +763,7 @@ export default Backbone.View.extend({
if (type !== "subworkflow") {
buttons.append(
$("<div/>")
.addClass("fa-icon-button fa fa-files-o")
.addClass("fa-icon-button fa fa-files-o node-clone")
.click(e => {
node.clone();
})
@@ -771,7 +771,7 @@ export default Backbone.View.extend({
}
buttons.append(
$("<div/>")
.addClass("fa-icon-button fa fa-times")
.addClass("fa-icon-button fa fa-times node-destroy")
.click(e => {
node.destroy();
})
@@ -247,7 +247,7 @@
<div class="toolSectionBg">
%for module in module_section["modules"]:
<div class="toolTitle">
<a href="#" onclick="workflow_globals.app.add_node_for_module( '${module['name']}', '${module['title']}' )">
<a href="#" id="tool-menu-${module_section['name']}-${module['name']}" onclick="workflow_globals.app.add_node_for_module( '${module['name']}', '${module['title']}' )">
${module['description']}
</a>
</div>
@@ -270,7 +270,7 @@
<div class="unified-panel-body" style="overflow: auto;">
<div class="toolMenuContainer">
<div class="toolMenu">
<div class="toolMenu" id="workflow-tool-menu">
<%
from galaxy.workflow.modules import load_module_sections
module_sections = load_module_sections( trans )
@@ -355,11 +355,11 @@
<div class="unified-panel-header-inner" style="float: right">
<a id="workflow-options-button" class="panel-header-button" href="#"><span class="fa fa-cog"></span></a>
</div>
<div class="unified-panel-header-inner">
<div class="unified-panel-header-inner" id="workflow-canvas-title">
Workflow Canvas | ${h.to_unicode( stored.name ) | h}
</div>
</div>
<div class="unified-panel-body">
<div class="unified-panel-body" id="workflow-canvas-body">
<div id="canvas-viewport" style="width: 100%; height: 100%; position: absolute; overflow: hidden; background: #EEEEEE; background: white url(${h.url_for('/static/images/light_gray_grid.gif')}) repeat;">
<div id="canvas-container" style="position: absolute; width: 100%; height: 100%;"></div>
</div>
+3
View File
@@ -30,6 +30,9 @@ def convert_and_import_workflow(has_workflow, **kwds):
else:
workflow = yaml_to_workflow(has_workflow, galaxy_interface, workflow_directory)
name = kwds.get("name", None)
if name is not None:
workflow["name"] = name
publish = kwds.get("publish", False)
exact_tools = kwds.get("exact_tools", False)
import_kwds = {}
+5 -4
View File
@@ -25,15 +25,16 @@ class Target(object):
class SelectorTemplate(Target):
def __init__(self, selector, selector_type, children={}, kwds=None, with_classes=None):
def __init__(self, selector, selector_type, children=None, kwds=None, with_classes=None):
self._selector = selector
self.selector_type = selector_type
self._children = children
self._children = children or {}
self.__kwds = kwds or {}
self.with_classes = with_classes or []
@staticmethod
def from_dict(raw_value, children={}):
def from_dict(raw_value, children=None):
children = children or {}
if isinstance(raw_value, dict):
return SelectorTemplate(raw_value["selector"], raw_value.get("type", "css"), children=children)
else:
@@ -53,7 +54,7 @@ class SelectorTemplate(Target):
return SelectorTemplate(self.selector + " " + selector, self.selector_type, kwds=self.__kwds, children=self._children)
def __call__(self, **kwds):
new_kwds = self.__kwds
new_kwds = self.__kwds.copy()
new_kwds.update(**kwds)
return SelectorTemplate(self._selector, self.selector_type, kwds=new_kwds, with_classes=self.with_classes, children=self._children)
+1 -1
View File
@@ -422,7 +422,7 @@ class NavigatesGalaxy(HasDriver):
# /api/histories/cfc05ccec54895e2/contents?keys=type_id%2Celement_count&order=hid&v=dev&q=history_content_type&q=deleted&q=purged&q=visible&qv=dataset_collection&qv=False&qv=False&qv=True HTTP/1.1" 403 - "http://localhost:8080/"
# Like the logged in user doesn't have permission to the previously anonymous user's
# history, it is odd but I cannot replicate this outside of Selenium.
time.sleep(.35)
time.sleep(1.35)
if assert_valid:
# self.wait_for_selector_visible(".donemessage")
+27
View File
@@ -220,6 +220,33 @@ workflows:
new_button: '#new-workflow'
import_button: '#import-workflow'
workflow_editor:
node:
selectors:
_: "[node-label='${label}']"
title: '${_} .nodeTitle'
destroy: '${_} .node-destroy'
clone: '${_} .node-clone'
output_terminal: "${_} [output-name='${name}']"
input_terminal: "${_} [input-name='${name}']"
selectors:
canvas_body: '#workflow-canvas-body'
canvas_title: '#workflow-canvas-title'
edit_name: '#edit-attributes #workflow-name'
tool_menu: '#workflow-tool-menu'
tool_menu_section_link: '#title___workflow__${section_name}__ a span'
tool_menu_item_link: 'a#tool-menu-${section_name}-${item_name}'
label_input: "[tour_id='__label'] input"
annotation_input: "[tour_id='__annotation'] textarea"
connector_for: "canvas[handle1-id='${source_id}'][handle2-id='${sink_id}']"
tour:
popover:
selectors:
+4 -1
View File
@@ -39,7 +39,7 @@ class SmartTarget(object):
self._has_driver = has_driver
def __call__(self, *args, **kwds):
return self._wrap(self._target(*args, **kwds))
return self._wrap(self._target(*args[:], **kwds.copy()))
def __getattr__(self, name):
return self._wrap(getattr(self._target, name))
@@ -65,6 +65,9 @@ class SmartTarget(object):
def wait_for_text(self, **kwds):
return self._has_driver.wait_for_visible(self._target, **kwds).text
def wait_for_value(self, **kwds):
return self._has_driver.wait_for_visible(self._target, **kwds).get_attribute("value")
@property
def is_displayed(self):
return self._has_driver.is_displayed(self._target)
+105 -22
View File
@@ -16,33 +16,77 @@ class WorkflowEditorTestCase(SeleniumTestCase):
@selenium_test
def test_build_workflow(self):
name = "test_edit_nam"
self.workflow_create_new(name=name)
element = self.wait_for_selector("#edit-attributes #workflow-name")
assert name in element.text, element.text
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
@selenium_test
def test_data_input(self):
self.workflow_create_new()
self.sleep_for(self.wait_types.UX_TRANSITION)
menu = self.wait_for_selector(".toolMenu")
self.sleep_for(self.wait_types.UX_RENDER)
inputs_section = menu.find_element_by_css_selector("#title___workflow__inputs__")
inputs_link = inputs_section.find_element_by_css_selector("a span")
inputs_link.click()
self.wait_for_selector_visible("#__workflow__inputs__ .toolTitle")
input_links = self.driver.find_elements_by_css_selector("#__workflow__inputs__ .toolTitle a")
input_links[0].click()
self.screenshot("workflow_editor_data_input")
# TODO: verify box is highlighted and side panel is a form describing input.
# More work needs to be done to develop testing abstractions for doing these things.
editor = self.components.workflow_editor
name = self.workflow_create_new()
editor.canvas_body.wait_for_visible()
menu_element = editor.tool_menu.wait_for_visible()
editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
editor.tool_menu_item_link(section_name="inputs", item_name="data_input").wait_for_and_click()
self.screenshot("workflow_editor_data_input_new")
editor.label_input.wait_for_and_send_keys("input1")
editor.annotation_input.wait_for_and_send_keys("my cool annotation")
editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation.
self.screenshot("workflow_editor_data_input_filled_in")
self.workflow_editor_save_and_close()
self.workflow_index_open_with_name(name)
data_input_node = editor.node._(label="input1")
data_input_node.title.wait_for_and_click()
label = editor.label_input.wait_for_value()
assert label == "input1", label
assert editor.annotation_input.wait_for_value() == "my cool annotation"
data_input_node.destroy.wait_for_and_click()
data_input_node.wait_for_absent()
self.screenshot("workflow_editor_data_input_deleted")
@selenium_test
def test_collection_input(self):
editor = self.components.workflow_editor
name = self.workflow_create_new()
editor.canvas_body.wait_for_visible()
menu_element = editor.tool_menu.wait_for_visible()
editor.tool_menu_section_link(section_name="inputs").wait_for_and_click()
editor.tool_menu_item_link(section_name="inputs", item_name="data_collection_input").wait_for_and_click()
self.screenshot("workflow_editor_data_input_collection_new")
editor.label_input.wait_for_and_send_keys("input1")
editor.annotation_input.wait_for_and_send_keys("my cool annotation")
editor.label_input.wait_for_and_click() # Seems to help force the save of whole annotation.
self.screenshot("workflow_editor_data_input_collection_filled_in")
self.workflow_editor_save_and_close()
self.workflow_index_open_with_name(name)
data_input_node = editor.node._(label="input1")
data_input_node.title.wait_for_and_click()
label = editor.label_input.wait_for_value()
assert label == "input1", label
assert editor.annotation_input.wait_for_value() == "my cool annotation"
data_input_node.destroy.wait_for_and_click()
data_input_node.wait_for_absent()
self.screenshot("workflow_editor_data_input_deleted")
@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.assert_connected("input1#output", "first_cat#input1")
@selenium_test
def test_save_as(self):
workflow_populator = self.workflow_populator
workflow_populator.upload_yaml_workflow(WORKFLOW_SIMPLE_CAT_TWICE)
name = self.workflow_upload_yaml_with_random_name(WORKFLOW_SIMPLE_CAT_TWICE)
self.workflow_index_open()
self.workflow_index_click_option("Edit")
self.workflow_index_open_with_name(name)
self.sleep_for(self.wait_types.UX_RENDER)
self.screenshot("workflow_editor_edit_menu")
self.workflow_editor_click_option("Save As")
@@ -84,16 +128,55 @@ steps:
self.assert_modal_has_text("Tool is not installed")
self.screenshot("workflow_editor_missing_tool")
def workflow_create_new(self, name=None, annotation=None):
def workflow_editor_save_and_close(self):
self.workflow_editor_click_option("Save")
self.workflow_editor_click_option("Close")
def assert_connected(self, source, sink):
editor = self.components.workflow_editor
source_node_label, source_output = source.split("#", 1)
sink_node_label, sink_input = sink.split("#", 1)
source_node = editor.node._(label=source_node_label)
sink_node = editor.node._(label=sink_node_label)
source_node.wait_for_visible()
sink_node.wait_for_visible()
output_terminal = source_node.output_terminal(name=source_output)
input_terminal = sink_node.input_terminal(name=sink_input)
output_element = output_terminal.wait_for_visible()
input_element = input_terminal.wait_for_visible()
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()
def workflow_index_open_with_name(self, name):
self.workflow_index_search_for(name)
self.workflow_index_open()
self.workflow_index_click_option("Edit")
def workflow_upload_yaml_with_random_name(self, content):
workflow_populator = self.workflow_populator
name = self._get_random_name()
workflow_populator.upload_yaml_workflow(content, name=name)
return name
def workflow_create_new(self, annotation=None):
self.workflow_index_open()
self.click_button_new_workflow()
form_element = self.driver.find_element_by_id("submit")
name = name or self._get_random_name()
name = self._get_random_name()
annotation = annotation or self._get_random_name()
inputs = self.driver.find_elements_by_class_name("ui-input")
inputs[0].send_keys(name)
inputs[1].send_keys(annotation)
form_element.click()
return name
@retry_assertion_during_transitions
def assert_modal_has_text(self, expected_text):