Search code examples
python-3.xselenium-webdriverwebautomation

How do I resolve an Element Not Interactable Exception in web automation with Selenium


I'm practicing web automation with Selenium on GitHub's website, but when I try to automate clicking on the Sign in button on the GitHub's home page, VS code throws an Element Not Interactable exception. Here is my code, including page objects:

from selenium import webdriver
from selenium.webdriver.edge.options import Options
import unittest
import page


class GitHubLoginLogoutTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        edge_options = Options()
        edge_options.add_experimental_option("detach", True)
        cls.driver = webdriver.Edge(edge_options)
        cls.driver.get("https://github.com")
    
    def test_login(self):
        home_page = page.HomePage(self.driver)
        home_page.click_sign_in_button()
        login_page = page.LoginPage(self.driver)
        login_page.username_box_element = "*****"
        login_page.password_box_element = "*****"
        login_page.click_sign_in_button()

    def test_logout(self):
        account_page = page.AccountPage(self.driver)
        account_page.click_user_avatar()
        account_page.click_sign_out_button()
        logout_page = page.LogoutPage(self.driver)
        logout_page.click_active_user_sign_out_button()

    @classmethod
    def tearDownClass(cls):
        cls.driver.close()

if __name__ == "__main__":
    unittest.main()

and here is the page.py module I made:

from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver import ActionChains
from locator import HomePageLocators, LoginPageLocators, AccountPageLocators, LogoutPageLocators
from element import BasePageElement

class UsernameBoxElement(BasePageElement):
    locator = "#login_field"

class PasswordBoxElement(BasePageElement):
    locator = "#password"

class BasePage():
    def __init__(self, driver):
        self.driver = driver

class HomePage(BasePage):
    def click_sign_in_button(self):
        self.driver.find_element(*HomePageLocators.SIGN_IN_BUTTON).click()
        

class LoginPage(BasePage):
    username_box_element = UsernameBoxElement()

    password_box_element = PasswordBoxElement()

    def click_sign_in_button(self):
        self.driver.find_element(*LoginPageLocators.SIGN_IN_BUTTON).click()

class AccountPage(BasePage):
    def click_user_avatar(self):
        WebDriverWait(self.driver, 15).until(
            EC.element_to_be_clickable(
                AccountPageLocators.AVATAR_BUTTON)).click()
        
    def click_sign_out_button(self):
        element = self.driver.find_element(*AccountPageLocators.SIGN_OUT_BUTTON)
        action_chains = ActionChains(self.driver)
        action_chains.scroll_to_element(element)
        element.click()

class LogoutPage(BasePage):
    def click_active_user_sign_out_button(self):
        WebDriverWait(self.driver, 15).until(
            EC.element_to_be_clickable(
                LogoutPageLocators.ACTIVE_USER_SIGN_OUT_BUTTON)).click()

and here is the locator.py module:

from selenium.webdriver.common.by import By

class HomePageLocators():
    SIGN_IN_BUTTON = (By.CSS_SELECTOR, "a[href='/login']"

class LoginPageLocators():
    SIGN_IN_BUTTON = (By.CSS_SELECTOR, "input[value='Sign in']")

class AccountPageLocators():
    AVATAR_BUTTON = (By.CSS_SELECTOR, ".AppHeader-user")
    SIGN_OUT_BUTTON = (By.CSS_SELECTOR, "a[href='/logout']")

class LogoutPageLocators():
    ACTIVE_USER_SIGN_OUT_BUTTON = (By.CSS_SELECTOR, "input[value='Sign out']")

and finally here is the element.py module:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BasePageElement():
    def __set__(self, obj, value):
        driver = obj.driver
        element = WebDriverWait(driver, 15).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, self.locator)))
        element.send_keys(value)
    
    def __get__(self, obj, owner):
        driver = obj.driver
        element = WebDriverWait(driver, 15).until(
            EC.visibility_of_element_located((By.CSS_SELECTOR, self.locator)))
        return element.get_attribute("innerHTML")

I've tried using WebDriverWait and expected_conditions to wait for the element to be visible and for the element to be clickable but they result in a timeout. Here is the HTML of the element I want to click:

