mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 12:13:33 +08:00
Merge pull request #5876 from jmchilton/editor_tests
More, better workflow editor Selenium tests.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user