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?
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:
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...
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()