<div class="flex-1 flex-rder-2 text-right">
    <a href="/login" class="HeaderMenu-link HeaderMenu-button d-inline-flex d-lg-none
    flex-order-1 f5 no-underline border color-border-default rounded-2 px-2 py-1
    color-fg-inherit js-prevent-focus-on-mobile-nav" data-hydro-click="{"event_type":
    "authentication.click","payload":{"location_in_page":"site header menu","reposito
    ry_id":null,"auth_type":"SIGN_UP","originating_url":"https://github.com/",
    "user_id":null}}" data-hydro-click-hmac="1ac0bd316eb4ecff0fd1f338bc397cea8b5025ce
    78fffb7ade6ffdd600360286" data-analytics-event="{category":"Marketing nav",
    "action":"click to Sign in","label":"ref_page:Marketing;ref_cta:Sign in;ref_loc:
    Header"}"> Sign in </a> 

I've tried some CSS selectors to find and click the element such as a[href='/login'] and .text-right .flex-order-1 but the result is the Element Not Interactable exception. The only thing that's worked is using a Link Text to click the element, but I want to use a CSS Selector to do the same. Does anyone know why the element is not interactable using CSS selectors?


Solution

  • I'm not a fan of the page object model as they implement it in the python Selenium docs. It breaks many of the core concepts of the page object model.

    The problems:

    1. They have multiple classes/page objects in a single file which is going to get very long and messy if you do anything more than a super simple test.
    2. Locators for a page belong in the page object for that page. Same as #1... if you expand this to a real project, this locator file is going to be huge and very messy.
    3. setUp() and tearDown() should not be in the same file as a test. If you have 100 tests, you just end up with a bunch of unnecessary duplication. Also if you ever need to change either, you have to change it in 100 files rather than just one.

    Here's how I would do it...

    1. Each page object should have its own file/class.
    2. All locators for that page belong in the page object.
    3. All actions that can be done on the page should have a corresponding method in the page object.
    4. setUp() and tearDown() should be in their own file. NOTE: I didn't do this below to keep it somewhat simple.

    With these suggestions, the code is below...

    \tests\github_login_logout_test.py

    import unittest
    
    from selenium import webdriver
    from selenium.webdriver.edge.options import Options
    
    from home_page import HomePage
    from login_page import LoginPage
    from user_side_panel import UserSidePanel
    
    
    class GitHubLoginLogoutTest(unittest.TestCase):
        """Verify successful login and logout on github.com"""
    
        def setUp(self):
            edge_options = Options()
            edge_options.add_experimental_option("detach", True)
            self.driver = webdriver.Edge(edge_options)
            self.driver.get("https://github.com")
    
        def test_login_logout(self):
            """Verify successful login and logout on github.com"""
    
            USERNAME = "*****"
            PASSWORD = "*****"
    
            home_page = HomePage(self.driver)
            home_page.sign_in()
    
            LoginPage(self.driver).login(USERNAME, PASSWORD)
    
            home_page.open_user_side_panel()
    
            UserSidePanel(self.driver).sign_out()
    
            # TODO: add an assert that we're successfully logged out here
    
        def tearDown(self):
            self.driver.close()
    
    
    if __name__ == "__main__":
        unittest.main()
    

    \page_objects\homepage.py

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.support.wait import WebDriverWait
    
    class HomePage:
        '''Page object for the home page'''
        def __init__(self, driver: webdriver):
            self.driver = driver
            self.sign_in_locator = (By.CSS_SELECTOR, "div.HeaderMenu a[href='/login']")
            self.user_nav_menu_icon_locator = (By.CSS_SELECTOR, "button[aria-label='Open user navigation menu']")
            self.user_nav_menu_shadow_root_locator = (By.CSS_SELECTOR, "deferred-side-panel[data-url='/_side-panels/user'] > include-fragment")
    
        def sign_in(self):
            '''Clicks the "Sign in" link'''
            wait = WebDriverWait(self.driver, 10)
            wait.until(EC.element_to_be_clickable(self.sign_in_locator)).click()
    
        def open_user_side_panel(self):
            '''Signs out of the site'''
            wait = WebDriverWait(self.driver, 10)
            root = wait.until(EC.visibility_of_element_located(self.user_nav_menu_shadow_root_locator)).shadow_root
            root.find_element(*self.user_nav_menu_icon_locator).click()
    

    \page_objects\login_page.py

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.support.wait import WebDriverWait
    
    class LoginPage:
        '''Page object for the login page'''
        def __init__(self, driver: webdriver):
            self.driver = driver
            self.username_locator = (By.ID, "login_field")
            self.password_locator = (By.ID, "password")
            self.login_button_locator = (By.CSS_SELECTOR, "input[data-signin-label='Sign in']")
    
        def login(self, username: str, password: str):
            '''Logs in with the provided credentials'''
            wait = WebDriverWait(self.driver, 10)
            wait.until(EC.visibility_of_element_located(self.username_locator)).send_keys(username)
            wait.until(EC.visibility_of_element_located(self.password_locator)).send_keys(password)
            wait.until(EC.element_to_be_clickable(self.login_button_locator)).click()
    

    \page_objects\user_side_panel.py

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.support.wait import WebDriverWait
    
    class UserSidePanel:
        '''Page object for the user side panel'''
        def __init__(self, driver: webdriver):
            self.driver = driver
            self.sign_out_locator = (By.CSS_SELECTOR, "a[href='/logout']")
    
        def sign_out(self):
            '''Clicks "Sign out" link'''
            wait = WebDriverWait(self.driver, 10)
            wait.until(EC.element_to_be_clickable(self.sign_out_locator)).click()