Working with Python and Selenium

Irving Kcam
Cubo Marketing Digital
3 min readApr 24, 2019
Image source

Selenium it’s a amazing tool for testing, scrapping, automating and even more (for sure), in my case I used it for automating and I run into a few issues like this ones:

SPA (Single Page Applications) updates the DOM on the go, so we need to wait a few seconds before some actions or retry “n” times before the element it’s present or interactive, here are some of the questions raised for this scenario:

  • What if the driver opens non-desired tabs initially?
  • What if won’t click on the element because isn’t visible?
  • What if is it an optional or non-critical action?
  • What if the same element has a different selector on each load?

All of this happens over and over again so I had to work on some sort of solution to avoid some of this weird exceptions raised by Selenium when something fails. So I created a Selenium handler:

import os
import pdb
import platform
import time

from selenium import webdriver
from selenium.common.exceptions import (
TimeoutException, WebDriverException
)
from selenium.webdriver.common.action_chains import ActionChains

from logger import Logger


class BaseSelenium:
def __init__(self, *args, **kwargs):
self.logger = Logger()

def get_driver(self):
if hasattr(self, 'driver') and self.driver:
return self.driver

driver = webdriver.Chrome()

while len(driver.window_handles) > 1:
driver.switch_to.window(driver.window_handles[1])
driver.close()
driver.switch_to.window(driver.window_handles[0])
self._wait(2)

return driver

def quit_driver(self):
self.logger(data="Closing at {}.".format(self.driver.current_url))
self.driver.quit()

def perform_action(func):
def wrapper(self, by, selector, *args, **kwargs):
retry = 0
success = False

try:
max_retries = int(kwargs.get('max_retries', 5))
except ValueError:
max_retries = 5

try:
timeout = int(kwargs.get('timeout', 0))
except ValueError:
timeout = 0

return_method = func.__name__.startswith('get_')
raise_exception = kwargs.get('raise_exception', True)

if timeout:
self._wait(timeout)

while not success:
retry += 1

if retry > max_retries:
if raise_exception:
self._start_debug()
raise TimeoutException
else:
return success

try:
if not isinstance(selector, (list, tuple)):
selector = [selector]

for s in selector:
try:
self.logger(data={
'action': func.__name__,
'selector': s,
'args': args,
'retry': retry,
})
response = func(self, by, s, *args, **kwargs)
success = True
break
except Exception:
pass
if not success:
raise TimeoutException
except (TimeoutException, WebDriverException):
self._wait(1)

return response if return_method else success

return wrapper

@perform_action
def click_element(
self, by, selector, source=None, move=False, *args, **kwargs
):
final_source = source or self.driver
element = final_source.find_element(by, selector)

if move:
ActionChains(self.driver) \
.move_to_element(source or element) \
.perform()

disabled = element.get_attribute('aria-disabled')
if disabled == 'true':
self._wait(5)
raise WebDriverException
element.click()

@perform_action
def clear_input(self, by, selector, source=None, *args, **kwargs):
source = source or self.driver
element = source.find_element(by, selector)
element.clear()

@perform_action
def fill_input(self, by, selector, content, source=None, *args, **kwargs):
source = source or self.driver
element = source.find_element(by, selector)
element.send_keys(content)

@perform_action
def get_text(self, by, selector, source=None, *args, **kwargs):
source = source or self.driver
element = source.find_element(by, selector)
return element.text

@perform_action
def get_element(self, by, selector, source=None, *args, **kwargs):
source = source or self.driver
element = source.find_element(by, selector)
return element

@perform_action
def get_elements(self, by, selector, source=None, *args, **kwargs):
source = source or self.driver
elements = source.find_elements(by, selector)
if len(elements) == 0:
raise WebDriverException
return elements

def handle(self):
raise NotImplementedError("`handle` nor implemented in the Base.")

def _wait(self, seconds):
for second in range(seconds):
print('Wait: {:d}/{:d}'.format(second + 1, seconds))
time.sleep(1)

def _start_debug(self, *args, **kwargs):
pdb.set_trace()

View in Gist

How do we use this?

from base import BaseSelenium


class GoogleLoginSelenium(BaseSelenium):
def __init__(self, entity, *args, **kwargs):
super().__init__(*args, **kwargs)
self.entity = entity
self.handle()
self.quit_driver()

def handle(self):
self.driver = self.get_driver()
self.do_login()

def is_success_login(self):
self._wait(3)
return self.driver.current_url.startswith(
'https://myaccount.google'
)

def do_login(self):
self.driver.get('https://accounts.google.com/ServiceLogin')
self.fill_input(
By.ID,
'identifierId',
self.entity.email + Keys.RETURN
)
self.fill_input(
By.NAME,
'password',
self.entity.password + Keys.RETURN,
timeout=3
)

if self.is_success_login():
return

element = self.get_element(
By.CSS_SELECTOR,
'input[type="password"]',
raise_exception=False
)
if element:
raise CredentialInvalid("Wrong password.")

success = self.click_element(
By.CSS_SELECTOR,
'div[data-challengetype="12"]',
raise_exception=False,
timeout=3
)
if success:
self.fill_input(
By.NAME,
'knowledgePreregisteredEmailResponse',
self.entity.recovery_email + Keys.RETURN,
timeout=3
)

if not self.is_success_login():
raise CredentialInvalid(
msg="Login failed", logger=self.logger
)

View on Gist

You simply run GoogleLoginSelenium(entity) and it will try to login on Google.

from google import GoogleLoginSelenium


class CFake:
username = "test@gmail.com"
password = "MyPassword"
revovery_email = "recovery@other.com"


GoogleLoginSelenium(entity=CFake)

View on Gist

Enjoy

--

--