I'd like to use Selenium for creating simple functional tests against Plone add-ons. The main driver here is that non-programmers can create and understand test cases with some effort, because they see in a web browser what's happening. What is the recommended best practice to
The test case prepares Plone site environment where the test will be run (installs add-ons, mocks up mail host, creates sample content)
How to run Plone functional test case to the point you can start a Selenium recording in a browser and how to open a browser with recording enabled?
How later run the recorded test output from Python code?
Are there other test recording frameworks out there which you combine with Plone? Able to tests against Javascripted UI is a requirement.
My guess is that there are tools that do the individual steps independently, but those tools don't work together exactly as you describe.
In my experience, the quality of recorded tests is so bad that a programmer will have to rewrite them anyway. It only gets worse when you have a lot of JavaScript. If in addition, you have a site that uses AJAX, one of the problems that occurs is that you will sometimes have to wait for a specific element to appear, before doing the next click, and this is where most recorders will fail.
I would also love to hear about a tool that is targeted to end users, and allows them to record and run Plone tests on their own, and if anyone knows about this kind of project, I would really like to get involved in its development.
plone.app.testing comes with seleniumtestlayer since 4.1.
Here is my own more sophisticated helper code:
"""
Some PSE 2012 Selenium notes
* https://github.com/plone/plone.seleniumtesting
* https://github.com/emanlove/pse2012
Selenium WebDriver API
* http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/remote/webdriver.py
Selenium element match options
* http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/common/by.py
Selenium element API (after find_xxx())
* http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/remote/webelement.py
You can do pdb debugging using ``selenium_helper.selenium_error_trapper()`` if you run
tests with ``SELENIUM_DEBUG`` turned on::
SELENIUM_DEBUG=true bin/test -s testspackage -t test_usermenu
Then you'll get debug prompt on any Selenium error.
"""
import os
# Try use ipdb debugger if we have one
try:
import ipdb as pdb
except ImportError:
import pdb
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support.wait import WebDriverWait
from plone.app.testing import selenium_layers
SELENIUM_DEBUG = "SELENIUM_DEBUG" in os.environ
class SeleniumTrapper(object):
"""
With statement for break on Selenium errors to ipdb if it has been enabled for this test run.
"""
def __init__(self, driver):
self.driver = driver
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
"""
http://effbot.org/zone/python-with-statement.htm
"""
if isinstance(value, WebDriverException) and SELENIUM_DEBUG:
# This was Selenium exception
print "Selenium API call failed because of browser state error: %s" % value
print "Selenium instance has been bound to self.driver"
pdb.set_trace()
class SeleniumHelper(object):
"""
Selenium convenience methods for Plone.
Command Selenium browser to do common actions.
This mainly curries and delegates to plone.app.testing.selenium_layers helper methods.
More info:
* https://github.com/plone/plone.app.testing/blob/master/plone/app/testing/selenium_layers.py
"""
def __init__(self, testcase, driver=None):
"""
:param testcase: Yout test class instance
:param login_ok_method: Selenium check function to run to see if login success login_ok_method(selenium_helper)
"""
self.testcase = testcase
if driver:
# Use specific Selenium WebDriver instance
self.driver = driver
else:
# plone.app.tesrting selenium layer
self.driver = testcase.layer['selenium']
self.portal = testcase.layer["portal"]
def selenium_error_trapper(self):
"""
Create ``with`` statement context helper which will invoke Python ipdb debugger if Selenium fails to do some action.
If you run test with SELENIUM_DEBUG env var set you'll get dropped into a debugger on error.
"""
return SeleniumTrapper(self.driver)
def reset(self):
"""
Reset Selenium test browser between tests.
"""
def login(self, username, password, timeout=15, poll=0.5, login_cookie_name="__ac", login_url=None):
"""
Perform Plone login using Selenium test browser and Plone's /login_form page.
"""
submit_button_css = '#login_form input[name=submit]'
if not login_url:
# Default Plone login URL
login_url = self.portal.absolute_url() + '/login_form'
with self.selenium_error_trapper():
submit_button = self.open(login_url, wait_until_visible=submit_button_css)
self.find_element(By.CSS_SELECTOR, 'input#__ac_name').send_keys(username)
self.find_element(By.CSS_SELECTOR, 'input#__ac_password').send_keys(password)
submit_button.click()
# Check that we get Plone login cookie before the timeout
waitress = WebDriverWait(self.driver, timeout, poll)
matcher = lambda driver: driver.get_cookie(login_cookie_name) not in ["", None]
waitress.until(matcher, "After login did not get login cookie named %s" % login_cookie_name)
def logout(self, logout_url=None):
"""
Perform logout using Selenium test browser.
:param logout_url: For non-default Plone logout view
"""
if not logout_url:
logout_url = self.portal.absolute_url() + "/logout"
self.open(logout_url)
def get_plone_page_heading(self):
"""
Get Plone main <h1> contents as lowercase.
XXX: Looks like Selenium API returns uppercase if there is text-transform: uppercase?
:return: Empty string if there is no title on the page (convenience for string matching)
"""
try:
title_elem = self.driver.find_element_by_class_name("documentFirstHeading")
except NoSuchElementException:
return ""
if not title_elem:
return ""
return title_elem.text.lower()
def trap_error_log(self, orignal_page=None):
"""
Read error from the site error log and dump it to output.
Makes debugging Selenium tests much more fun when you directly see
the actual errors instead of OHO.
:param orignal_page: Decorate the traceback with URL we tried to access.
"""
# http://svn.zope.org/Zope/trunk/src/Products/SiteErrorLog/SiteErrorLog.py?rev=96315&view=auto
error_log = self.portal.error_log
entries = error_log.getLogEntries()
if len(entries) == 0:
# No errors, yay!
return
msg = ""
if orignal_page:
msg += "Plone logged an error when accessing page %s\n" % orignal_page
# We can only fail on traceback
if len(entries) >= 2:
msg += "Several exceptions were logged.\n"
entry = entries[0]
raise AssertionError(msg + entry["tb_text"])
def is_error_page(self):
"""
Check that if the current page is Plone error page.
"""
return "but there seems to be an error" in self.get_plone_page_heading()
def is_unauthorized_page(self):
"""
Check that the page is not unauthorized page.
..note ::
We cannot distingush login from unauthorized
"""
# require_login <-- auth redirect final target
return "/require_login/" in self.driver.current_url
def is_not_found_page(self):
"""
Check if we got 404
"""
return "this page does not seem to exist" in self.get_plone_page_heading()
def find_element(self, by, target):
"""
Call Selenium find_element() API and break on not found and such errors if running tests in SELENIUM_DEBUG mode.
"""
with self.selenium_error_trapper():
return self.driver.find_element(by, target)
def find_elements(self, by, target):
"""
Call Selenium find_elements() API and break on not found and such errors if running tests in SELENIUM_DEBUG mode.
"""
with self.selenium_error_trapper():
return self.driver.find_elements(by, target)
def click(self, by, target):
"""
Click an element.
:param by: selenium.webdriver.common.by.By contstant
:param target: CSS selector or such
"""
with self.selenium_error_trapper():
elem = self.driver.find_element(by, target)
elem.click()
def open(self, url, check_error_log=True, check_sorry_error=True, check_unauthorized=True, check_not_found=True, wait_until_visible=None):
"""
Open an URL in Selenium browser.
If url does not start with http:// assume it is a site root relative URL.
:param wait_until_visible: CSS selector which must match before we proceed
:param check_error_log: If the page has created anything in Plone error log then dump this traceback out.
:param check_sorry_error: Assert on Plone error response page
:param check_unauthorized: Assert on Plone Unauthorized page (login dialog)
:return: Element queried by wait_until_visible or None
"""
elem = None
# Convert to abs URL
if not (url.startswith("http://") or url.startswith("https://")):
url = self.portal.absolute_url() + url
selenium_layers.open(self.driver, url)
if check_error_log:
self.trap_error_log(url)
if wait_until_visible:
elem = self.wait_until_visible(By.CSS_SELECTOR, wait_until_visible)
# XXX: These should be waited also
if check_sorry_error:
self.testcase.assertFalse(self.is_error_page(), "Got Plone error page for url: %s" % url)
if check_unauthorized:
self.testcase.assertFalse(self.is_unauthorized_page(), "Got Plone Unauthorized page for url: %s" % url)
if check_not_found:
self.testcase.assertFalse(self.is_not_found_page(), "Got Plone not found page for url: %s" % url)
return elem
def wait_until_visible(self, by, target, message=None, timeout=10, poll=0.5):
"""
Wait until some element is visible on the page (assume DOM is ready by then).
Wraps selenium.webdriver.support.wait() API.
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver_support/selenium.webdriver.support.wait.html#module-selenium.webdriver.support.wait
"""
if not message:
message = "Waiting for element: %s" % target
waitress = WebDriverWait(self.driver, timeout, poll)
matcher = lambda driver: driver.find_element(by, target)
waitress.until(matcher, message)
elem = self.driver.find_element(by, target)
return elem
(Not on github yet)
Related
I want to sign in to my Google Account and enable a Google API and extract the developer's key. My main task is to automate this process.
Everyone knows that you can't log into the Google Account using an automated browser. I did manage to do that using scrapy splash.
import re
import time
import base64
import scrapy
from scrapy_splash import SplashRequest
from selenium import webdriver
class GoogleScraperSpider(scrapy.Spider):
name = 'google_scraper'
script = """
function main(splash)
splash:init_cookies(splash.args.cookies)
local url = splash.args.url
local youtube_url = "https://console.cloud.google.com/apis/library/youtube.googleapis.com"
assert(splash:go(url))
assert(splash:wait(1))
splash:set_viewport_full()
local search_input = splash:select('.whsOnd.zHQkBf')
search_input:send_text("xxxxxxxxxxx#gmail.com")
assert(splash:wait(1))
splash:runjs("document.getElementById('identifierNext').click()")
splash:wait(5)
local search_input = splash:select('.whsOnd.zHQkBf')
search_input:send_text("xxxxxxxx")
assert(splash:wait(1))
splash:runjs("document.getElementById('passwordNext').click()")
splash:wait(5)
return {
cookies = splash:get_cookies(),
html = splash:html(),
png = splash:png()
}
end
"""
def start_requests(self):
url = 'https://accounts.google.com'
yield SplashRequest(url, self.parse, endpoint='execute', session_id="1", args={'lua_source': self.script})
def parse(self, response):
imgdata = base64.b64decode(response.data['png'])
with open('image.png', 'wb') as file:
file.write(imgdata)
cookies = response.data.get("cookies")
driver = webdriver.Chrome("./chromedriver")
for cookie in cookies:
if "." in cookie["domain"][:1]:
url = f"https://www{cookie['domain']}"
else:
url = f"https://{cookie['domain']}"
driver.get(url)
driver.add_cookie(cookie)
driver.get("https://console.cloud.google.com/apis/library/youtube.googleapis.com")
time.sleep(5)
In the parse function I'm trying to retrieve those cookies and add them to my chromedriver to bypass the login process so I can move ahead to enabling the API and extracting the key but I always face the login page in the chromedriver.
Your help would be most appreciated.
Thanks.
try using pickle to save cookies instead, just use any python console to save the cookies with this code
import pickle
input('press enter when logged')
pickle.dump(driver.get_cookies(), open('cookies.pkl'))
then you get the cookies.pkl file with google login data, import it in your code using:
import pickle
cookies = pickle.load(open('cookies.pkl'))
for cookie in cookies:
driver.add_cookies(cookie)
driver.refresh()
# rest of work here...
refresh the driver to enable the cookies
I am trying to scrape this website using python's BeautifulSoup package and for automating the user flow I am using selenium. As this website requires authentication to access this page, I am trying to log in first using selenium webdriver. Here is my code:
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
def configure_driver():
# Add additional Options to the webdriver
chrome_options = Options()
# add the argument and make the browser Headless.
chrome_options.add_argument("--headless")
# Instantiate the Webdriver: Mention the executable path of the webdriver you have downloaded
# For linux/Mac
# driver = webdriver.Chrome(options = chrome_options)
# For windows
driver = webdriver.Chrome(executable_path="/home/<user_name>/Downloads/chromedriver_linux64/chromedriver",
options = chrome_options)
return driver
def getLinks(driver):
# Step 1: Go to pluralsight.com, category section with selected search keyword
driver.get(f"https://www.coursera.org/learn/competitive-data-science/supplement/RrrDR/additional-material-and-links")
# wait for the element to load
try:
WebDriverWait(driver, 5).until(lambda s: s.find_element_by_class_name("_ojjigd").is_displayed())
except TimeoutException:
print("TimeoutException: Element not found")
return None
email = driver.find_element_by_name('email')
print(str(email))
password = driver.find_element_by_name('password')
email.send_keys("username") # provide some actual username
password.send_keys("password") # provide some actual password
form = driver.find_element_by_name('login')
print(form.submit())
WebDriverWait(driver, 10)
print(driver.title)
soup = BeautifulSoup(driver.page_source)
# Step 3: Iterate over the search result and fetch the course
divs = soup.findAll('div', args={'class': 'item-box-content'})
print(len(divs))
# create the driver object.
driver = configure_driver()
getLinks(driver)
# close the driver.
driver.close()
Now after doing form.submit() it is expected to log in and change the page, right? But it is simply staying in the same page, so I cannot access the contents of the authenticated page. Someone please help.
That is because there is no name attribute.
instead of this :
form = driver.find_element_by_name('login')
Use this :
wait.until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Login']"))).click()
I tried this code on my local, seems to be working fine
driver.maximize_window()
wait = WebDriverWait(driver, 30)
driver.get("https://www.coursera.org/learn/competitive-data-science/supplement/RrrDR/additional-material-and-links")
wait.until(EC.element_to_be_clickable((By.ID, "email"))).send_keys("some user name")
wait.until(EC.element_to_be_clickable((By.ID, "password"))).send_keys("some user name")
wait.until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Login']"))).click()
Since login button is in a form so .submit() should work too.
wait.until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Login']"))).submit()
This works too.
I'd like to run an end-to-end test of logging into our website using Selenium. We use Auth0 and the only available login mechanism is through Google social login. I wrote a script using Python Selenium (version 3.141.0), pytest, and selenium/standalone-chrome:87.0 Docker image which works correctly on my local machine, Mac OS 10.15.4.
However, it gets stuck at some point when I try to run it on CircleCI.
I use ubuntu-1604:202007-01 image in CircleCI
How I set up remote driver (tried a lot of arguments/commands..):
#pytest.fixture(scope="function")
def browser(remote_webdriver_url):
options = webdriver.ChromeOptions()
options.add_argument('--disable-popup-blocking')
options.add_argument('--disable-web-security')
options.add_argument('--allow-running-insecure-content')
options.add_argument('--start-maximized')
options.add_argument('-incognito')
options.add_experimental_option("useAutomationExtension", False)
options.add_experimental_option("excludeSwitches", ["enable-automation"])
browser = webdriver.Remote(
command_executor=remote_webdriver_url,
desired_capabilities=DesiredCapabilities.CHROME,
options=options)
return browser
My docker-compose.yml
version: '3.1'
services:
selenium-chrome:
image: selenium/standalone-chrome:87.0
# added the envvar as I found something about this in Selenium forums, it has no effect.
environment:
DBUS_SESSION_BUS_ADDRESS: /dev/null
shm_size: 2g
restart: 'no'
ports:
- "4444:4444"
My test code:
import os
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def test_logging_in(browser: WebElement, url):
auth0_title = "Sign In with Auth0"
browser.get(url)
assert browser.title == auth0_title
# Log in
auth_login_button_class_name = 'auth0-lock-social-button-text'
_ = WebDriverWait(browser, 20).until(EC.presence_of_element_located((By.CLASS_NAME, auth_login_button_class_name)))
sign_in_button = browser.find_element_by_class_name(auth_login_button_class_name)
browser.implicitly_wait(5)
#
sign_in_button.click()
# Wait until we're redirected to Google's login page
_ = WebDriverWait(browser,20).until(EC.title_contains('Google'))
# Type in the email address and go to the next page
email_input = browser.find_element_by_tag_name('input')
email_input.send_keys(os.environ.get('E2E_TEST_DEVELOPMENT_USER_EMAIL'))
first_next_button = browser.find_element_by_id("identifierNext")
first_next_button.click()
# Wait until the password entry screen is loaded
browser.get_screenshot_as_file('/tmp/scr.png')
_ = WebDriverWait(browser, 20).until(EC.presence_of_element_located((By.ID, "forgotPassword"))) ##### THIS IS WHERE I GET THE TIMEOUT
# Put the password in
password_input = browser.find_element_by_xpath("//input[#name='password']")
password_input.send_keys(os.environ.get('E2E_TEST_DEVELOPMENT_USER_PASSWORD'))
second_next_button = browser.find_element_by_id("passwordNext")
second_next_button.click()
# Wait until the login is successful by observing the logout button
logout_icon_class_name = "bp3-icon-log-out"
_ = WebDriverWait(browser, 20).until(EC.presence_of_element_located((By.CLASS_NAME, logout_icon_class_name)))
assert browser.title == 'My page title'
sign_out_button = browser.find_element_by_class_name(logout_icon_class_name)
sign_out_button.click()
def test_teardown(browser):
browser.close()
browser.quit()
The test times out after clicking on the first button after typing in the email. I got screenshots from the run in CI, and it does seem to be stuck loading (see the Google's progress bar at the top, and the fact that it's more white-ish color), see the screenshot:
I also took a screenshot before clicking on the "Next" button, to show the contrast:
After having spent a long time on this and trying many things, I'm about to give up. Any ideas why this works locally but not in CI environment?
I'm trying to set MutationObserver for observing page mutation while loading.
In order to do that, MutationObserver should be configured before page loading.
With selenium-chromedriver, couldn't find the way to inject JS for such purpose.
I know chrome extension can do that but extensions won't work on headless mode.
That's the problem.
It's possible via the DevTool API by calling Page.addScriptToEvaluateOnNewDocument
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver
import json
def send(driver, cmd, params={}):
resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
url = driver.command_executor._url + resource
body = json.dumps({'cmd': cmd, 'params': params})
response = driver.command_executor._request('POST', url, body)
if response['status']:
raise Exception(response.get('value'))
return response.get('value')
def add_script(driver, script):
send(driver, "Page.addScriptToEvaluateOnNewDocument", {"source": script})
WebDriver.add_script = add_script
# launch Chrome
driver = webdriver.Chrome()
# add a script which will be executed when the page starts loading
driver.add_script("""
if (window.self === window.top) { // if main document
console.log('add script');
}
""")
# load a page
driver.get("https://stackoverflow.com/questions")
We can now use execute_cdp_cmd(cmd, cmd_args) to execute Chrome Devtools Protocol command in Selenium
from selenium import webdriver
driver = webdriver.Chrome()
driver.execute_cdp_cmd(
"Page.addScriptToEvaluateOnNewDocument",
{
"source": """// Your JavaScript here"""
}
)
driver.get("https://stackoverflow.com")
driver.quit()
The argument for "source" is just a string. So you can actually write your script in a .js file (for syntax highlighting) and read it using Python
I have a problem with Selenium that I can't make sense of. Also, I can't find a lot of information about this problem via Google.
My Selenium script performs the following steps:
Log into Facebook.
Go to the list of friend proposals.
Scroll down a few times (in order to load more proposals).
Present all proposals one by one on the console and ask the user whether the friend should be added.
On confirmation, an Action chain is created that moves to the proposal in question and then the add button is clicked.
But the Action chain does not work. I get the following error:
Potential friend name: 'John Doe'
Social context: 'Max Mustermann und 3 weitere gemeinsame Freunde'
Traceback (most recent call last):
File "c:\...\facebook_selenium_minimal.py", line 74, in <module>
main()
File "c:\...\facebook_selenium_minimal.py", line 57, in main
friend_add_button).perform()
File "C:\Python36\lib\site-packages\selenium\webdriver\common\action_chains.py", line 77, in perform
self.w3c_actions.perform()
File "C:\Python36\lib\site-packages\selenium\webdriver\common\actions\action_builder.py", line 76, in perform
self.driver.execute(Command.W3C_ACTIONS, enc)
File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 238, in execute
self.error_handler.check_response(response)
File "C:\Python36\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 193, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: Expected 'id' mouse to be mapped to InputState whose subtype is undefined, got: pointerMove
This is my Selenium script:
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait # available since 2.4.0
from selenium.webdriver.support import expected_conditions as EC # available since 2.26.0
from selenium.webdriver.common.action_chains import ActionChains
TIMEOUT = 5
def main():
driver = webdriver.Firefox()
driver.get("http://www.facebook.com")
print(driver.title)
input_mail = driver.find_element_by_id("email")
input_password = driver.find_element_by_id("pass")
input_mail.send_keys("your_login#example.com")
input_password.send_keys("your_password")
input_password.submit()
try:
WebDriverWait(driver, TIMEOUT).until(
EC.visibility_of_element_located((By.NAME, "requests")))
driver.get("https://www.facebook.com/friends/requests/?fcref=jwl")
WebDriverWait(driver, TIMEOUT).until(
EC.visibility_of_element_located((By.ID, "fbSearchResultsBox")))
# Let Facebook load more friend proposals.
for i in range(2):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(1.0)
friend_proposals = driver.find_elements_by_class_name(
"friendBrowserListUnit")
for friend_proposal in friend_proposals:
try:
friend_title = friend_proposal.find_element_by_class_name(
"friendBrowserNameTitle")
except NoSuchElementException:
print("Title element could not be found. Skipping.")
continue
print("Potential friend name: '%s'" % friend_title.text)
social_context = friend_proposal.find_element_by_class_name(
"friendBrowserSocialContext")
social_context_text = social_context.text
print("Social context: '%s'" % social_context_text)
friend_add_button = friend_proposal.find_element_by_class_name(
"FriendRequestAdd")
actions = ActionChains(driver)
actions.move_to_element(friend_proposal).move_to_element(
friend_add_button).perform()
time.sleep(0.1)
print("Should I add the friend (y/N): ")
response = input()
if response == "y":
friend_add_button.click()
time.sleep(1.0)
print("Added friend...")
except TimeoutException as exc:
print("TimeoutException: " + str(exc))
finally:
driver.quit()
if __name__ == '__main__':
try:
main()
except:
raise
I'm using the latest Selenium version:
C:\Users\Robert>pip show selenium
Name: selenium
Version: 3.3.1
And I have Firefox 52.0.1 with geckodriver v0.15.0.
Update: A quick test revealed that the same script works flawlessly with the Chrome Webdriver.
Update 2: This issue in the Selenium bugtracker on Github might be related: https://github.com/SeleniumHQ/selenium/issues/3642
I ran into the same issue today. You might have observed that the first move_to_element and perform() worked - at least this was true in my case. To repeat this action, you should reset the action chain in your for loop:
actions.perform()
actions.reset_actions()
For me - the .perform fails the first time through - I am on selenium 3.3.1, gecko 15 and latest firefox using java - same code works perfectly on chrome.