diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index b15aa70e7..a184a7c6d 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -249,7 +249,9 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertTrue(s1_element.is_displayed()) # should still be displayed self.assertIn('hidden-parent', s1_element.get_attribute('class'), 'Session should be hidden when parent disabled') - s1_element.click() # try to select + + self.scroll_and_click((By.CSS_SELECTOR, '#session{}'.format(s1.pk))) + self.assertNotIn('selected', s1_element.get_attribute('class'), 'Session should not be selectable when parent disabled') @@ -299,9 +301,9 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): 'Session s1 should have moved to second meeting day') # swap timeslot column - put session in a differently-timed timeslot - self.driver.find_element(By.CSS_SELECTOR, + self.scroll_and_click((By.CSS_SELECTOR, '.day .swap-timeslot-col[data-timeslot-pk="{}"]'.format(slot1b.pk) - ).click() # open modal on the second timeslot for room1 + )) # open modal on the second timeslot for room1 self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-timeslot-col-modal").is_displayed()) self.driver.find_element(By.CSS_SELECTOR, '#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk) @@ -1373,13 +1375,8 @@ class InterimTests(IetfSeleniumTestCase): self.assertFalse(modal_div.is_displayed()) # Click the 'materials' button - open_modal_button = self.wait.until( - expected_conditions.element_to_be_clickable( - (By.CSS_SELECTOR, '[data-bs-target="#modal-%s"]' % slug) - ), - 'Modal open button not found or not clickable', - ) - open_modal_button.click() + open_modal_button_locator = (By.CSS_SELECTOR, '[data-bs-target="#modal-%s"]' % slug) + self.scroll_and_click(open_modal_button_locator) self.wait.until( expected_conditions.visibility_of(modal_div), 'Modal did not become visible after clicking open button', diff --git a/ietf/settings.py b/ietf/settings.py index a1a7fee10..0c57d87d1 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -598,6 +598,7 @@ TEST_CODE_COVERAGE_EXCLUDE_FILES = [ "ietf/review/import_from_review_tool.py", "ietf/utils/patch.py", "ietf/utils/test_data.py", + "ietf/utils/jstest.py", ] # These are code line regex patterns diff --git a/ietf/utils/jstest.py b/ietf/utils/jstest.py index 157f97912..215d78d65 100644 --- a/ietf/utils/jstest.py +++ b/ietf/utils/jstest.py @@ -12,6 +12,8 @@ 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 @@ -87,6 +89,48 @@ class IetfSeleniumTestCase(IetfLiveServerTestCase): # 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