Commit d0ed6939 authored by vabr's avatar vabr Committed by Commit bot

[Password manager Python tests] Re-arrange tests

This CL changes the structure of the tests. Instead of many repetitions, there are now just 3 types of tests: failed login, successful login, and autofill test.

The Environment and WebsiteTest classes are also being refactored for clarity.

BUG=369521

Review URL: https://codereview.chromium.org/1026833003

Cr-Commit-Position: refs/heads/master@{#322574}
parent 9973bb23
......@@ -75,10 +75,6 @@ class NewWebsiteTest(WebsiteTest):
self.FillPasswordInto("Password CSS selector")
self.Submit("Password CSS selector")
def Logout(self):
# Add logout steps for the website, for example:
self.Click("Logout button CSS selector")
Then, to create the new test, you need just to add:
environment.AddWebsiteTest(NewWebsiteTest("website name"))
......@@ -149,7 +145,7 @@ manager. When this bug is solved, all the tests that were failing because of
it are going to be moved to working tests.
Other files:
* websites.xml : a private file where you can find all the passwords. You can
* websites.xml: a private file where you can find all the passwords. You can
ask someone to give it to you or just create your own with your personal
accounts.
<websites>
......
......@@ -2,7 +2,12 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The testing Environment class."""
"""The testing Environment class.
It holds the WebsiteTest instances, provides them with credentials,
provides clean browser environment, runs the tests, and gathers the
results.
"""
import os
import shutil
......@@ -13,34 +18,18 @@ from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# Message strings to look for in chrome://password-manager-internals
# Message strings to look for in chrome://password-manager-internals.
MESSAGE_ASK = "Message: Decision: ASK the user"
MESSAGE_SAVE = "Message: Decision: SAVE the password"
class TestResult:
"""Stores the information related to a test result. """
def __init__(self, name, test_type, successful, message):
"""Creates a new TestResult.
Args:
name: The tested website name.
test_type: The test type.
successful: Whether or not the test was successful.
message: The error message of the test.
"""
self.name = name
self.test_type = test_type
self.successful = successful
self.message = message
INTERNALS_PAGE_URL = "chrome://password-manager-internals/"
class Environment:
"""Sets up the testing Environment. """
def __init__(self, chrome_path, chromedriver_path, profile_path,
passwords_path, enable_automatic_password_saving):
"""Creates a new testing Environment.
"""Creates a new testing Environment, starts Chromedriver.
Args:
chrome_path: The chrome binary file.
......@@ -51,6 +40,8 @@ class Environment:
saved without showing the prompt.
Raises:
IOError: When the passwords file cannot be accessed.
ParseError: When the passwords file cannot be parsed.
Exception: An exception is raised if |profile_path| folder could not be
removed.
"""
......@@ -58,39 +49,35 @@ class Environment:
# Cleaning the chrome testing profile folder.
if os.path.exists(profile_path):
shutil.rmtree(profile_path)
options = Options()
self.enable_automatic_password_saving = enable_automatic_password_saving
if enable_automatic_password_saving:
options.add_argument("enable-automatic-password-saving")
# Chrome path.
# TODO(vabr): show_prompt is used in WebsiteTest for asserting that
# Chrome set-up corresponds to the test type. Remove that knowledge
# about Environment from the WebsiteTest.
self.show_prompt = not enable_automatic_password_saving
options.binary_location = chrome_path
# Chrome testing profile path.
options.add_argument("user-data-dir=%s" % profile_path)
# The webdriver. It's possible to choose the port the service is going to
# run on. If it's left to 0, a free port will be found.
self.driver = webdriver.Chrome(chromedriver_path, 0, options)
# The password internals window.
# Password internals page tab/window handle.
self.internals_window = self.driver.current_window_handle
if passwords_path:
# An xml tree filled with logins and passwords.
self.passwords_tree = ElementTree.parse(passwords_path).getroot()
else:
raise Exception("Error: |passwords_path| needs to be provided if"
"|chrome_path| is provided, otherwise the tests could not be run")
# Password internals page.
self.internals_page = "chrome://password-manager-internals/"
# The Website window.
self.website_window = None
# The WebsiteTests list.
# An xml tree filled with logins and passwords.
self.passwords_tree = ElementTree.parse(passwords_path).getroot()
self.website_window = self._OpenNewTab()
self.websitetests = []
# Map messages to the number of their appearance in the log.
self.message_count = { MESSAGE_ASK: 0, MESSAGE_SAVE: 0 }
# The tests needs two tabs to work. A new tab is opened with the first
# GoTo. This is why we store here whether or not it's the first time to
# execute GoTo.
self.first_go_to = True
# List of all tests results.
# A list of (test_name, test_type, test_success, failure_log).
self.tests_results = []
def AddWebsiteTest(self, websitetest):
......@@ -110,248 +97,197 @@ class Environment:
# TODO(vabr): Make driver a property of WebsiteTest.
websitetest.driver = self.driver
if not websitetest.username:
username_tag = (
self.passwords_tree.find(
".//*[@name='%s']/username" % websitetest.name))
username_tag = (self.passwords_tree.find(
".//*[@name='%s']/username" % websitetest.name))
websitetest.username = username_tag.text
if not websitetest.password:
password_tag = (
self.passwords_tree.find(
".//*[@name='%s']/password" % websitetest.name))
password_tag = (self.passwords_tree.find(
".//*[@name='%s']/password" % websitetest.name))
websitetest.password = password_tag.text
self.websitetests.append(websitetest)
def ClearCache(self, clear_passwords):
"""Clear the browser cookies. If |clear_passwords| is true, clear all the
saved passwords too.
def _ClearBrowserDataInit(self):
"""Opens and resets the chrome://settings/clearBrowserData dialog.
Args:
clear_passwords : Clear all the passwords if the bool value is true.
It unchecks all checkboxes, and sets the time range to the "beginning of
time".
"""
self.driver.get("chrome://settings/clearBrowserData")
self.driver.switch_to_frame("settings")
script = (
"if (!document.querySelector('#delete-cookies-checkbox').checked)"
" document.querySelector('#delete-cookies-checkbox').click();"
)
negation = ""
if clear_passwords:
negation = "!"
script += (
"if (%sdocument.querySelector('#delete-passwords-checkbox').checked)"
" document.querySelector('#delete-passwords-checkbox').click();"
% negation)
script += "document.querySelector('#clear-browser-data-commit').click();"
self.driver.execute_script(script)
time_range_selector = "#clear-browser-data-time-period"
# TODO(vabr): Wait until time_range_selector is displayed instead.
time.sleep(2)
# Every time we do something to the cache let's enable password saving.
set_time_range = (
"var range = document.querySelector('{0}');".format(
time_range_selector) +
"range.value = 4" # 4 == the beginning of time
)
self.driver.execute_script(set_time_range)
all_cboxes_selector = (
"#clear-data-checkboxes [type=\"checkbox\"]")
uncheck_all = (
"var checkboxes = document.querySelectorAll('{0}');".format(
all_cboxes_selector ) +
"for (var i = 0; i < checkboxes.length; ++i) {"
" checkboxes[i].checked = false;"
"}"
)
self.driver.execute_script(uncheck_all)
def _ClearDataForCheckbox(self, selector):
"""Causes the data associated with |selector| to be cleared.
Opens chrome://settings/clearBrowserData, unchecks all checkboxes, then
checks the one described by |selector|, then clears the corresponding
browsing data for the full time range.
Args:
selector: describes the checkbox through which to delete the data.
"""
self._ClearBrowserDataInit()
check_cookies_and_submit = (
"document.querySelector('{0}').checked = true;".format(selector) +
"document.querySelector('#clear-browser-data-commit').click();"
)
self.driver.execute_script(check_cookies_and_submit)
def _EnablePasswordSaving(self):
"""Make sure that password manager is enabled."""
# TODO(melandory): We should check why it's off in a first place.
# TODO(melandory): Investigate, maybe there is no need to enable it that
# often.
self.EnablePasswordsSaving()
def EnablePasswordsSaving(self):
self.driver.get("chrome://settings")
self.driver.switch_to_frame("settings")
script = "document.getElementById('advanced-settings-expander').click();"
self.driver.execute_script(script)
# TODO(vabr): Wait until element is displayed instead.
time.sleep(2)
script = (
"if (!document.querySelector('#password-manager-enabled').checked)"
"{ document.querySelector('#password-manager-enabled').click();}")
"document.querySelector('#password-manager-enabled').checked = true;")
self.driver.execute_script(script)
time.sleep(2)
def OpenTabAndGoToInternals(self, url):
"""If there is no |self.website_window|, opens a new tab and navigates to
|url| in the new tab. Navigates to the passwords internals page in the
first tab. Raises an exception otherwise.
Args:
url: Url to go to in the new tab.
def _OpenNewTab(self):
"""Open a new tab, and loads the internals page in the old tab.
Raises:
Exception: An exception is raised if |self.website_window| already
exists.
Returns:
A handle to the new tab.
"""
if self.website_window:
raise Exception("Error: The window was already opened.")
self.driver.get("chrome://newtab")
number_old_tabs = len(self.driver.window_handles)
# There is no straightforward way to open a new tab with chromedriver.
# One work-around is to go to a website, insert a link that is going
# to be opened in a new tab, click on it.
# to be opened in a new tab, and click on it.
self.driver.get("about:blank")
a = self.driver.execute_script(
"var a = document.createElement('a');"
"a.target = '_blank';"
"a.href = arguments[0];"
"a.href = 'about:blank';"
"a.innerHTML = '.';"
"document.body.appendChild(a);"
"return a;",
url)
"return a;")
a.click()
time.sleep(1)
while number_old_tabs == len(self.driver.window_handles):
time.sleep(1) # Wait until the new tab is opened.
self.website_window = self.driver.window_handles[-1]
self.driver.get(self.internals_page)
self.driver.switch_to_window(self.website_window)
new_tab = self.driver.window_handles[-1]
self.driver.get(INTERNALS_PAGE_URL)
self.driver.switch_to_window(new_tab)
return new_tab
def SwitchToInternals(self):
"""Switches from the Website window to internals tab."""
self.driver.switch_to_window(self.internals_window)
def _DidStringAppearUntilTimeout(self, strings, timeout):
"""Checks whether some of |strings| appeared in the current page.
def SwitchFromInternals(self):
"""Switches from internals tab to the Website window."""
self.driver.switch_to_window(self.website_window)
def _DidMessageAppearUntilTimeout(self, log_message, timeout):
"""Checks whether the save password prompt is shown.
Waits for up to |timeout| seconds until at least one of |strings| is
shown in the current page. Updates self.message_count with the current
number of occurrences of the shown string. Assumes that at most
one of |strings| is newly shown.
Args:
log_message: Log message to look for in the password internals.
timeout: There is some delay between the login and the password
internals update. The method checks periodically during the first
|timeout| seconds if the internals page reports the prompt being
shown. If the prompt is not reported shown within the first
|timeout| seconds, it is considered not shown at all.
strings: A list of strings to look for.
timeout: If any such string does not appear within the first |timeout|
seconds, it is considered a no-show.
Returns:
True if the save password prompt is shown.
False otherwise.
True if one of |strings| is observed until |timeout|, False otherwise.
"""
log = self.driver.find_element_by_css_selector("#log-entries")
count = log.text.count(log_message)
if count > self.message_count[log_message]:
self.message_count[log_message] = count
return True
elif timeout > 0:
log = self.driver.find_element_by_css_selector("#log-entries")
while timeout:
for string in strings:
count = log.text.count(string)
if count > self.message_count[string]:
self.message_count[string] = count
return True
time.sleep(1)
return self._DidMessageAppearUntilTimeout(log_message, timeout - 1)
else:
return False
timeout -= 1
return False
def CheckForNewMessage(self, log_message, message_should_show_up,
error_message, timeout=15):
"""Detects whether the save password prompt is shown.
def CheckForNewString(self, strings, string_should_show_up, error):
"""Checks that |strings| show up on the internals page as it should.
Args:
log_message: Log message to look for in the password internals. The
only valid values are the constants MESSAGE_* defined at the
beginning of this file.
message_should_show_up: Whether or not the message is expected to be
shown.
error_message: Error message for the exception.
timeout: There is some delay between the login and the password
internals update. The method checks periodically during the first
|timeout| seconds if the internals page reports the prompt being
shown. If the prompt is not reported shown within the first
|timeout| seconds, it is considered not shown at all.
Raises:
Exception: An exception is raised in case the result does not match the
expectation
"""
if (self._DidMessageAppearUntilTimeout(log_message, timeout) !=
message_should_show_up):
raise Exception(error_message)
def AllTests(self, prompt_test):
"""Runs the tests on all the WebsiteTests.
TODO(vabr): Currently, "all tests" always means one.
Switches to the internals page and looks for a new instances of |strings|
being shown up there. It checks that |string_should_show_up| is true if
and only if at leas one string from |strings| shows up, and throws an
Exception if that check fails.
Args:
prompt_test: If True, tests caring about showing the save-password
prompt are going to be run, otherwise tests which don't care about
the prompt are going to be run.
strings: A list of strings to look for in the internals page.
string_should_show_up: Whether or not at least one string from |strings|
is expected to be shown.
error: Error message for the exception.
Raises:
Exception: An exception is raised if the tests fail.
Exception: (See above.)
"""
if prompt_test:
self.PromptTestList(self.websitetests)
else:
self.TestList(self.websitetests)
def Test(self, tests, prompt_test):
"""Runs the tests on websites named in |tests|.
self.driver.switch_to_window(self.internals_window)
try:
if (self._DidStringAppearUntilTimeout(strings, 15) !=
string_should_show_up):
raise Exception(error)
finally:
self.driver.switch_to_window(self.website_window)
Args:
tests: A list of the names of the WebsiteTests that are going to be
tested.
prompt_test: If True, tests caring about showing the save-password
prompt are going to be run, otherwise tests which don't care about
the prompt are going to be executed.
def DeleteCookies(self):
"""Deletes cookies via the settings page."""
Raises:
Exception: An exception is raised if the tests fail.
"""
websitetests = []
for websitetest in self.websitetests:
if websitetest.name in tests:
websitetests.append(websitetest)
self._ClearDataForCheckbox("#delete-cookies-checkbox")
if prompt_test:
self.PromptTestList(websitetests)
else:
self.TestList(websitetests)
def RunTestsOnSites(self, test_type):
"""Runs the specified test on the known websites.
def TestList(self, websitetests):
"""Runs the tests on the websites in |websitetests|.
Also saves the test results in the environment. Note that test types
differ in their requirements on whether the save password prompt
should be displayed. Make sure that such requirements are consistent
with the enable_automatic_password_saving argument passed to |self|
on construction.
Args:
websitetests: A list of WebsiteTests that are going to be tested.
Raises:
Exception: An exception is raised if the tests fail.
test_type: A test identifier understood by WebsiteTest.run_test().
"""
self.ClearCache(True)
for websitetest in websitetests:
successful = True
error = ""
try:
websitetest.was_run = True
websitetest.WrongLoginTest()
websitetest.SuccessfulLoginTest()
self.ClearCache(False)
websitetest.SuccessfulLoginWithAutofilledPasswordTest()
self.ClearCache(True)
websitetest.SuccessfulLoginTest()
self.ClearCache(True)
except Exception as e:
successful = False
error = e.message
self.tests_results.append(TestResult(websitetest.name, "normal",
successful, error))
self.DeleteCookies()
self._ClearDataForCheckbox("#delete-passwords-checkbox")
self._EnablePasswordSaving()
def PromptTestList(self, websitetests):
"""Runs the prompt tests on the websites in |websitetests|.
Args:
websitetests: A list of WebsiteTests that are going to be tested.
Raises:
Exception: An exception is raised if the tests fail.
"""
self.ClearCache(True)
for websitetest in websitetests:
for websitetest in self.websitetests:
successful = True
error = ""
try:
websitetest.was_run = True
websitetest.PromptTest()
websitetest.RunTest(test_type)
except Exception as e:
successful = False
error = e.message
self.tests_results.append(TestResult(websitetest.name, "prompt",
successful, error))
self.tests_results.append(
(websitetest.name, test_type, successful, error))
def Quit(self):
"""Closes the tests."""
# Close the webdriver.
"""Shuts down the driver."""
self.driver.quit()
......@@ -67,12 +67,11 @@ class TestRunner(object):
# TODO(vabr): Ideally we would replace timeout with something allowing
# calling tests directly inside Python, and working on other platforms.
#
# The website test runs in two passes, each pass has an internal
# The website test runs multiple scenarios, each one has an internal
# timeout of 200s for waiting (see |remaining_time_to_wait| and
# Wait() in websitetest.py). Accounting for some more time spent on
# the non-waiting execution, 300 seconds should be the upper bound on
# the runtime of one pass, thus 600 seconds for the whole test.
self.test_cmd = ["timeout", "600"] + self.test_cmd
# Wait() in websitetest.py). Expecting that not every scenario should
# take 200s, the maximum time allocated for all of them is 300s.
self.test_cmd = ["timeout", "300"] + self.test_cmd
self.logger.log(SCRIPT_DEBUG,
"TestRunner set up for test %s, command '%s', "
......@@ -110,21 +109,26 @@ class TestRunner(object):
def _check_if_test_passed(self):
"""Returns True if and only if the test passed."""
success = False
if os.path.isfile(self.results_path):
with open(self.results_path, "r") as results:
count = 0 # Count the number of successful tests.
# TODO(vabr): Parse the results to make sure all scenarios succeeded
# instead of hard-coding here the number of tests scenarios from
# test.py:main.
NUMBER_OF_TEST_SCENARIOS = 3
passed_scenarios = 0
for line in results:
self.failures.append(line)
count += line.count("successful='True'")
# There is only two tests running for every website: the prompt and
# the normal test. If both of the tests were successful, the tests
# would be stopped for the current website.
self.logger.log(SCRIPT_DEBUG, "Test run of %s: %s",
self.test_name, "pass" if count == 2 else "fail")
if count == 2:
return True
return False
passed_scenarios += line.count("successful='True'")
success = passed_scenarios == NUMBER_OF_TEST_SCENARIOS
if success:
break
self.logger.log(
SCRIPT_DEBUG,
"Test run of {0} succeded: {1}".format(self.test_name, success))
return success
def _run_test(self):
"""Executes the command to run the test."""
......
......@@ -475,7 +475,7 @@ all_tests = {
}
def saveResults(environment_tests_results, environment_save_path):
def SaveResults(environment_tests_results, environment_save_path):
"""Save the test results in an xml file.
Args:
......@@ -488,17 +488,16 @@ def saveResults(environment_tests_results, environment_save_path):
"""
if environment_save_path:
xml = "<result>"
for test_result in environment_tests_results:
xml += ("<test name='%s' successful='%s' type='%s'>%s</test>"
% (test_result.name, str(test_result.successful),
test_result.test_type, test_result.message))
for (name, test_type, success, failure_log) in environment_tests_results:
xml += (
"<test name='{0}' successful='{1}' type='{2}'>{3}</test>".format(
name, success, test_type, failure_log))
xml += "</result>"
with open(environment_save_path, "w") as save_file:
save_file.write(xml)
def RunTest(chrome_path, chromedriver_path, profile_path,
environment_passwords_path, enable_automatic_password_saving,
website_test_name):
environment_passwords_path, website_test_name, test_type):
"""Runs the test for the specified website.
Args:
......@@ -506,8 +505,6 @@ def RunTest(chrome_path, chromedriver_path, profile_path,
chromedriver_path: The chromedriver binary file.
profile_path: The chrome testing profile folder.
environment_passwords_path: The usernames and passwords file.
enable_automatic_password_saving: If True, the passwords are going to be
saved without showing the prompt.
website_test_name: Name of the website to test (refer to keys in
all_tests above).
......@@ -519,26 +516,22 @@ def RunTest(chrome_path, chromedriver_path, profile_path,
fails, or if the website name is not known.
"""
enable_automatic_password_saving = (
test_type == WebsiteTest.TEST_TYPE_SAVE_AND_AUTOFILL)
environment = Environment(chrome_path, chromedriver_path, profile_path,
environment_passwords_path,
enable_automatic_password_saving)
# Test which care about the save-password prompt need the prompt
# to be shown. Automatic password saving results in no prompt.
run_prompt_tests = not enable_automatic_password_saving
if website_test_name in all_tests:
environment.AddWebsiteTest(all_tests[website_test_name])
else:
raise Exception("Test name {} is unknown.".format(website_test_name))
environment.AllTests(run_prompt_tests)
environment.RunTestsOnSites(test_type)
environment.Quit()
return environment.tests_results
# Tests setup.
if __name__ == "__main__":
def main():
parser = argparse.ArgumentParser(
description="Password Manager automated tests help.")
......@@ -568,22 +561,19 @@ if __name__ == "__main__":
if args.save_path:
save_path = args.save_path
# Run the test without enable-automatic-password-saving to check whether or
# not the prompt is shown in the way we expected.
tests_results = RunTest(args.chrome_path,
args.chromedriver_path,
args.profile_path,
args.passwords_path,
False,
args.test)
# Run the test with enable-automatic-password-saving to check whether or not
# the passwords is stored in the the way we expected.
tests_results += RunTest(args.chrome_path,
args.chromedriver_path,
args.profile_path,
args.passwords_path,
True,
args.test)
saveResults(tests_results, save_path)
tests_results = RunTest(
args.chrome_path, args.chromedriver_path, args.profile_path,
args.passwords_path, args.test, WebsiteTest.TEST_TYPE_PROMPT_FAIL)
tests_results += RunTest(
args.chrome_path, args.chromedriver_path, args.profile_path,
args.passwords_path, args.test, WebsiteTest.TEST_TYPE_PROMPT_SUCCESS)
tests_results += RunTest(
args.chrome_path, args.chromedriver_path, args.profile_path,
args.passwords_path, args.test, WebsiteTest.TEST_TYPE_SAVE_AND_AUTOFILL)
SaveResults(tests_results, save_path)
if __name__ == "__main__":
main()
......@@ -12,390 +12,372 @@ from selenium.webdriver.common.keys import Keys
import environment
SCRIPT_DEBUG = 9 # TODO(vabr) -- make this consistent with run_tests.py.
def _IsOneSubstringOfAnother(s1, s2):
"""Checks if one of the string arguements is substring of the other.
Args:
s1: The first string.
s2: The second string.
Returns:
class WebsiteTest:
"""WebsiteTest testing class.
True if one of the string arguements is substring of the other.
False otherwise.
Represents one website, defines some generic operations on that site.
To customise for a particular website, this class needs to be inherited
and the Login() method overridden.
"""
return s1 in s2 or s2 in s1
# Possible values of self.autofill_expectation.
AUTOFILLED = 1 # Expect password and username to be autofilled.
NOT_AUTOFILLED = 2 # Expect password and username not to be autofilled.
class WebsiteTest:
"""Handles a tested WebsiteTest."""
class Mode:
"""Test mode."""
# Password and username are expected to be autofilled.
AUTOFILLED = 1
# Password and username are not expected to be autofilled.
NOT_AUTOFILLED = 2
# The maximal accumulated time to spend in waiting for website UI
# interaction.
MAX_WAIT_TIME_IN_SECONDS = 200
def __init__(self):
pass
# Types of test to be passed to self.RunTest().
TEST_TYPE_PROMPT_FAIL = 1
TEST_TYPE_PROMPT_SUCCESS = 2
TEST_TYPE_SAVE_AND_AUTOFILL = 3
def __init__(self, name, username_not_auto=False):
"""Creates a new WebsiteTest.
Args:
name: The website name.
username_not_auto: Username inputs in some websites (like wikipedia) are
sometimes filled with some messages and thus, the usernames are not
automatically autofilled. This flag handles that and disables us from
checking if the state of the DOM is the same as the username of
website.
name: The website name, identifying it in the test results.
username_not_auto: Expect that the tested website fills username field
on load, and Chrome cannot autofill in that case.
"""
# Name of the website
self.name = name
# Username of the website.
self.username = None
# Password of the website.
self.password = None
# Username is not automatically filled.
self.username_not_auto = username_not_auto
# Autofilling mode.
self.mode = self.Mode.NOT_AUTOFILLED
# The |remaining_time_to_wait| limits the total time in seconds spent in
# potentially infinite loops.
self.remaining_time_to_wait = 200
# The testing Environment.
# Specify, whether it is expected that credentials get autofilled.
self.autofill_expectation = WebsiteTest.NOT_AUTOFILLED
self.remaining_seconds_to_wait = WebsiteTest.MAX_WAIT_TIME_IN_SECONDS
# The testing Environment, if added to any.
self.environment = None
# The webdriver.
# The webdriver from the environment.
self.driver = None
# Whether or not the test was run.
self.was_run = False
# Mouse/Keyboard actions.
def Click(self, selector):
"""Clicks on an element.
"""Clicks on the element described by |selector|.
Args:
selector: The element CSS selector.
selector: The clicked element's CSS selector.
"""
logging.info("action: Click %s" % selector)
self.WaitUntilDisplayed(selector)
element = self.driver.find_element_by_css_selector(selector)
logging.log(SCRIPT_DEBUG, "action: Click %s" % selector)
element = self.WaitUntilDisplayed(selector)
element.click()
def ClickIfClickable(self, selector):
"""Clicks on an element if it's clickable: If it doesn't exist in the DOM,
it's covered by another element or it's out viewing area, nothing is
done and False is returned. Otherwise, even if the element is 100%
transparent, the element is going to receive a click and a True is
returned.
"""Clicks on the element described by |selector| if it is clickable.
The driver's find_element_by_css_selector method defines what is clickable
-- anything for which it does not throw, is clickable. To be clickable,
the element must:
* exist in the DOM,
* be not covered by another element
* be inside the visible area.
Note that transparency does not influence clickability.
Args:
selector: The element CSS selector.
selector: The clicked element's CSS selector.
Returns:
True if the click happens.
True if the element is clickable (and was clicked on).
False otherwise.
"""
logging.info("action: ClickIfVisible %s" % selector)
self.WaitUntilDisplayed(selector)
logging.log(SCRIPT_DEBUG, "action: ClickIfVisible %s" % selector)
element = self.WaitUntilDisplayed(selector)
try:
element = self.driver.find_element_by_css_selector(selector)
element.click()
return True
except Exception:
return False
def GoTo(self, url):
"""Navigates the main frame to the |url|.
"""Navigates the main frame to |url|.
Args:
url: The URL.
url: The URL of where to go to.
"""
logging.info("action: GoTo %s" % self.name)
if self.environment.first_go_to:
self.environment.OpenTabAndGoToInternals(url)
self.environment.first_go_to = False
else:
self.driver.get(url)
logging.log(SCRIPT_DEBUG, "action: GoTo %s" % self.name)
self.driver.get(url)
def HoverOver(self, selector):
"""Hovers over an element.
"""Hovers over the element described by |selector|.
Args:
selector: The element CSS selector.
selector: The CSS selector of the element to hover over.
"""
logging.info("action: Hover %s" % selector)
self.WaitUntilDisplayed(selector)
element = self.driver.find_element_by_css_selector(selector)
logging.log(SCRIPT_DEBUG, "action: Hover %s" % selector)
element = self.WaitUntilDisplayed(selector)
hover = ActionChains(self.driver).move_to_element(element)
hover.perform()
# Waiting/Displaying actions.
def IsDisplayed(self, selector):
"""Returns False if an element doesn't exist in the DOM or is 100%
transparent. Otherwise, returns True even if it's covered by another
element or it's out viewing area.
def _ReturnElementIfDisplayed(self, selector):
"""Returns the element described by |selector|, if displayed.
Note: This takes neither overlapping among elements nor position with
regards to the visible area into account.
Args:
selector: The element CSS selector.
selector: The CSS selector of the checked element.
Returns:
The element if displayed, None otherwise.
"""
logging.info("action: IsDisplayed %s" % selector)
try:
element = self.driver.find_element_by_css_selector(selector)
return element.is_displayed()
return element if element.is_displayed() else None
except Exception:
return False
return None
def IsDisplayed(self, selector):
"""Check if the element described by |selector| is displayed.
Note: This takes neither overlapping among elements nor position with
regards to the visible area into account.
Args:
selector: The CSS selector of the checked element.
Returns:
True if the element is in the DOM and less than 100% transparent.
False otherwise.
"""
logging.log(SCRIPT_DEBUG, "action: IsDisplayed %s" % selector)
return self._ReturnElementIfDisplayed(selector) is not None
def Wait(self, duration):
"""Wait for a duration in seconds. This needs to be used in potentially
infinite loops, to limit their running time.
"""Wait for |duration| in seconds.
To avoid deadlocks, the accummulated waiting time for the whole object does
not exceed MAX_WAIT_TIME_IN_SECONDS.
Args:
duration: The time to wait in seconds.
Raises:
Exception: In case the accummulated waiting limit is exceeded.
"""
logging.info("action: Wait %s" % duration)
logging.log(SCRIPT_DEBUG, "action: Wait %s" % duration)
self.remaining_seconds_to_wait -= duration
if self.remaining_seconds_to_wait < 0:
raise Exception("Waiting limit exceeded for website: %s" % self.name)
time.sleep(duration)
self.remaining_time_to_wait -= 1
if self.remaining_time_to_wait < 0:
raise Exception("Tests took more time than expected for the following "
"website : %s \n" % self.name)
def WaitUntilDisplayed(self, selector, timeout=10):
"""Waits until an element is displayed.
# TODO(vabr): Pull this out into some website-utils and use in Environment
# also?
def WaitUntilDisplayed(self, selector):
"""Waits until the element described by |selector| is displayed.
Args:
selector: The element CSS selector.
timeout: The maximum waiting time in seconds before failing.
selector: The CSS selector of the element to wait for.
Returns:
The displayed element.
"""
if not self.IsDisplayed(selector):
element = self._ReturnElementIfDisplayed(selector)
while not element:
self.Wait(1)
timeout = timeout - 1
if (timeout <= 0):
raise Exception("Error: Element %s not shown before timeout is "
"finished for the following website: %s"
% (selector, self.name))
else:
self.WaitUntilDisplayed(selector, timeout)
element = self._ReturnElementIfDisplayed(selector)
return element
# Form actions.
def FillPasswordInto(self, selector):
"""If the testing mode is the Autofilled mode, compares the website
password to the DOM state.
If the testing mode is the NotAutofilled mode, checks that the DOM state
is empty.
Then, fills the input with the Website password.
"""Ensures that the selected element's value is the saved password.
Depending on self.autofill_expectation, this either checks that the
element already has the password autofilled, or checks that the value
is empty and replaces it with the password.
Args:
selector: The password input CSS selector.
selector: The CSS selector for the filled element.
Raises:
Exception: An exception is raised if the DOM value of the password is
different than the one we expected.
Exception: An exception is raised if the element's value is different
from the expectation.
"""
logging.info("action: FillPasswordInto %s" % selector)
self.WaitUntilDisplayed(selector)
password_element = self.driver.find_element_by_css_selector(selector)
logging.log(SCRIPT_DEBUG, "action: FillPasswordInto %s" % selector)
password_element = self.WaitUntilDisplayed(selector)
# Chrome protects the password inputs and doesn't fill them until
# the user interacts with the page. To be sure that such thing has
# happened we perform |Keys.CONTROL| keypress.
action_chains = ActionChains(self.driver)
action_chains.key_down(Keys.CONTROL).key_up(Keys.CONTROL).perform()
if self.mode == self.Mode.AUTOFILLED:
autofilled_password = password_element.get_attribute("value")
if autofilled_password != self.password:
raise Exception("Error: autofilled password is different from the one "
"we just saved for the following website : %s p1: %s "
"p2:%s \n" % (self.name,
password_element.get_attribute("value"),
self.password))
elif self.mode == self.Mode.NOT_AUTOFILLED:
autofilled_password = password_element.get_attribute("value")
if autofilled_password:
raise Exception("Error: password is autofilled when it shouldn't be "
"for the following website : %s \n"
% self.name)
if self.autofill_expectation == WebsiteTest.AUTOFILLED:
if password_element.get_attribute("value") != self.password:
raise Exception("Error: autofilled password is different from the saved"
" one on website: %s" % self.name)
elif self.autofill_expectation == WebsiteTest.NOT_AUTOFILLED:
if password_element.get_attribute("value"):
raise Exception("Error: password value unexpectedly not empty on"
"website: %s" % self.name)
password_element.send_keys(self.password)
def FillUsernameInto(self, selector):
"""If the testing mode is the Autofilled mode, compares the website
username to the input value. Then, fills the input with the website
"""Ensures that the selected element's value is the saved username.
Depending on self.autofill_expectation, this either checks that the
element already has the username autofilled, or checks that the value
is empty and replaces it with the password. If self.username_not_auto
is true, it skips the checks and just overwrites the value with the
username.
Args:
selector: The username input CSS selector.
selector: The CSS selector for the filled element.
Raises:
Exception: An exception is raised if the DOM value of the username is
different that the one we expected.
Exception: An exception is raised if the element's value is different
from the expectation.
"""
logging.info("action: FillUsernameInto %s" % selector)
self.WaitUntilDisplayed(selector)
username_element = self.driver.find_element_by_css_selector(selector)
if (self.mode == self.Mode.AUTOFILLED and not self.username_not_auto):
if not (username_element.get_attribute("value") == self.username):
raise Exception("Error: autofilled username is different form the one "
"we just saved for the following website : %s \n" %
self.name)
else:
username_element.clear()
username_element.send_keys(self.username)
logging.log(SCRIPT_DEBUG, "action: FillUsernameInto %s" % selector)
username_element = self.WaitUntilDisplayed(selector)
if not self.username_not_auto:
if self.autofill_expectation == WebsiteTest.AUTOFILLED:
if username_element.get_attribute("value") != self.username:
raise Exception("Error: filled username different from the saved"
" one on website: %s" % self.name)
return
if self.autofill_expectation == WebsiteTest.NOT_AUTOFILLED:
if username_element.get_attribute("value"):
raise Exception("Error: username value unexpectedly not empty on"
"website: %s" % self.name)
username_element.clear()
username_element.send_keys(self.username)
def Submit(self, selector):
"""Finds an element using CSS Selector and calls its submit() handler.
"""Finds an element using CSS |selector| and calls its submit() handler.
Args:
selector: The input CSS selector.
selector: The CSS selector for the element to call submit() on.
"""
logging.info("action: Submit %s" % selector)
self.WaitUntilDisplayed(selector)
element = self.driver.find_element_by_css_selector(selector)
logging.log(SCRIPT_DEBUG, "action: Submit %s" % selector)
element = self.WaitUntilDisplayed(selector)
element.submit()
# Login/Logout Methods
# Login/Logout methods
def Login(self):
"""Login Method. Has to be overloaded by the WebsiteTest test."""
"""Login Method. Has to be overridden by the WebsiteTest test."""
raise NotImplementedError("Login is not implemented.")
def LoginWhenAutofilled(self):
"""Logs in and checks that the password is autofilled."""
self.mode = self.Mode.AUTOFILLED
self.autofill_expectation = WebsiteTest.AUTOFILLED
self.Login()
def LoginWhenNotAutofilled(self):
"""Logs in and checks that the password is not autofilled."""
self.mode = self.Mode.NOT_AUTOFILLED
self.autofill_expectation = WebsiteTest.NOT_AUTOFILLED
self.Login()
def Logout(self):
"""Logout Method."""
self.environment.DeleteCookies()
# Tests
# Test scenarios
def WrongLoginTest(self):
"""Does the wrong login test: Tries to login with a wrong password and
checks that the password is not saved.
def PromptFailTest(self):
"""Checks that prompt is not shown on a failed login attempt.
Tries to login with a wrong password and checks that the password
is not offered for saving.
Raises:
Exception: An exception is raised if the test fails: If there is a
problem when performing the login (ex: the login button is not
available ...), if the state of the username and password fields is
not like we expected or if the password is saved.
Exception: An exception is raised if the test fails.
"""
logging.info("\nWrong Login Test for %s \n" % self.name)
try:
correct_password = self.password
# Hardcoded random wrong password. Chosen by fair `pwgen` call.
# For details, see: http://xkcd.com/221/.
self.password = "ChieF2ae"
self.LoginWhenNotAutofilled()
self.password = correct_password
self.Wait(2)
self.environment.SwitchToInternals()
self.environment.CheckForNewMessage(
environment.MESSAGE_SAVE,
False,
"Error: password manager thinks that a login with wrong password was "
"successful for the following website : %s \n" % self.name)
finally:
self.environment.SwitchFromInternals()
def SuccessfulLoginTest(self):
"""Does the successful login when the password is not expected to be
autofilled test: Checks that the password is not autofilled, tries to login
with a right password and checks if the password is saved. Then logs out.
logging.log(SCRIPT_DEBUG, "PromptFailTest for %s" % self.name)
correct_password = self.password
# Hardcoded random wrong password. Chosen by fair `pwgen` call.
# For details, see: http://xkcd.com/221/.
self.password = "ChieF2ae"
self.LoginWhenNotAutofilled()
self.password = correct_password
self.environment.CheckForNewString(
[environment.MESSAGE_ASK, environment.MESSAGE_SAVE],
False,
"Error: did not detect wrong login on website: %s" % self.name)
def PromptSuccessTest(self):
"""Checks that prompt is shown on a successful login attempt.
Tries to login with a correct password and checks that the password
is offered for saving. Chrome cannot have the auto-save option on
when running this test.
Raises:
Exception: An exception is raised if the test fails: If there is a
problem when performing the login and the logout (ex: the login
button is not available ...), if the state of the username and
password fields is not like we expected or if the password is not
saved.
Exception: An exception is raised if the test fails.
"""
logging.info("\nSuccessful Login Test for %s \n" % self.name)
try:
self.LoginWhenNotAutofilled()
self.Wait(2)
self.environment.SwitchToInternals()
self.environment.CheckForNewMessage(
environment.MESSAGE_SAVE,
True,
"Error: password manager hasn't detected a successful login for the "
"following website : %s \n"
% self.name)
finally:
self.environment.SwitchFromInternals()
self.Logout()
def SuccessfulLoginWithAutofilledPasswordTest(self):
"""Does the successful login when the password is expected to be autofilled
test: Checks that the password is autofilled, tries to login with the
autofilled password and checks if the password is saved. Then logs out.
logging.log(SCRIPT_DEBUG, "PromptSuccessTest for %s" % self.name)
if not self.environment.show_prompt:
raise Exception("Switch off auto-save during PromptSuccessTest.")
self.LoginWhenNotAutofilled()
self.environment.CheckForNewString(
[environment.MESSAGE_ASK],
True,
"Error: did not detect login success on website: %s" % self.name)
def SaveAndAutofillTest(self):
"""Checks that a correct password is saved and autofilled.
Tries to login with a correct password and checks that the password
is saved and autofilled on next visit. Chrome must have the auto-save
option on when running this test.
Raises:
Exception: An exception is raised if the test fails: If there is a
problem when performing the login and the logout (ex: the login
button is not available ...), if the state of the username and
password fields is not like we expected or if the password is not
saved.
Exception: An exception is raised if the test fails.
"""
logging.info("\nSuccessful Login With Autofilled Password"
" Test %s \n" % self.name)
try:
self.LoginWhenAutofilled()
self.Wait(2)
self.environment.SwitchToInternals()
self.environment.CheckForNewMessage(
environment.MESSAGE_SAVE,
True,
"Error: password manager hasn't detected a successful login for the "
"following website : %s \n"
% self.name)
finally:
self.environment.SwitchFromInternals()
self.Logout()
def PromptTest(self):
"""Does the prompt test: Tries to login with a wrong password and
checks that the prompt is not shown. Then tries to login with a right
password and checks that the prompt is not shown.
logging.log(SCRIPT_DEBUG, "SaveAndAutofillTest for %s" % self.name)
if self.environment.show_prompt:
raise Exception("Switch off auto-save during PromptSuccessTest.")
self.LoginWhenNotAutofilled()
self.environment.CheckForNewString(
[environment.MESSAGE_SAVE],
True,
"Error: did not detect login success on website: %s" % self.name)
self.Logout()
self.LoginWhenAutofilled()
self.environment.CheckForNewString(
[environment.MESSAGE_SAVE],
True,
"Error: failed autofilled login on website: %s" % self.name)
def RunTest(self, test_type):
"""Runs test according to the |test_type|.
Raises:
Exception: An exception is raised if the test fails: If there is a
problem when performing the login (ex: the login button is not
available ...), if the state of the username and password fields is
not like we expected or if the prompt is not shown for the right
password or is shown for a wrong one.
Exception: If |test_type| is not one of the TEST_TYPE_* constants.
"""
logging.info("\nPrompt Test for %s \n" % self.name)
try:
correct_password = self.password
self.password = self.password + "1"
self.LoginWhenNotAutofilled()
self.password = correct_password
self.Wait(2)
self.environment.SwitchToInternals()
self.environment.CheckForNewMessage(
environment.MESSAGE_ASK,
False,
"Error: password manager thinks that a login with wrong password was "
"successful for the following website : %s \n" % self.name)
self.environment.SwitchFromInternals()
self.LoginWhenNotAutofilled()
self.Wait(2)
self.environment.SwitchToInternals()
self.environment.CheckForNewMessage(
environment.MESSAGE_ASK,
True,
"Error: password manager hasn't detected a successful login for the "
"following website : %s \n" % self.name)
finally:
self.environment.SwitchFromInternals()
if test_type == WebsiteTest.TEST_TYPE_PROMPT_FAIL:
self.PromptFailTest()
elif test_type == WebsiteTest.TEST_TYPE_PROMPT_SUCCESS:
self.PromptSuccessTest()
elif test_type == WebsiteTest.TEST_TYPE_SAVE_AND_AUTOFILL:
self.SaveAndAutofillTest()
else:
raise Exception("Unknown test type {}.".format(test_type))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment