* fix: selenium tests scroll_and_click * fix: reduce default timeout to 5 seconds * fix: also use scroll_and_click on test_upcoming_materials_modal * fix: remove conditional check on restoring scroll CSS * fix: restore conditional check on restoring scroll CSS * chore: code comments and adding jstest.py to coverage ignore
147 lines
6.3 KiB
Python
147 lines
6.3 KiB
Python
# Copyright The IETF Trust 2014-2021, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
|
|
from django.urls import reverse as urlreverse
|
|
from unittest import skipIf
|
|
|
|
skip_selenium = False
|
|
skip_message = ""
|
|
try:
|
|
from selenium import webdriver
|
|
from selenium.webdriver.firefox.service import Service
|
|
from selenium.webdriver.firefox.options import Options
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions
|
|
from selenium.webdriver.common.by import By
|
|
except ImportError as e:
|
|
skip_selenium = True
|
|
skip_message = "Skipping selenium tests: %s" % e
|
|
|
|
|
|
from ietf.utils.pipe import pipe
|
|
from ietf.utils.test_runner import IetfLiveServerTestCase
|
|
|
|
executable_name = 'geckodriver'
|
|
code, out, err = pipe('{} --version'.format(executable_name))
|
|
if code != 0:
|
|
skip_selenium = True
|
|
skip_message = "Skipping selenium tests: '{}' executable not found.".format(executable_name)
|
|
if skip_selenium:
|
|
print(" "+skip_message)
|
|
|
|
def start_web_driver():
|
|
service = Service(executable_path=f"/usr/bin/{executable_name}", log_output=f"{executable_name}.log", service_args=['--log-no-truncate'])
|
|
options = Options()
|
|
options.add_argument("--headless")
|
|
os.environ["MOZ_REMOTE_SETTINGS_DEVTOOLS"] = "1"
|
|
return webdriver.Firefox(service=service, options=options)
|
|
|
|
|
|
def selenium_enabled():
|
|
"""Are Selenium tests enabled?"""
|
|
return not skip_selenium
|
|
|
|
|
|
def ifSeleniumEnabled(func):
|
|
"""Only run test if Selenium testing is enabled"""
|
|
return skipIf(skip_selenium, skip_message)(func)
|
|
|
|
|
|
class IetfSeleniumTestCase(IetfLiveServerTestCase):
|
|
login_view = 'ietf.ietfauth.views.login'
|
|
|
|
def setUp(self):
|
|
super(IetfSeleniumTestCase, self).setUp()
|
|
self.driver = start_web_driver()
|
|
self.driver.set_window_size(1024,768)
|
|
|
|
def tearDown(self):
|
|
super(IetfSeleniumTestCase, self).tearDown()
|
|
self.driver.close()
|
|
|
|
def absreverse(self,*args,**kwargs):
|
|
return '%s%s'%(self.live_server_url, urlreverse(*args, **kwargs))
|
|
|
|
def debug_snapshot(self,filename='debug_this.png'):
|
|
self.driver.execute_script("document.body.bgColor = 'white';")
|
|
self.driver.save_screenshot(filename)
|
|
|
|
def login(self, username='plain'):
|
|
url = self.absreverse(self.login_view)
|
|
password = '%s+password' % username
|
|
self.driver.get(url)
|
|
self.driver.find_element(By.NAME, 'username').send_keys(username)
|
|
self.driver.find_element(By.NAME, 'password').send_keys(password)
|
|
self.driver.find_element(By.XPATH, '//*[@id="content"]//button[@type="submit"]').click()
|
|
|
|
def scroll_to_element(self, element):
|
|
"""Scroll an element into view"""
|
|
# Compute the offset to put the element in the center of the window
|
|
win_height = self.driver.get_window_rect()['height']
|
|
offset = element.rect['y'] + (element.rect['height'] - win_height) // 2
|
|
self.driver.execute_script(
|
|
'window.scroll({top: arguments[0], behavior: "instant"})',
|
|
offset,
|
|
)
|
|
# The ActionChains approach below seems to be fragile, hence he JS above.
|
|
# actions = ActionChains(self.driver)
|
|
# actions.move_to_element(element).perform()
|
|
|
|
def scroll_and_click(self, element_locator, timeout_seconds=5):
|
|
"""
|
|
Selenium has restrictions around clicking elements outside the viewport, so
|
|
this wrapper encapsulates the boilerplate of forcing scrolling and clicking.
|
|
|
|
:param element_locator: A two item tuple of a Selenium locator eg `(By.CSS_SELECTOR, '#something')`
|
|
"""
|
|
|
|
# so that we can restore the state of the webpage after clicking
|
|
original_html_scroll_behaviour_to_restore = self.driver.execute_script('return document.documentElement.style.scrollBehavior')
|
|
original_html_overflow_to_restore = self.driver.execute_script('return document.documentElement.style.overflow')
|
|
|
|
original_body_scroll_behaviour_to_restore = self.driver.execute_script('return document.body.style.scrollBehavior')
|
|
original_body_overflow_to_restore = self.driver.execute_script('return document.body.style.overflow')
|
|
|
|
self.driver.execute_script('document.documentElement.style.scrollBehavior = "auto"')
|
|
self.driver.execute_script('document.documentElement.style.overflow = "auto"')
|
|
|
|
self.driver.execute_script('document.body.style.scrollBehavior = "auto"')
|
|
self.driver.execute_script('document.body.style.overflow = "auto"')
|
|
|
|
element = self.driver.find_element(element_locator[0], element_locator[1])
|
|
self.scroll_to_element(element)
|
|
|
|
# Note that Selenium itself seems to have multiple definitions of 'clickable'.
|
|
# You might expect that the following wait for the 'element_to_be_clickable'
|
|
# would confirm that the following .click() would succeed but it doesn't.
|
|
# That's why the preceeding code attempts to force scrolling to bring the
|
|
# element into the viewport to allow clicking.
|
|
WebDriverWait(self.driver, timeout_seconds).until(expected_conditions.element_to_be_clickable(element_locator))
|
|
|
|
element.click()
|
|
|
|
if original_html_scroll_behaviour_to_restore:
|
|
self.driver.execute_script(f'document.documentElement.style.scrollBehavior = "{original_html_scroll_behaviour_to_restore}"')
|
|
if original_html_overflow_to_restore:
|
|
self.driver.execute_script(f'document.documentElement.style.overflow = "{original_html_overflow_to_restore}"')
|
|
|
|
if original_body_scroll_behaviour_to_restore:
|
|
self.driver.execute_script(f'document.body.style.scrollBehavior = "{original_body_scroll_behaviour_to_restore}"')
|
|
if original_body_overflow_to_restore:
|
|
self.driver.execute_script(f'document.body.style.overflow = "{original_body_overflow_to_restore}"')
|
|
|
|
class presence_of_element_child_by_css_selector:
|
|
"""Wait for presence of a child of a WebElement matching a CSS selector
|
|
|
|
This is a condition class for use with WebDriverWait.
|
|
"""
|
|
def __init__(self, element, child_selector):
|
|
self.element = element
|
|
self.child_selector = child_selector
|
|
|
|
def __call__(self, driver):
|
|
child = self.element.find_element(By.CSS_SELECTOR, self.child_selector)
|
|
return child if child is not None else False
|