Commit a3eb53ab authored by asredzki's avatar asredzki Committed by Commit bot

Automated Autofill testing library + extension

Adds a new browser automation testing library for Autofill.
The library leverages ChromeDriver to playback tasks composed
of user actions. A action recorder extension is included in order
to assist in the process of creating test tasks.

BUG=

Review-Url: https://codereview.chromium.org/2116583004
Cr-Commit-Position: refs/heads/master@{#407283}
parent 8c62ff08
# Action Recorder Extension
> An extension that generates Python scripts which automate integration testing
> through Chrome. It was primarily designed for testing Autofill but is easily
> portable to other uses.
## Usage
1. Install the extension into Chrome as an unpacked extension on
chrome://extensions (don't forget to turn on "Developer mode" on this page).
2. Navigate to the desired start page or use the extension's dropdown menu
(next to the omnibox) to go to the next "top 100" site.
3. Use the dropdown menu or right-click context menu to start action recording.
4. Proceed to click on page elements to navigate through the desired sequence
of pages.
5. To validate the input field types simply right-click on the inputs and
select the appropriate 'Input Field Type'. Before performing any other
actions, right click on the page and select 'Validate Field Types'.
6. Select 'Stop & Copy', at which point the test code will be in your
clipboard.
7. Paste the generated code into the autofill_top_100.py file
(components/test/data/password_manager/form_classification_tests).
8. Clean up as necessary.
You might also visit arbitrary sites. Just go to a site and start recording
there.
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// global IndentedTextFactory, Type, SetContext, Open, ValidateFields
'use strict';
class ActionSet {
constructor(startingUrl) {
this._startingUrl = this._stripUrl(startingUrl);
this._name = this._getTestName(this._startingUrl);
/**
* A record of the actions taken by a user on a given test (website).
*
* When the user is done with a website, the extension saves the actions to
* a python script that can be used to re-run the action sequence as a test
* suite.
*
* @type {Array}
*/
this._steps = [];
this.addAction(new Open(this._startingUrl));
}
addAction(action) { this._steps.push(action); }
toString() {
const textFactory = new IndentedTextFactory();
// Task class setup
textFactory.addLine(`class ${this._name}(AutofillTask):`);
textFactory.increase();
textFactory.addLine('def _create_script(self):');
textFactory.increase();
textFactory.addLine('self.script = [');
textFactory.increase(2);
for (let i = 0; i < this._steps.length; i++) {
let action = this._steps[i];
if (action instanceof ValidateFields) {
// Trim last character, which is a new line
const actionText = `${action.toString().slice(0, -1)},`;
textFactory.addLines(actionText);
} else {
const actionText = `${action},`;
textFactory.addLine(actionText);
}
}
// Close script array
textFactory.decrease(2);
textFactory.addLine(']');
return textFactory.toString();
}
/**
* Return the name of the test based on the form's url |url|
* (e.g. http://login.example.com/path => test_login_example_com).
*
* @param {String} url The form's url
* @return {String} The test name
*/
_getTestName(url) {
const a = document.createElement('a');
a.href = url;
let splitHostname = a.hostname.split(/[.-]+/);
let hostname = '';
for (var i = 0; i < splitHostname.length; i++) {
let segment = splitHostname[i];
if (i === 0 && segment === 'www') {
continue;
}
hostname += segment.charAt(0).toUpperCase() + segment.slice(1);
}
return `Test${hostname}`;
}
/**
* Removes query and anchor from |url|
* (e.g. https://example.com/path?query=1#anchor => https://example.com/path).
*
* @param {String} url The full url to be processed
* @return {String} The url w/o parameters and anchors
*/
_stripUrl(url) {
const a = document.createElement('a');
a.href = url;
return a.origin + a.pathname;
}
/**
* Remove the specified set of |indiciesToRemove| from the internal action
* array.
*
* The method does the removal in linear time and in place.
*
* Will truncate the internal array by the length of |indiciesToRemove|.
* @param {Array} indiciesToRemove An array of indicies to remove from _steps
*/
_removeIndicies(indiciesToRemove) {
if (!indiciesToRemove || indiciesToRemove.length === 0) {
return;
}
indiciesToRemove.sort((a, b) => a - b);
let removalIndex = 0;
// Jump to first removal
for (var i = indiciesToRemove[0]; i < this._steps.length; i++) {
if (removalIndex < indiciesToRemove.length &&
i === indiciesToRemove[removalIndex]) {
// Undesired element so skip copying it to it's "new" place
removalIndex++;
} else {
this._steps[i - removalIndex] = this._steps[i];
}
}
// Truncate array
this._steps.length -= removalIndex;
}
/**
* Eliminate redundant actions.
*
* Current optimizations:
* - Multi-pass removal of redundant context switching
* - Remove adjacent typing events for same element
*
* Warning: This removes events from the internal action set.
*/
optimize() {
this._optimizeContextSwitching();
this._optimizeTyping();
}
/**
* Remove adjacent typing events for same element.
*
* Example: The following set of actions:
* Type(ByXPath('//*[@id="tbPhone"]'), ''),
* Type(ByXPath('//*[@id="tbPhone"]'), '324'),
* Type(ByXPath('//*[@id="tbPhone"]'), '5603928181'),
*
* will be reduced to the following:
* Type(ByXPath('//*[@id="tbPhone"]'), '5603928181'),
*
* Warning: This removes events from the internal action set.
*/
_optimizeTyping() {
const indiciesToRemove = [];
for (let i = 0; i < this._steps.length - 1; i++) {
const currentAction = this._steps[i];
if (!(currentAction instanceof Type)) {
continue;
}
if (currentAction.isEqual(this._steps[i + 1])) {
// Mark this index for removal
indiciesToRemove.push(i);
console.log(`Removed redundant typing action ${this._steps[i]}`);
}
}
// Remove duplicate indicies
this._removeIndicies(indiciesToRemove);
}
/**
* Multi-pass removal of redundant context switching.
*
* Note: A ContextSwitch action called with None changes to the parent context
*
* Example: The following set of actions:
* Click(ByID('register')),
* SetContext(ByID('overlayRegFrame')),
* Click(ByCssSelector('.regTaEmail')),
* SetContext(None),
* SetContext(ByID('overlayRegFrame')),
* Click(ByCssSelector('div.ui_button.regSubmitBtn')),
* SetContext(None),
* Click(ByCssSelector('.greeting.link')),
*
* will be reduced to the following:
* Click(ByID('register')),
* SetContext(ByID('overlayRegFrame')),
* Click(ByCssSelector('.regTaEmail')),
* Click(ByCssSelector('div.ui_button.regSubmitBtn')),
* SetContext(None),
* Click(ByCssSelector('.greeting.link')),
*
* Warning: This removes events from the internal action set.
*/
_optimizeContextSwitching() {
let hasChanged = true;
while (hasChanged) {
hasChanged = false;
const indiciesToRemove = [];
for (let i = 0; i < this._steps.length - 1; i++) {
const currentAction = this._steps[i];
const nextAction = this._steps[i + 1];
if (!(currentAction instanceof SetContext)) {
console.log(`Skipping ${currentAction}`);
continue;
}
if (currentAction.isEqual(nextAction)) {
console.log(
'Removed redundantly inverse context switching actions' +
`${currentAction} and ${nextAction}`);
// Mark both indicies for removal
indiciesToRemove.push(i++);
indiciesToRemove.push(i);
hasChanged = true;
} else {
console.log(`Not-equal objects ${currentAction} and ${nextAction}`);
}
}
// Remove duplicate indicies
this._removeIndicies(indiciesToRemove);
}
}
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* global IndentedTextFactory */
'use strict';
function escapeSingleQuotes(str) {
return str.replace(/\'/g, '\\\'');
}
function compareArrays(array1, array2) {
if (array1.length !== array2.length) {
return false;
}
for (let i = 0; i < array1.length; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}
return true;
}
class ByXPath {
constructor(xPath) {
if (xPath) {
xPath = escapeSingleQuotes(xPath);
}
this._xPath = xPath || null;
}
/**
* Defines the method used to represent an XPath selector as executable python
* code.
*
* @return {String} Python code representation of the selector class
*/
toString() { return `ByXPath('${this._xPath}')`; }
/**
* Compares the two actions to check whether they are identical.
*
* @param {Action} other Action to compare this against
* @return {Boolean} Whether the actions are identical
*/
isEqual(other) {
return (other instanceof ByXPath) && this._xPath === other._xPath;
}
}
class Action {
/**
* @constructor
* @param {String} type Action type name, also the name of the class
* used in the generated test
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {String} ...args Additional arguments specific to the action
*/
constructor(type, selector, ...args) {
this._type = type;
this._selector = selector;
this._extraArgs = args;
}
/**
* Defines the method used to represent Actions as executable python code.
*
* These stringified actions can be combined in an action set to create
* automated browser integration tests.
*
* @return {String} Python code representation of the Action class
*/
toString() {
let extraArgString = '';
for (let i = 0; i < this._extraArgs.length; i++) {
if (this._extraArgs[i] !== undefined && this._extraArgs[i] !== null) {
extraArgString += `, ${this._extraArgs[i]}`;
}
}
return `${this._type}(${this._selector}${extraArgString})`;
}
/**
* Compares the two actions to check whether they are identical.
*
* @param {Action} other Action to compare this against
* @return {Boolean} Whether the actions are identical
*/
isEqual(other) {
return this._type === other._type &&
this._selector.isEqual(other._selector) &&
compareArrays(this._extraArgs, other._extraArgs);
}
}
class Open extends Action {
/**
* @constructor
* @param {String} url Absolute url to navigate the browser to
*/
constructor(url) {
if (url) {
url = escapeSingleQuotes(url);
}
super('Open', `'${url}'`);
}
}
class SetContext extends Action {
/**
* @constructor
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {Boolean} ignorable Whether the test can proceed if the action fails
* @param {ByXPath} inverse If this selector is 'None', then this property is
* the selector for the context that it is returning
* 'out' of (used to reduce redundant actions)
*/
constructor(selector, ignorable, inverse) {
super('SetContext', selector, ignorable);
this._inverse = inverse;
}
/**
* Compares the two SetContext actions and evaluates whether they cancel
* eachother (and thus can be both removed with no net effect).
*
* @param {Action} other Action to compare this against
* @return {Boolean} Whether the two actions can be removed safely
*/
isEqual(other) {
if (this._type !== other._type) {
return false;
}
if (this._selector === 'None') {
return this._inverse.isEqual(other._selector);
} else if (other._selector === 'None') {
return this._selector.isEqual(other._inverse);
}
return false;
}
}
class Type extends Action {
/**
* @constructor
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {String} text Content to fill the input field with
* @param {Boolean} ignorable Whether the test can proceed if the action fails
* @param {Boolean*} rawText Whether the text should be printed as is. This
* is useful for injecting helper functions
* (ex. GenEmail())
*/
constructor(selector, text, ignorable, rawText) {
text = rawText ? text : `'${text}'`;
super('Type', selector, text, ignorable);
}
/**
* Compares the two typing actions.
*
* Note: even if the text of the two actions is different, they are
* considered as equal as long as they target the same object.
*
* @param {Action} other Action to compare this against
* @return {Boolean} Whether the first of the two actions is redundant
*/
isEqual(other) {
return this._type === other._type &&
this._selector.isEqual(other._selector);
}
}
class Select extends Action {
/**
* @constructor
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {String} value Value of the option to select
* @param {Boolean} ignorable Whether the test can proceed if the action fails
* @param {Boolean*} byLabel Whether |value| represents the option's label.
* This is useful for improving the reliability of
* an action if the value is less stable than the
* human readable form
*/
constructor(selector, value, ignorable, byLabel) {
super('Select', selector, `'${value}'`, ignorable, byLabel);
}
}
class Click extends Action {
/**
* @constructor
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {Boolean} ignorable Whether the test can proceed if the action fails
*/
constructor(selector, ignorable) { super('Click', selector, ignorable); }
}
class TriggerAutofill extends Action {
/**
* @constructor
* Will attempt to trigger autofill using keystrokes with a form field in
* focus.
* @param {ByXPath} selector An instance of a valid selector which will be
* used to find the element to perform the action on
* @param {Boolean} ignorable Whether the test can proceed if the action fails
*/
constructor(selector, ignorable) {
super('TriggerAutofill', selector, ignorable);
}
}
class TypedField {
constructor(selector, fieldType) {
this._selector = selector;
this._fieldType = fieldType;
}
toString() { return `(${this._selector}, '${this._fieldType}')`; }
}
class ValidateFields {
constructor(fields) { this._fields = fields; }
toString() {
const textFactory = new IndentedTextFactory();
textFactory.addLine('ValidateFields([');
textFactory.increase(2);
for (let i = 0; i < this._fields.length; i++) {
textFactory.addLine(`${this._fields[i]},`);
}
textFactory.decrease(2);
textFactory.addLine('])');
return textFactory.toString();
}
isEqual() { return false; }
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const FIELD_TYPES = [
'NONE',
'NO_SERVER_DATA',
'UNKNOWN_TYPE',
'EMPTY_TYPE',
'NAME_FIRST',
'NAME_MIDDLE',
'NAME_LAST',
'NAME_MIDDLE_INITIAL',
'NAME_FULL',
'NAME_SUFFIX',
'EMAIL_ADDRESS',
'PHONE_HOME_NUMBER',
'PHONE_HOME_CITY_CODE',
'PHONE_HOME_COUNTRY_CODE',
'PHONE_HOME_CITY_AND_NUMBER',
'PHONE_HOME_WHOLE_NUMBER',
'PHONE_FAX_NUMBER',
'PHONE_FAX_CITY_CODE',
'PHONE_FAX_COUNTRY_CODE',
'PHONE_FAX_CITY_AND_NUMBER',
'PHONE_FAX_WHOLE_NUMBER',
'ADDRESS_HOME_LINE1',
'ADDRESS_HOME_LINE2',
'ADDRESS_HOME_APPT_NUM',
'ADDRESS_HOME_CITY',
'ADDRESS_HOME_STATE',
'ADDRESS_HOME_ZIP',
'ADDRESS_HOME_COUNTRY',
'ADDRESS_BILLING_LINE1',
'ADDRESS_BILLING_LINE2',
'ADDRESS_BILLING_APPT_NUM',
'ADDRESS_BILLING_CITY',
'ADDRESS_BILLING_STATE',
'ADDRESS_BILLING_ZIP',
'ADDRESS_BILLING_COUNTRY',
'CREDIT_CARD_NAME',
'CREDIT_CARD_NUMBER',
'CREDIT_CARD_EXP_MONTH',
'CREDIT_CARD_EXP_2_DIGIT_YEAR',
'CREDIT_CARD_EXP_4_DIGIT_YEAR',
'CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR',
'CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR',
'CREDIT_CARD_TYPE',
'CREDIT_CARD_VERIFICATION_CODE',
'COMPANY_NAME',
'FIELD_WITH_DEFAULT_VALUE',
'PHONE_BILLING_NUMBER',
'PHONE_BILLING_CITY_CODE',
'PHONE_BILLING_COUNTRY_CODE',
'PHONE_BILLING_CITY_AND_NUMBER',
'PHONE_BILLING_WHOLE_NUMBER',
'NAME_BILLING_FIRST',
'NAME_BILLING_MIDDLE',
'NAME_BILLING_LAST',
'NAME_BILLING_MIDDLE_INITIAL',
'NAME_BILLING_FULL',
'NAME_BILLING_SUFFIX',
'MERCHANT_EMAIL_SIGNUP',
'MERCHANT_PROMO_CODE',
'PASSWORD',
'ACCOUNT_CREATION_PASSWORD',
'ADDRESS_HOME_STREET_ADDRESS',
'ADDRESS_BILLING_STREET_ADDRESS',
'ADDRESS_HOME_SORTING_CODE',
'ADDRESS_BILLING_SORTING_CODE',
'ADDRESS_HOME_DEPENDENT_LOCALITY',
'ADDRESS_BILLING_DEPENDENT_LOCALITY',
'ADDRESS_HOME_LINE3',
'ADDRESS_BILLING_LINE3',
'NOT_ACCOUNT_CREATION_PASSWORD',
'USERNAME',
'USERNAME_AND_EMAIL_ADDRESS',
'NEW_PASSWORD',
'PROBABLY_NEW_PASSWORD',
'NOT_NEW_PASSWORD'
];
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
class IndentedTextFactory {
/**
* Create an intented text factory, optionally with initial indentation level.
* @param {Number} indent The initial number of indents (must be natural)
* @param {String} spaces (Optional) Spacing to use for an indent
*/
constructor(indent, spaces) {
this._indent = indent || 0;
this._spaces = spaces || ' ';
this._contents = '';
}
/**
* Increase indent level.
* @param {Number} steps The number of indents to increase by
*/
increase(steps) { this._indent += (steps || 1); }
/**
* Decrease indent level.
* @param {Number} steps The number of indents to decrease by
*/
decrease(steps) { this._indent = (this._indent - (steps || 1)) || 0; }
/**
* Add a single line of text to the internal content.
* @param {String} text A single line of text
*/
addLine(text) {
let indents = '';
// Add indentation
for (let i = 0; i < this._indent; i++) {
indents += this._spaces;
}
this._contents += `${indents}${text}\n`;
}
/**
* Add a multiline string of text to the internal content.
* Each line will receive the current level of indentation.
* @param {String} multilineText A multi-line string of text
*/
addLines(multilineText) {
const lines = multilineText.split('\n');
for (let i = 0; i < lines.length; i++) {
this.addLine(lines[i]);
}
}
toString() { return this._contents; }
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The list of sites to generate Python tests for.
const SITES_TO_VISIT = [
'www.lowes.ca', 'Abercrombie.com', 'www.GameStop.com', 'Lowes.com',
'Newegg.com', 'Costco.com', 'Ikea.com', 'Etsy.com', 'LuluLemon.com',
'Williams-Sonoma.com'
];
{
"manifest_version": 2,
"name": "Action Recorder Extension",
"description": "Extension to record user actions and generate autofill tests from them",
"version": "1.0.0",
"minimum_chrome_version": "50",
"icons": {
"16": "icons/icon_idle16.png",
"48": "icons/icon_idle48.png",
"128": "icons/icon_idle128.png"
},
"browser_action": {
"default_icon": {
"16": "icons/icon_idle16.png",
"32": "icons/icon_idle32.png"
},
"default_title": "Action Recorder",
"default_popup": "popup/popup.html"
},
"background": {
"scripts": [
"background/sites_to_visit.js",
"background/field_types.js",
"background/indented_string_factory.js",
"background/actions.js",
"background/action_set.js",
"background/action_recorder.js"
],
"persistent": true
},
"content_scripts": [{
"js": [
"content/x_path_tools.js",
"content/action_handler.js"
],
"matches": ["<all_urls>"],
"all_frames": true
}],
"permissions": [
"clipboardWrite",
"tabs",
"notifications",
"contextMenus",
"webNavigation"
]
}
<!doctype html>
<html>
<head>
<title>Action Recorder Extension</title>
<style>
body {
overflow: hidden;
margin: 0px;
padding: 0px;
background: white;
}
div:first-child {
margin-top: 0px;
}
div {
cursor: pointer;
text-align: center;
padding: 5px;
font-family: sans-serif;
font-size: 1em;
width: 100px;
margin-top: 1px;
background: #f5f5f5;
border: #c8c8c8 1px solid;
}
div:hover {
background: #e8e8e8;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="start" class="hidden">Start</div>
<div id="next-site" class="hidden">Next Site</div>
<div id="stop" class="hidden">Stop &amp; Copy</div>
<div id="cancel" class="hidden">Cancel</div>
<script src="popup.js"></script>
</body>
</html>
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
class PopupController {
constructor() {
this._startButton = document.getElementById('start');
this._nextSiteButton = document.getElementById('next-site');
this._stopButton = document.getElementById('stop');
this._cancelButton = document.getElementById('cancel');
this._startListeners();
this._getRecordingState();
}
startRecording() {
chrome.runtime.sendMessage({
type: 'start-recording'
},
(response) => this._handleRecordingResponse(response));
}
stopRecording() {
chrome.runtime.sendMessage({
type: 'stop-recording'
},
(response) => this._handleRecordingResponse(response));
}
cancelRecording() {
chrome.runtime.sendMessage({
type: 'cancel-recording'
},
(response) => this._handleRecordingResponse(response));
}
nextSite() {
chrome.runtime.sendMessage({
type: 'next-site'
});
}
_getRecordingState() {
chrome.runtime.sendMessage({
type: 'recording-state-request'
},
(response) => this._handleRecordingResponse(response));
}
_handleRecordingResponse(response) {
if (!response) {
return;
}
this._setRecordingState(!!response.isRecording);
}
_startListeners() {
this._startButton.addEventListener(
'click', (event) => {
this.startRecording();
});
this._stopButton.addEventListener(
'click', (event) => {
this.stopRecording();
});
this._cancelButton.addEventListener(
'click', (event) => {
this.cancelRecording();
});
this._nextSiteButton.addEventListener(
'click', (event) => {
this.nextSite();
});
}
_setRecordingState(isRecording) {
this._isRecording = isRecording;
this._updateStyling();
}
_updateStyling() {
let shownButton1, shownButton2, hiddenButton1, hiddenButton2;
if (this._isRecording) {
shownButton1 = this._stopButton;
shownButton2 = this._cancelButton;
hiddenButton1 = this._startButton;
hiddenButton2 = this._nextSiteButton;
} else {
shownButton1 = this._startButton;
shownButton2 = this._nextSiteButton;
hiddenButton1 = this._stopButton;
hiddenButton2 = this._cancelButton;
}
this._removeClass(shownButton1, 'hidden');
this._removeClass(shownButton2, 'hidden');
this._applyClass(hiddenButton1, 'hidden');
this._applyClass(hiddenButton2, 'hidden');
chrome.browserAction.setIcon({
path: this._getIconUrl()
});
}
_removeClass(element, className) {
if (element.classList.contains(className)) {
element.classList.remove(className);
}
}
_applyClass(element, className) {
element.classList.add(className);
}
_getIconUrl() {
const iconUrlPrefix = '../icons/icon_' +
(this._isRecording ? 'recording' : 'idle');
return {
'16': iconUrlPrefix + '16.png',
'32': iconUrlPrefix + '32.png'
};
}
}
document.addEventListener('DOMContentLoaded', function() {
const popupController = new PopupController();
});
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Autofill task automation library.
"""
import abc
# Local imports
from .soft_task import SoftTask
class AutofillTask(SoftTask):
"""Extendable autofill task that provides soft/hard assertion functionality.
The task consists of a script (list of Actions) that are to be executed when
run.
Attributes:
profile_data: Dict of profile data that acts as the master source for
validating autofill behaviour.
debug: Whether debug output should be printed (False if not specified).
"""
script = []
def __init__(self, profile_data, debug=False):
super(AutofillTask, self).__init__()
self._profile_data = profile_data
self._debug = debug
def __str__(self):
return self.__class__.__name__
@abc.abstractmethod
def _create_script(self):
"""Creates a script (list of Actions) to execute.
Note: Subclasses must implement this method.
Raises:
NotImplementedError: Subclass did not implement the method.
"""
raise NotImplementedError()
def set_up(self):
"""Sets up the task by creating the action script.
Raises:
NotImplementedError: Subclass did not implement _create_script()
"""
self._create_script()
def tear_down(self):
"""Tears down the task running environment.
Any persistent changes made by set_up() must be reversed here.
"""
pass
def run(self, driver):
"""Sets up, attempts to execute the task, and always tears down.
Args:
driver: ChromeDriver instance to use for action execution.
Raises:
Exception: Task execution failed.
"""
self._driver = driver
super(AutofillTask, self).run()
def _run_task(self):
"""Executes the script defined by the subclass.
Raises:
Exception: Script execution failed.
"""
for step in self.script:
step.Apply(self._driver, self, self._debug)
self.assert_expectations()
def profile_data(self, field):
if field in self._profile_data:
return self._profile_data[field]
else:
return ''
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
class ExpectationFailure(Exception):
"""Represents an unsatisfied expectation.
"""
def __init__(self, *args,**kwargs):
super(ExpectationFailure, self).__init__(*args, **kwargs)
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from random import choice
from string import ascii_lowercase
class Generator(object):
"""A string generator utility.
"""
def __init__(self):
super(Generator, self).__init__()
@staticmethod
def _lower_case_string(length=8):
return ''.join(choice(ascii_lowercase) for i in range(length))
@staticmethod
def email():
"""Generates a fake email address.
Format: 8 character string at an 8 character .com domain name
Returns: The generated email address string.
"""
return '%s@%s.com' % (Generator._lower_case_string(),
Generator._lower_case_string())
@staticmethod
def password():
"""Generates a fake password.
Format: 8 character lowercase string plus 'A!234&'
The postpended string exists to assist in satisfying common "secure
password" requirements
Returns: The generated password string.
"""
return 'A!234&%s' % Generator._lower_case_string()
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import abc
import inspect
import os.path
import sys
# Local Imports
from .exceptions import ExpectationFailure
class SoftTask(object):
"""An extendable base task that provides soft/hard assertion functionality.
"""
__metaclass__ = abc.ABCMeta
def __init__(self):
self._failed_expectations = []
@abc.abstractmethod
def set_up(self):
"""Sets up the task running environment.
Note: Subclasses must implement this method.
Raises:
NotImplementedError: Subclass did not implement the method
"""
raise NotImplementedError()
@abc.abstractmethod
def tear_down(self):
"""Tears down the task running environment.
Any persistent changes made by set_up() must be reversed here.
Note: Subclasses must implement this method.
Raises:
NotImplementedError: Subclass did not implement the method
"""
raise NotImplementedError()
@abc.abstractmethod
def _run_task(self):
"""Executes the task.
Note: Subclasses must implement this method.
Raises:
NotImplementedError: Subclass did not implement the method
"""
raise NotImplementedError()
def run(self):
"""Sets up, attempts to execute the task, and always tears down.
Raises:
ExpectationFailure: Task execution failed. Contains a
NotImplementedError: Subclass did not implement the abstract methods
"""
self.set_up()
try:
self._run_task()
finally:
# Attempt to tear down despite test failure
self.tear_down()
def expect_expression(self, expr, msg=None, stack=None):
"""Verifies an expression, logging a failure, but not abort the test.
In order to ensure that none of the expected expressions have failed, one
must call assert_expectations before the end of the test (which will fail
it if any expectations were not met).
"""
if not expr:
self._log_failure(msg, stack=stack)
def assert_expression(self, expr, msg=None, stack=None):
"""Perform a hard assertion but include failures from soft expressions too.
Raises:
ExpectationFailure: This or previous assertions did not pass. Contains a
failure report.
"""
if not expr:
self._log_failure(msg, stack=stack)
if self._failed_expectations:
raise ExpectationFailure(self._report_failures())
def assert_expectations(self):
"""Raise an assert if there were any failed expectations.
Raises:
ExpectationFailure: An expectation was not met. Contains a failure report.
"""
if self._failed_expectations:
raise ExpectationFailure(self._report_failures())
def _log_failure(self, msg=None, frames=7, skip_frames=0, stack=None):
"""Generates a failure report from the current traceback.
The generated failure report is added to an internal list. The reports
are used by _report_failures
Note: Since this is always called internally a minimum of two frames are
dropped unless you provide a specific stack trace.
Args:
msg: Description of the failure.
frames: The number of frames to include from the call to this function.
skip_frames: The number of frames to skip. Useful if you have helper
functions that you don't desire to be part of the trace.
stack: A custom traceback to use instead of one from this function. No
frames will be skipped by default.
"""
if msg:
failure_message = msg + '\n'
else:
failure_message = '\n'
if stack is None:
stack = inspect.stack()[skip_frames + 2:]
else:
stack = stack[skip_frames:]
frames_to_use = min(frames, len(stack))
stack = stack[:frames_to_use]
for frame in stack:
# First two frames are from logging
if len(frame) == 4:
# From the traceback module
(filename, line, function_name, context) = frame
context = ' %s\n' % context
else:
# Stack trace from the inspect module
(filename, line, function_name, context_list) = frame[1:5]
context = context_list[0]
filename = os.path.basename(filename)
failure_message += ' File "%s", line %s, in %s()\n%s' % (filename, line,
function_name,
context)
self._failed_expectations.append(failure_message)
def _report_failures(self, frames=1, skip_frames=0):
"""Generates a failure report for all failed expectations.
Used in exception descriptions.
Note: Since this is always called internally a minimum of two frames are
dropped.
Args:
frames: The number of frames to include from the call to this function.
skip_frames: The number of frames to skip. Useful if you have helper
functions that you don't desire to be part of the trace.
Returns:
A string representation of all failed expectations.
"""
if self._failed_expectations:
# First two frames are from logging
(filename, line, function_name) = inspect.stack()[skip_frames + 2][1:4]
filename = os.path.basename(filename)
report = [
'Failed Expectations: %s\n' % len(self._failed_expectations),
'assert_expectations() called from',
'"%s" line %s, in %s()\n' % (filename, line, function_name)
]
for i, failure in enumerate(self._failed_expectations, start=1):
report.append('Expectation %d: %s' % (i, failure))
self._failed_expectations = []
return '\n'.join(report)
else:
return ''
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import sys
import unittest
from selenium.common.exceptions import TimeoutException
# Local Imports
from autofill_task.exceptions import ExpectationFailure
from .flow import AutofillTestFlow
class AutofillTestCase(unittest.TestCase):
"""Wraps a single autofill test flow for use with the unittest library.
task_class: AutofillTask to use for the test.
profile: Dict of profile data that acts as the master source for
validating autofill behaviour.
debug: Whether debug output should be printed (False if not specified).
"""
def __init__(self, task_class, user_data_dir, profile, chrome_binary=None,
debug=False):
super(AutofillTestCase, self).__init__('run')
self._flow = AutofillTestFlow(task_class, profile, debug=debug)
self._user_data_dir = user_data_dir
self._chrome_binary = chrome_binary
self._debug = debug
def __str__(self):
return str(self._flow)
def run(self, result):
result.startTest(self)
try:
self._flow.run(self._user_data_dir, chrome_binary=self._chrome_binary)
except KeyboardInterrupt:
raise
except (TimeoutException, ExpectationFailure):
result.addFailure(self, sys.exc_info())
except:
result.addError(self, sys.exc_info())
else:
result.addSuccess(self)
finally:
result.stopTest(self)
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Chrome Autofill Test Flow
Execute a set of autofill tasks in a fresh ChromeDriver instance that has been
pre-loaded with some default profile.
Requires:
- Selenium python bindings
http://selenium-python.readthedocs.org/
- ChromeDriver
https://sites.google.com/a/chromium.org/chromedriver/downloads
The ChromeDriver executable must be available on the search PATH.
- Chrome (>= 53)
"""
# Local Imports
from task_flow import TaskFlow
class AutofillTestFlow(TaskFlow):
"""Represents an executable set of Autofill Tasks.
Note: currently the test flows consist of a single AutofillTask
Used for automated autofill integration testing.
Attributes:
task_class: AutofillTask to use for the test.
profile: Dict of profile data that acts as the master source for
validating autofill behaviour.
debug: Whether debug output should be printed (False if not specified).
"""
def __init__(self, task_class, profile, debug=False):
self._task_class = task_class
super(AutofillTestFlow, self).__init__(profile, debug)
def _generate_task_sequence(self):
"""Generates a set of executable tasks that will be run in ChromeDriver.
Returns:
A list of AutofillTask instances that are to be run in ChromeDriver.
These tasks are to be run in order.
"""
task = self._task_class(self._profile, self._debug)
return [task]
def __str__(self):
if self._tasks:
return 'Autofill Test Flow using \'%s\'' % self._tasks[0]
else:
return 'Empty Autofill Test Flow'
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
class AutofillTestResult(unittest.TextTestResult):
"""A test result class that can print formatted text results to a stream.
Used by AutofillTestRunner.
"""
def startTest(self, test):
"""Called when a test is started.
"""
super(unittest.TextTestResult, self).startTest(test)
if self.showAll:
self.stream.write('Running ')
self.stream.write(self.getDescription(test))
self.stream.write('\n')
self.stream.flush()
def addFailure(self, test, err):
"""Logs a test failure as part of the specified test.
Overloaded to not include the stack trace.
Args:
err: A tuple of values as returned by sys.exc_info().
"""
err = (None, err[1], None)
# self.failures.append((test, str(exception)))
# self._mirrorOutput = True
super(AutofillTestResult, self).addFailure(test, err)
class AutofillTestRunner(unittest.TextTestRunner):
"""An autofill test runner class that displays results in textual form.
It prints out the names of tests as they are run, errors as they
occur, and a summary of the results at the end of the test run.
"""
resultclass = AutofillTestResult
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Chrome Autofill Test Flow
Execute a set of autofill tasks in a fresh ChromeDriver instance that has been
pre-loaded with some default profile.
Requires:
- Selenium python bindings
http://selenium-python.readthedocs.org/
- ChromeDriver
https://sites.google.com/a/chromium.org/chromedriver/downloads
The ChromeDriver executable must be available on the search PATH.
- Chrome
"""
import importlib
import unittest
# Local Imports
from autofill_task.autofill_task import AutofillTask
from testdata import profile_data
from .case import AutofillTestCase
class AutofillTestSuite(unittest.TestSuite):
"""Represents an aggregation of individual Autofill test cases.
Attributes:
user_data_dir: Path string for the writable directory in which profiles
should be stored.
chrome_binary: Path string to the Chrome binary that should be used by
ChromeDriver.
If None then it will use the PATH to find a binary.
test_class: Name of the test class that should be run.
If this is set, then only the specified class will be executed
module: The module to load test cases from. This is relative to the tasks
package.
profile: Dict of profile data that acts as the master source for
validating autofill behaviour. If not specified then default profile data
will be used from testdata.profile_data.
debug: Whether debug output should be printed (False if not specified).
"""
def __init__(self, user_data_dir, chrome_binary=None, test_class=None,
module='sites', profile=None, debug=False):
if profile is None:
profile = profile_data.DEFAULT
super(AutofillTestSuite, self).__init__()
self._test_class = test_class
self._profile = profile
self._debug = debug
module = 'tasks.%s' % module
try:
importlib.import_module(module)
except ImportError:
print 'Unable to load %s from tasks.' % module
raise
self._generate_tests(user_data_dir, chrome_binary)
def _generate_tests(self, user_data_dir, chrome_binary=None):
task_classes = AutofillTask.__subclasses__()
tests = []
if self._test_class:
for task in task_classes:
if task.__name__ == self._test_class:
test = AutofillTestCase(task, user_data_dir, self._profile,
chrome_binary=chrome_binary,
debug=self._debug)
self.addTest(test)
return
raise ValueError('Autofill Test \'%s\' could not be found.' %
self._test_class)
else:
for task in task_classes:
tests.append(AutofillTestCase(task, user_data_dir, self._profile,
chrome_binary=chrome_binary,
debug=self._debug))
self.addTests(tests)
#!/usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Autofill automated integration test runner
Allows you to run integration test(s) for Autofill.
At this time only a limited set of websites are supported.
Requires:
- Selenium python bindings
http://selenium-python.readthedocs.org/
- ChromeDriver
https://sites.google.com/a/chromium.org/chromedriver/downloads
The ChromeDriver executable must be available on the search PATH.
- Chrome (>= 53)
- Write access to '/var/google/autofill/chrome_user_data'
Instructions:
- Add tests to tasks/sites.py (or a new module in tasks/)
- Run main.py -h to view the available flags.
- All tests in tasks/sites.py will be run in the default chrome binary if
no flags are specified.
"""
import argparse
import types
import sys
# Local Imports
from autofill_test.suite import AutofillTestSuite
from autofill_test.runner import AutofillTestRunner
USER_DATA_DIR = '/var/google/autofill/chrome_user_data'
def parse_args():
description = 'Allows you to run integration test(s) for Autofill.'
epilog = ('All tests in tasks/sites.py will be run in the default chrome '
'binary if no flags are specified. At this time only a limited '
'set of websites are supported.')
parser = argparse.ArgumentParser(description=description, epilog=epilog)
parser.add_argument('--user-data-dir', dest='user_data_dir', metavar='PATH',
default=USER_DATA_DIR, help='chrome user data directory')
parser.add_argument('--chrome-binary', dest='chrome_binary', metavar='PATH',
default=None, help='chrome binary location')
parser.add_argument('--module', default='sites', help='task module name')
parser.add_argument('--test', dest='test_class',
help='name of a specific test to run')
parser.add_argument('-d', '--debug', action='store_true', default=False,
help='print additional information, useful for debugging')
args = parser.parse_args()
args.debug = bool(args.debug)
return args
def run(args):
if args.debug:
print 'Running with arguments: %s' % vars(args)
try:
test_suite = AutofillTestSuite(args.user_data_dir,
chrome_binary=args.chrome_binary,
test_class=args.test_class,
module=args.module, debug=args.debug)
verbosity = 2 if args.debug else 1
runner = AutofillTestRunner(verbosity=verbosity)
runner.run(test_suite)
except ImportError as e:
print 'Test Execution failed. %s' % str(e)
except Exception as e:
raise
if __name__ == '__main__':
args = parse_args()
run(args)
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Chrome Autofill Task Flow
Execute a set of autofill tasks in a fresh ChromeDriver instance that has been
pre-loaded with some default profile.
Requires:
- Selenium python bindings
http://selenium-python.readthedocs.org/
- ChromeDriver
https://sites.google.com/a/chromium.org/chromedriver/downloads
The ChromeDriver executable must be available on the search PATH.
- Chrome
"""
import abc
from urlparse import urlparse
import os
import shutil
from random import choice
from string import ascii_lowercase
from selenium import webdriver
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.chrome.options import Options
class TaskFlow(object):
"""Represents an executable set of Autofill Tasks.
Attributes:
profile: Dict of profile data that acts as the master source for
validating autofill behaviour.
debug: Whether debug output should be printed (False if not specified).
"""
__metaclass__ = abc.ABCMeta
def __init__(self, profile, debug=False):
self.set_profile(profile)
self._debug = debug
self._running = False
self._tasks = self._generate_task_sequence()
def set_profile(self, profile):
"""Validates |profile| before assigning it as the source of user data.
Args:
profile: Dict of profile data that acts as the master source for
validating autofill behaviour.
Raises:
ValueError: The |profile| dict provided is missing required keys
"""
if not isinstance(profile, dict):
raise ValueError('profile must be a a valid dictionary');
self._profile = profile
def run(self, user_data_dir, chrome_binary=None):
"""Generates and executes a sequence of chrome driver tasks.
Args:
user_data_dir: Path string for the writable directory in which profiles
should be stored.
chrome_binary: Path string to the Chrome binary that should be used by
ChromeDriver.
If None then it will use the PATH to find a binary.
Raises:
RuntimeError: Running the TaskFlow was attempted while it's already
running.
Exception: Any failure encountered while running the tests
"""
if self._running:
raise RuntimeError('Cannot run TaskFlow when already running')
self._running = True
self._run_tasks(user_data_dir, chrome_binary=chrome_binary)
self._running = False
@abc.abstractmethod
def _generate_task_sequence(self):
"""Generates a set of executable tasks that will be run in ChromeDriver.
Note: Subclasses must implement this method.
Raises:
NotImplementedError: Subclass did not implement the method
Returns:
A list of AutofillTask instances that are to be run in ChromeDriver.
These tasks are to be run in order.
"""
raise NotImplementedError()
def _run_tasks(self, user_data_dir, chrome_binary=None):
"""Runs the internal set of tasks in a fresh ChromeDriver instance.
Args:
user_data_dir: Path string for the writable directory in which profiles
should be stored.
chrome_binary: Path string to the Chrome binary that should be used by
ChromeDriver.
If None then it will use the PATH to find a binary.
Raises:
Exception: Any failure encountered while running the tests
"""
driver = self._get_driver(user_data_dir, chrome_binary=chrome_binary)
try:
for task in self._tasks:
task.run(driver)
finally:
driver.quit()
shutil.rmtree(self._profile_dir_dst)
def _get_driver(self, user_data_dir, profile_name=None, chrome_binary=None,
chromedriver_binary='chromedriver'):
"""Spin up a ChromeDriver instance that uses a given set of user data.
Generates a temporary profile data directory using a local set of test data.
Args:
user_data_dir: Path string for the writable directory in which profiles
should be stored.
profile_name: Name of the profile data directory to be created/used in
user_data_dir.
If None then an eight character name will be generated randomly.
This directory will be removed after the task flow completes.
chrome_binary: Path string to the Chrome binary that should be used by
ChromeDriver.
If None then it will use the PATH to find a binary.
Returns: The generated Chrome Driver instance.
"""
options = Options()
if profile_name is None:
profile_name = ''.join(choice(ascii_lowercase) for i in range(8))
options.add_argument('--profile-directory=%s' % profile_name)
full_path = os.path.realpath(__file__)
path, filename = os.path.split(full_path)
profile_dir_src = os.path.join(path, 'testdata', 'Default')
self._profile_dir_dst = os.path.join(user_data_dir, profile_name)
self._copy_tree(profile_dir_src, self._profile_dir_dst)
if chrome_binary is not None:
options.binary_location = chrome_binary
options.add_argument('--user-data-dir=%s' % user_data_dir)
options.add_argument('--show-autofill-type-predictions')
service_args = []
driver = webdriver.Chrome(executable_path=chromedriver_binary,
chrome_options=options,
service_args=service_args)
driver.set_page_load_timeout(15) # seconds
return driver
def _copy_tree(self, src, dst):
"""Recursively copy a directory tree.
If the destination directory does not exist then it will be created for you.
Doesn't overwrite newer existing files.
Args:
src: Path to the target source directory. It must exist.
dst: Path to the target destination directory. Permissions to create the
the directory (if necessary) and modify it's contents.
"""
if not os.path.exists(dst):
os.makedirs(dst)
for item in os.listdir(src):
src_item = os.path.join(src, item)
dst_item = os.path.join(dst, item)
if os.path.isdir(src_item):
self._copy_tree(src_item, dst_item)
elif (not os.path.exists(dst_item) or
os.stat(src_item).st_mtime - os.stat(dst_item).st_mtime > 1):
# Copy a file if it doesn't already exist, or if existing one is older.
shutil.copy2(src_item, dst_item)
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from autofill_task.autofill_task import AutofillTask
# pylint: disable=g-multiple-import
# pylint: disable=unused-import
from autofill_task.actions import (SetContext, Open, Click, Type, Wait, Select,
ByID, ByClassName, ByCssSelector, Screenshot,
ByXPath, ValidateFields, TriggerAutofill)
from autofill_task.generator import Generator
class TestNeweggComGuestCheckout(AutofillTask):
def _create_script(self):
self.script = [
Open('http://www.newegg.com/Product/Product.aspx?Item=N82E16823126097'),
Click(ByXPath('//*[@id="landingpage-cart"]/div/div[2]/button['
'contains(., \'ADD TO CART\')]')),
Click(ByXPath('//a[contains(., \'View Shopping Cart\')]'), True),
Click(ByXPath('//a[contains(., \'Secure Checkout\')]')),
Click(ByXPath('//a[contains(., \'CONTINUE AS A GUEST\')]'), True),
TriggerAutofill(ByXPath('//*[@id="SFirstName"]'), 'NAME_FIRST'),
ValidateFields([
(ByXPath('//*[@id="SFirstName"]'), 'NAME_FIRST'),
(ByXPath('//*[@id="SLastName"]'), 'NAME_LAST'),
(ByXPath('//*[@id="SAddress1"]'), 'ADDRESS_HOME_LINE1'),
(ByXPath('//*[@id="SAddress2"]'), 'ADDRESS_HOME_LINE2'),
(ByXPath('//*[@id="SCity"]'), 'ADDRESS_HOME_CITY'),
(ByXPath('//*[@id="SState_Option_USA"]'), 'ADDRESS_HOME_STATE',
'CA'),
(ByXPath('//*[@id="SZip"]'), 'ADDRESS_HOME_ZIP', '94035-____'),
(ByXPath('//*[@id="ShippingPhone"]'), 'PHONE_HOME_CITY_AND_NUMBER',
'(650) 670-1234 x__________'),
(ByXPath('//*[@id="email"]'), 'EMAIL_ADDRESS'),
]),
]
class TestGamestopCom(AutofillTask):
def _create_script(self):
self.script = [
Open('http://www.gamestop.com/ps4/consoles/playstation-4-500gb-system-'
'white/118544'),
# First redirects you to the canadian site if run internationally
Open('http://www.gamestop.com/ps4/consoles/playstation-4-500gb-system-'
'white/118544'),
Click(ByXPath('//*[@id="mainContentPlaceHolder_dynamicContent_ctl00_'
'RepeaterRightColumnLayouts_RightColumnPlaceHolder_0_'
'ctl00_0_ctl00_0_StandardPlaceHolder_2_ctl00_2_'
'rptBuyBoxes_2_lnkAddToCart_0"]')),
Click(ByXPath('//*[@id="checkoutButton"]')),
Click(ByXPath('//*[@id="cartcheckoutbtn"]')),
Click(ByXPath('//*[@id="buyasguest"]')),
TriggerAutofill(ByXPath('//*[@id="ShipTo_FirstName"]'), 'NAME_FIRST'),
ValidateFields([
(ByXPath('//*[@id="ShipTo_CountryCode"]'), 'ADDRESS_HOME_COUNTRY',
'US'),
(ByXPath('//*[@id="ShipTo_FirstName"]'), 'NAME_FIRST'),
(ByXPath('//*[@id="ShipTo_LastName"]'), 'NAME_LAST'),
(ByXPath('//*[@id="ShipTo_Line1"]'), 'ADDRESS_HOME_LINE1'),
(ByXPath('//*[@id="ShipTo_Line2"]'), 'ADDRESS_HOME_LINE2'),
(ByXPath('//*[@id="ShipTo_City"]'), 'ADDRESS_HOME_CITY'),
(ByXPath('//*[@id="USStates"]'), 'ADDRESS_HOME_STATE', 'CA'),
(ByXPath('//*[@id="ShipTo_PostalCode"]'), 'ADDRESS_HOME_ZIP'),
(ByXPath('//*[@id="ShipTo_PhoneNumber"]'),
'PHONE_HOME_CITY_AND_NUMBER'),
(ByXPath('//*[@id="ShipTo_EmailAddress"]'), 'EMAIL_ADDRESS'),
])
]
class TestLowesCom(AutofillTask):
def _create_script(self):
self.script = [
Open('http://www.lowes.com/pd/Weber-Original-Kettle-22-in-Black-'
'Porcelain-Enameled-Kettle-Charcoal-Grill/3055249'),
Type(ByXPath('//*[@id="zipcode-input"]'),
self.profile_data('ADDRESS_HOME_ZIP'), True),
Click(ByXPath('//button[contains(., \'Ok\')]'), True),
Click(ByXPath('//*[@id="storeList"]/li[1]/div/div[2]/button['
'contains(., \'Shop this store\')]'), True),
Wait(3),
Click(ByXPath('//button[contains(., \'Add To Cart\')]')),
Click(ByXPath('//a[contains(., \'View Cart\')]')),
Click(ByXPath('//*[@id="LDshipModeId_1"]')),
Click(ByXPath('//*[@id="ShopCartForm"]/div[2]/div[2]/a[contains(.,'
' \'Start Secure Checkout\')]')),
Click(ByXPath('//*[@id="login-container"]/div[2]/div/div/div/a['
'contains(., \'Check Out\')]')),
TriggerAutofill(ByXPath('//*[@id="fname"]'), 'NAME_FIRST'),
ValidateFields([
(ByXPath('//*[@id="fname"]'), 'NAME_FIRST'),
(ByXPath('//*[@id="lname"]'), 'NAME_LAST'),
(ByXPath('//*[@id="company-name"]'), 'COMPANY_NAME'),
(ByXPath('//*[@id="address-1"]'), 'ADDRESS_HOME_LINE1'),
(ByXPath('//*[@id="address-2"]'), 'ADDRESS_HOME_LINE2'),
(ByXPath('//*[@id="city"]'), 'ADDRESS_HOME_CITY'),
(ByXPath('//*[@id="state"]'), 'ADDRESS_HOME_STATE', 'CA'),
(ByXPath('//*[@id="zip"]'), 'ADDRESS_HOME_ZIP'),
]),
Click(ByXPath('//*[@id="revpay_com_order"]')),
Wait(1), # Buttons with the same xPath exists on both pages
Click(ByXPath('//*[@id="revpay_com_order"]')),
Wait(1), # Buttons with the same xPath exists on both pages
TriggerAutofill(ByXPath('//*[@name="cardNumber"]'),
'CREDIT_CARD_NUMBER'),
Type(ByXPath('//*[@id="s-code"]'),
self.profile_data('CREDIT_CARD_VERIFICATION_CODE')),
Type(ByXPath('//*[@id="billing-address-phone1"]'),
self.profile_data('PHONE_HOME_CITY_AND_NUMBER')),
Type(ByXPath('//*[@id="billingEmailAddress"]'),
self.profile_data('EMAIL_ADDRESS')),
ValidateFields([
(ByXPath('//*[@id="checkout-card-type"]'), 'CREDIT_CARD_TYPE'),
(ByXPath('//*[@name="cardNumber"]'), 'CREDIT_CARD_NUMBER'),
(ByXPath('//*[@id="s-code"]'), 'CREDIT_CARD_VERIFICATION_CODE'),
(ByXPath('//*[@id="expiration-month"]'), 'CREDIT_CARD_EXP_MONTH'),
(ByXPath('//*[@id="expiration-year"]'),
'CREDIT_CARD_EXP_4_DIGIT_YEAR'),
(ByXPath('//*[@id="billing-address-phone1"]'),
'PHONE_HOME_CITY_AND_NUMBER', '(650) 670-1234'),
(ByXPath('//*[@id="billingEmailAddress"]'), 'EMAIL_ADDRESS'),
]),
Click(ByXPath('//*[@id="revpay_com_order"]'))
]
This data is used to populate the generated user data directory with just enough
files to add a functioning autofill profile.
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Autofill expected data by field type. This matches the default profile.
"""
DEFAULT = {
# Personal Information categorization types.
'NAME_FIRST': 'Donald',
'NAME_MIDDLE': 'Craig',
'NAME_LAST': 'Figgleburg',
'NAME_MIDDLE_INITIAL': 'C',
'NAME_FULL': 'Donald Craig Figgleburg',
'NAME_SUFFIX': '',
'EMAIL_ADDRESS': 'donald@figgleburg.com',
'PHONE_HOME_NUMBER': '6701234',
'PHONE_HOME_CITY_CODE': '650',
'PHONE_HOME_COUNTRY_CODE': '+1',
'PHONE_HOME_CITY_AND_NUMBER': '6506701234',
'PHONE_HOME_WHOLE_NUMBER': '+16506701234',
'ADDRESS_HOME_LINE1': '314 Oceanwalk Rd.',
'ADDRESS_HOME_LINE2': 'Apt. 4',
'ADDRESS_HOME_APPT_NUM': '4',
'ADDRESS_HOME_CITY': 'Mountain View',
'ADDRESS_HOME_STATE': 'California',
'ADDRESS_HOME_ZIP': '94035',
'ADDRESS_HOME_COUNTRY': 'United States',
'ADDRESS_BILLING_LINE1': '314 Oceanwalk Rd.',
'ADDRESS_BILLING_LINE2': 'Apt. 4',
'ADDRESS_BILLING_APPT_NUM': '4',
'ADDRESS_BILLING_CITY': 'Mountain View',
'ADDRESS_BILLING_STATE': 'California',
'ADDRESS_BILLING_ZIP': '94035',
'ADDRESS_BILLING_COUNTRY': 'United States',
'CREDIT_CARD_NAME': 'Donald Figgleburg',
'CREDIT_CARD_NUMBER': '5432123498764567',
'CREDIT_CARD_EXP_MONTH': '05',
'CREDIT_CARD_EXP_2_DIGIT_YEAR': '21',
'CREDIT_CARD_EXP_4_DIGIT_YEAR': '2021',
'CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR': '05/21',
'CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR': '05/2021',
'CREDIT_CARD_TYPE': '',
'CREDIT_CARD_VERIFICATION_CODE': '',
'COMPANY_NAME': 'Frtizerg Inc.',
# Generic fieldtype having default value to use, used only by autocheckout
# experiment. These field types exist for select merchant pages. Field
# mappings for these pages are generated by autocheckout's buildstorage
# script.
'FIELD_WITH_DEFAULT_VALUE': '',
'PHONE_BILLING_NUMBER': '',
'PHONE_BILLING_CITY_CODE': '',
'PHONE_BILLING_COUNTRY_CODE': '',
'PHONE_BILLING_CITY_AND_NUMBER': '',
'PHONE_BILLING_WHOLE_NUMBER': '',
'NAME_BILLING_FIRST': '',
'NAME_BILLING_MIDDLE': '',
'NAME_BILLING_LAST': '',
'NAME_BILLING_MIDDLE_INITIAL': '',
'NAME_BILLING_FULL': '',
'NAME_BILLING_SUFFIX': '',
# Includes of the lines of a street address, including newlines, e.g.
# 123 Main Street,
# Apt. #42
'ADDRESS_HOME_STREET_ADDRESS': '314 Oceanwalk Rd.\nApt. 4',
'ADDRESS_BILLING_STREET_ADDRESS': '314 Oceanwalk Rd.\nApt. 4',
'ACCOUNT_CREATION_PASSWORD': '',
# The third line of the street address.
'ADDRESS_HOME_LINE3': '',
'ADDRESS_BILLING_LINE3': ''
}
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