Skip to content

Commit

Permalink
Switch to configurable, scaling approach to Selenium timeouts.
Browse files Browse the repository at this point in the history
Implement different "WAIT_TYPES" and try to sleep for different amounts of time based on what is being waited for.

Replace sleep statements in addition to Selenium wait on statements.
  • Loading branch information
jmchilton committed Oct 2, 2017
1 parent afaea96 commit c9915fc
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 58 deletions.
32 changes: 20 additions & 12 deletions test/galaxy_selenium/has_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,66 +35,74 @@ def selector_is_displayed(self, selector):
def assert_selector_absent(self, selector):
assert len(self.driver.find_elements_by_css_selector(selector)) == 0

def wait_for_xpath(self, xpath):
def wait_for_xpath(self, xpath, **kwds):
element = self._wait_on(
ec.presence_of_element_located((By.XPATH, xpath)),
"XPATH selector [%s] to become present" % xpath,
**kwds
)
return element

def wait_for_xpath_visible(self, xpath):
def wait_for_xpath_visible(self, xpath, **kwds):
element = self._wait_on(
ec.visibility_of_element_located((By.XPATH, xpath)),
"XPATH selector [%s] to become visible" % xpath,
**kwds
)
return element

def wait_for_selector(self, selector):
def wait_for_selector(self, selector, **kwds):
element = self._wait_on(
ec.presence_of_element_located((By.CSS_SELECTOR, selector)),
"CSS selector [%s] to become present" % selector,
**kwds
)
return element

def wait_for_selector_visible(self, selector):
def wait_for_selector_visible(self, selector, **kwds):
element = self._wait_on(
ec.visibility_of_element_located((By.CSS_SELECTOR, selector)),
"CSS selector [%s] to become visible" % selector,
**kwds
)
return element

def wait_for_selector_clickable(self, selector):
def wait_for_selector_clickable(self, selector, **kwds):
element = self._wait_on(
ec.element_to_be_clickable((By.CSS_SELECTOR, selector)),
"CSS selector [%s] to become clickable" % selector,
**kwds
)
return element

def wait_for_selector_absent_or_hidden(self, selector):
def wait_for_selector_absent_or_hidden(self, selector, **kwds):
element = self._wait_on(
ec.invisibility_of_element_located((By.CSS_SELECTOR, selector)),
"CSS selector [%s] to become absent or hidden" % selector,
**kwds
)
return element

def wait_for_selector_absent(self, selector):
def wait_for_selector_absent(self, selector, **kwds):
element = self._wait_on(
lambda driver: len(driver.find_elements_by_css_selector(selector)) == 0,
"CSS selector [%s] to become absent" % selector,
**kwds
)
return element

def wait_for_id(self, id):
def wait_for_id(self, id, **kwds):
return self._wait_on(
ec.presence_of_element_located((By.ID, id)),
"presence of DOM ID [%s]" % id,
**kwds
)

def _wait_on(self, condition, on_str=None):
def _wait_on(self, condition, on_str=None, **kwds):
if on_str is None:
on_str = str(condition)
message = "Timeout waiting on %s." % on_str
wait = self.wait()
wait = self.wait(**kwds)
return wait.until(condition, message)

def action_chains(self):
Expand All @@ -106,9 +114,9 @@ def send_enter(self, element):
def send_escape(self, element):
element.send_keys(Keys.ESCAPE)

def wait(self, timeout=UNSPECIFIED_TIMEOUT):
def wait(self, timeout=UNSPECIFIED_TIMEOUT, **kwds):
if timeout is UNSPECIFIED_TIMEOUT:
timeout = self.default_timeout
timeout = self.timeout_for(**kwds)
return WebDriverWait(self.driver, timeout)

def click_xpath(self, xpath):
Expand Down
60 changes: 48 additions & 12 deletions test/galaxy_selenium/navigates_galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from __future__ import print_function

import collections
import contextlib
import random
import string
Expand All @@ -13,6 +14,8 @@
import requests
import yaml

from galaxy.util.bunch import Bunch

from . import sizzle
from .data import NAVIGATION_DATA
from .has_driver import (
Expand All @@ -28,6 +31,26 @@
RETRY_DURING_TRANSITIONS_SLEEP_DEFAULT = .1
RETRY_DURING_TRANSITIONS_ATTEMPTS_DEFAULT = 10

WaitType = collections.namedtuple("WaitType", ["name", "default_length"])

# Default wait times should make sense for a development server under low
# load. Wait times for production servers can be scaled up with a multiplier.
WAIT_TYPES = Bunch(
# Rendering a form and registering callbacks, etc...
UX_RENDER=WaitType("ux_render", 1),
# Fade in, fade out, etc...
UX_TRANSITION=WaitType("ux_transition", 5),
# Toastr popup and dismissal, etc...
UX_POPUP=WaitType("ux_popup", 10),
# Creating a new history and loading it into the panel.
DATABASE_OPERATION=WaitType("database_operation", 10),
# Wait time for jobs to complete in default environment.
JOB_COMPLETION=WaitType("job_completion", 30),
)

# Choose a moderate wait type for operations that don't specify a type.
DEFAULT_WAIT_TYPE = WAIT_TYPES.DATABASE_OPERATION


class NullTourCallback(object):

Expand Down Expand Up @@ -95,6 +118,7 @@ class NavigatesGalaxy(HasDriver):
"""

default_password = DEFAULT_PASSWORD
wait_types = WAIT_TYPES

def get(self, url=""):
full_url = self.build_url(url)
Expand All @@ -104,6 +128,16 @@ def get(self, url=""):
def navigation_data(self):
return NAVIGATION_DATA

def wait_length(self, wait_type):
return wait_type.default_length * self.timeout_multiplier

def sleep_for(self, wait_type):
time.sleep(self.wait_length(wait_type))

def timeout_for(self, **kwds):
wait_type = kwds.get("wait_type", DEFAULT_WAIT_TYPE)
return self.wait_length(wait_type)

def home(self):
self.get()
self.wait_for_selector_visible("#masthead")
Expand Down Expand Up @@ -166,7 +200,7 @@ def latest_history_item(self):
assert len(history_contents) > 0
return history_contents[-1]

def wait_for_history(self, timeout=30, assert_ok=True):
def wait_for_history(self, assert_ok=True):
def history_becomes_terminal(driver):
current_history_id = self.current_history_id()
state = self.api_get("histories/%s" % current_history_id)["state"]
Expand All @@ -175,21 +209,23 @@ def history_becomes_terminal(driver):
else:
return None

final_state = self.wait(timeout).until(history_becomes_terminal)
timeout = self.timeout_for(wait_type=WAIT_TYPES.JOB_COMPLETION)
final_state = self.wait(timeout=timeout).until(history_becomes_terminal)
if assert_ok:
assert final_state == "ok", final_state
return final_state

def history_panel_wait_for_hid_ok(self, hid, timeout=60, allowed_force_refreshes=0):
self.history_panel_wait_for_hid_state(hid, 'ok', timeout=timeout, allowed_force_refreshes=allowed_force_refreshes)
def history_panel_wait_for_hid_ok(self, hid, allowed_force_refreshes=0):
self.history_panel_wait_for_hid_state(hid, 'ok', allowed_force_refreshes=allowed_force_refreshes)

def history_panel_wait_for_hid_visible(self, hid, timeout=60, allowed_force_refreshes=0):
def history_panel_wait_for_hid_visible(self, hid, allowed_force_refreshes=0):
current_history_id = self.current_history_id()

def history_has_hid(driver):
contents = self.api_get("histories/%s/contents" % current_history_id)
return any([d for d in contents if d["hid"] == hid])

timeout = self.timeout_for(wait_type=WAIT_TYPES.JOB_COMPLETION)
self.wait(timeout).until(history_has_hid)
contents = self.api_get("histories/%s/contents" % current_history_id)
history_item = [d for d in contents if d["hid"] == hid][0]
Expand All @@ -209,7 +245,7 @@ def history_item_wait_for_selector(self, history_item_selector, allowed_force_re
attempt = 0
while True:
try:
rval = self.wait_for_selector_visible(history_item_selector)
rval = self.wait_for_selector_visible(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION)
break
except self.TimeoutException:
if attempt >= allowed_force_refreshes:
Expand All @@ -223,18 +259,18 @@ def history_panel_wait_for_history_loaded(self):
# Use the search box showing up as a proxy that the history display
# has left the "loading" state and is showing a valid set of history contents
# (even if empty).
self.wait_for_selector_visible("#current-history-panel input.search-query")
self.wait_for_selector_visible("#current-history-panel input.search-query", wait_type=WAIT_TYPES.DATABASE_OPERATION)

def history_panel_wait_for_hid_hidden(self, hid, timeout=60):
def history_panel_wait_for_hid_hidden(self, hid):
current_history_id = self.current_history_id()
contents = self.api_get("histories/%s/contents" % current_history_id)
history_item = [d for d in contents if d["hid"] == hid][0]
history_item_selector = "#%s-%s" % (history_item["history_content_type"], history_item["id"])
self.wait_for_selector_absent(history_item_selector)
self.wait_for_selector_absent(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION)
return history_item_selector

def history_panel_wait_for_hid_state(self, hid, state, timeout=60, allowed_force_refreshes=0):
history_item_selector = self.history_panel_wait_for_hid_visible(hid, timeout=timeout, allowed_force_refreshes=allowed_force_refreshes)
def history_panel_wait_for_hid_state(self, hid, state, allowed_force_refreshes=0):
history_item_selector = self.history_panel_wait_for_hid_visible(hid, allowed_force_refreshes=allowed_force_refreshes)
history_item_selector_state = "%s.state-%s" % (history_item_selector, state)
try:
self.history_item_wait_for_selector(history_item_selector_state, allowed_force_refreshes)
Expand Down Expand Up @@ -509,7 +545,7 @@ def workflow_editor_click_option(self, option_label):
menu_element = self.workflow_editor_options_menu_element()
option_elements = menu_element.find_elements_by_css_selector("a")
assert len(option_elements) > 0, "Failed to find workflow editor options"
time.sleep(1)
self.sleep_for(WAIT_TYPES.UX_RENDER)
found_option = False
for option_element in option_elements:
if option_label in option_element.text:
Expand Down
10 changes: 5 additions & 5 deletions test/selenium_tests/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import datetime
import json
import os
import time
import traceback
import unittest
from functools import partial, wraps
Expand All @@ -30,14 +29,15 @@
from galaxy_selenium.navigates_galaxy import NavigatesGalaxy, retry_during_transitions # noqa: I100
from galaxy.util import asbool

DEFAULT_WAIT_TIMEOUT = 60
DEFAULT_TIMEOUT_MULTIPLIER = 1
DEFAULT_TEST_ERRORS_DIRECTORY = os.path.abspath("database/test_errors")
DEFAULT_SELENIUM_BROWSER = "auto"
DEFAULT_SELENIUM_REMOTE = False
DEFAULT_SELENIUM_REMOTE_PORT = "4444"
DEFAULT_SELENIUM_REMOTE_HOST = "127.0.0.1"
DEFAULT_SELENIUM_HEADLESS = "auto"

TIMEOUT_MULTIPLIER = float(os.environ.get("GALAXY_TEST_TIMEOUT_MULTIPLIER", DEFAULT_TIMEOUT_MULTIPLIER))
GALAXY_TEST_ERRORS_DIRECTORY = os.environ.get("GALAXY_TEST_ERRORS_DIRECTORY", DEFAULT_TEST_ERRORS_DIRECTORY)
# Test browser can be ["CHROME", "FIREFOX", "OPERA", "PHANTOMJS"]
GALAXY_TEST_SELENIUM_BROWSER = os.environ.get("GALAXY_TEST_SELENIUM_BROWSER", DEFAULT_SELENIUM_BROWSER)
Expand Down Expand Up @@ -196,8 +196,8 @@ def default_web_host(cls):
return default_web_host_for_selenium_tests()

@property
def default_timeout(self):
return DEFAULT_WAIT_TIMEOUT
def timeout_multiplier(self):
return TIMEOUT_MULTIPLIER

def build_url(self, url, for_selenium=True):
if for_selenium:
Expand Down Expand Up @@ -312,7 +312,7 @@ def assert_item_hid_text(self, hid):
def _assert_item_button(self, buttons_area, expected_button, button_def):
selector = button_def["selector"]
# Let old tooltip expire, etc...
time.sleep(1)
self.sleep_for(self.wait_types.UX_TRANSITION)
button_item = self.wait_for_selector_visible("%s %s" % (buttons_area, selector))
expected_tooltip = button_def.get("tooltip")
self.assert_tooltip_text(button_item, expected_tooltip)
Expand Down
4 changes: 1 addition & 3 deletions test/selenium_tests/test_custom_builds.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import time

from .framework import (
retry_assertion_during_transitions,
selenium_test,
Expand Down Expand Up @@ -81,7 +79,7 @@ def delete_custom_build(self, build_name):
delete_button.click()

def get_custom_builds(self):
time.sleep(1)
self.sleep_for(self.wait_types.UX_RENDER)
builds = []
grid = self.wait_for_selector('table.grid > tbody')
for row in grid.find_elements_by_tag_name('tr'):
Expand Down
11 changes: 4 additions & 7 deletions test/selenium_tests/test_history_panel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import time

from .framework import (
selenium_test,
SeleniumTestCase
Expand Down Expand Up @@ -70,21 +68,20 @@ def test_history_tags_and_annotations_buttons(self):
self.assert_selector_absent_or_hidden(anno_area_selector)

tag_icon.click()
time.sleep(.5)
self.sleep_for(self.wait_types.UX_TRANSITION)
annon_icon.click()

self.wait_for_selector(anno_area_selector)
self.assert_selector_absent_or_hidden(tag_area_selector)

annon_icon.click()
time.sleep(.5)
self.sleep_for(self.wait_types.UX_TRANSITION)

self.assert_selector_absent_or_hidden(tag_area_selector)
self.assert_selector_absent_or_hidden(anno_area_selector)

@selenium_test
def test_refresh_preserves_state(self):
refresh_wait = .5
self.register()
self.perform_upload(self.get_filename("1.txt"))
self.wait_for_history()
Expand All @@ -95,14 +92,14 @@ def test_refresh_preserves_state(self):
self.history_panel_refresh_click()

# After the refresh, verify the details are still open.
time.sleep(refresh_wait)
self.sleep_for(self.wait_types.UX_TRANSITION)
self.wait_for_selector_clickable(self.history_panel_item_selector(hid=1))
assert self.history_panel_item_showing_details(hid=1)

# Close the detailed display, refresh, and ensure they are still closed.
self.history_panel_click_item_title(hid=1, wait=True)
assert not self.history_panel_item_showing_details(hid=1)
self.history_panel_refresh_click()
time.sleep(refresh_wait)
self.sleep_for(self.wait_types.UX_TRANSITION)
self.wait_for_selector_clickable(self.history_panel_item_selector(hid=1))
assert not self.history_panel_item_showing_details(hid=1)
6 changes: 2 additions & 4 deletions test/selenium_tests/test_published_histories_grid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import time

from .framework import (
retry_assertion_during_transitions,
selenium_test,
Expand Down Expand Up @@ -98,7 +96,7 @@ def test_history_grid_tag_click(self):
self.assert_grid_histories_are([self.history1_name, self.history3_name], False)

def get_histories(self, sleep=False):
time.sleep(1.5)
self.sleep_for(self.wait_types.UX_RENDER)
names = []
grid = self.wait_for_selector('#grid-table-body')
for row in grid.find_elements_by_tag_name('tr'):
Expand Down Expand Up @@ -146,7 +144,7 @@ def unset_filter(self, filter_key, filter_value):
close_link_selector = 'a[filter_key="%s"][filter_val="%s"]' % \
(filter_key, filter_value)
self.wait_for_and_click_selector(close_link_selector)
time.sleep(.5)
self.sleep_for(self.wait_types.UX_RENDER)

def set_annotation(self, annotation):
anno_icon_selector = self.test_data['historyPanel']['selectors']['history']['annoIcon']
Expand Down

0 comments on commit c9915fc

Please sign in to comment.