Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(690)

Unified Diff: components/test/data/autofill/automated_integration/action_recorder_extension/background/action_recorder.js

Issue 2116583004: Automated Autofill testing library + extension (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressed nits, reduced preferences Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: components/test/data/autofill/automated_integration/action_recorder_extension/background/action_recorder.js
diff --git a/components/test/data/autofill/automated_integration/action_recorder_extension/background/action_recorder.js b/components/test/data/autofill/automated_integration/action_recorder_extension/background/action_recorder.js
new file mode 100644
index 0000000000000000000000000000000000000000..d9e5299fa09e8125ce99d128ea3a5c8b9343ffc0
--- /dev/null
+++ b/components/test/data/autofill/automated_integration/action_recorder_extension/background/action_recorder.js
@@ -0,0 +1,460 @@
+// 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 FIELD_TYPES, SITES_TO_VISIT, IndentedTextFactory, ActionSet, ByXPath,
+// Open, SetContext, Type, Select, Click, TypedField, ValidateFields
+'use strict';
+
+class ActionRecorder {
+ constructor() {
+ this._siteIndex = 0;
+ this.cancel();
+
+ this._startListeners();
+ }
+
+ start(url) {
+ this._actionSet = new ActionSet(url);
+ this._createRecordingContextMenus();
+
+ console.log('Started recording.');
+ }
+
+ stop() {
+ if (!this.isRecording()) {
+ console.error(
+ 'Attempted to stop recording when no action set was active.');
+ return;
+ }
+
+ this._createIdleContextMenus();
+
+ // Remove all redundant actions
+ this._actionSet.optimize();
+
+ this._copyText(this._actionSet.toString());
+ this.cancel();
+ console.log('Stopped recording.');
+ }
+
+ cancel() {
+ this._actionSet = null;
+ this._lastRightClick = null;
+ this._typedFields = [];
+
+ this._createIdleContextMenus();
+ }
+
+ isRecording() {
+ return this._actionSet !== null;
+ }
+
+ /**
+ * Copies a string to the system clipboard.
+ * Multiline strings are supported.
+ *
+ * @param {String} text Target string
+ */
+ _copyText(text) {
+ const input = document.createElement('textarea');
+ document.body.appendChild(input);
+ input.value = text;
+ input.focus();
+ input.select();
+ document.execCommand('Copy');
+ input.remove();
+
+ const iconUrl = 'icons/icon_' +
+ (this.isRecording() ? 'recording' : 'idle') +
+ '128.png';
+
+ chrome.notifications.create(undefined, {
+ type: 'basic',
+ iconUrl: iconUrl,
+ title: 'Action Recorder',
+ message: 'Recorded script copied to clipboard.'
+ });
+ }
+
+ /**
+ * Process an action data event and add the appropriate Action object to the
+ * active ActionSet.
+ * @param {Object} actionData Data object to construct an Action
+ * instance with
+ * @param {MessageSender} sender Chrome sender data object
+ */
+ _handleAction(actionData, sender) {
+ if (!this.isRecording()) {
+ console.warn('Actions cannot be added when recording is not active.');
+ return;
+ }
+ if (!actionData) {
+ console.error('Invalid action data.');
+ return;
+ }
+
+ console.log('Action data received: ', actionData);
+
+ let actionObject;
+
+ let selector = new ByXPath(actionData.selector);
+
+ switch (actionData.type) {
+ case 'set-context':
+ if (actionData.selector === 'None') {
+ selector = actionData.selector;
+ }
+ if (actionData.args.length >= 2) {
+ actionData.args[1] = new ByXPath(actionData.args[1]);
+ }
+ actionObject = new SetContext(selector, ...actionData.args);
+ break;
+ case 'type':
+ actionObject = new Type(selector, ...actionData.args);
+ break;
+ case 'fill-email':
+ actionObject = new Type(selector, 'GenEmail()', undefined, true);
+ break;
+ case 'fill-password':
+ actionObject = new Type(selector, 'GenPassword()', undefined, true);
+ break;
+ case 'select':
+ actionObject = new Select(selector, ...actionData.args);
+ break;
+ case 'left-click':
+ actionObject = new Click(selector);
+ break;
+ case 'right-click':
+ this._lastRightClick = {
+ selector: selector,
+ tabId: sender.tab.id,
+ frameId: sender.frameId
+ };
+ return;
+ break;
+ default:
+ console.error(`Unsupported action type: ${actionData.type}`);
+ return;
+ }
+
+ this._actionSet.addAction(actionObject);
+ }
+
+ _startListeners() {
+ /**
+ * Add listener for messages from content scripts.
+ */
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ if (!request) {
+ console.error('Invalid request from content script.', request);
+ return;
+ }
+
+ switch (request.type) {
+ case 'get-frame-info':
+ let frameInfo = {
+ tabId: sender.tab.id,
+ frameId: sender.frameId
+ };
+
+ if (frameInfo.frameId === 0) {
+ frameInfo.parentFrameId = -1;
+ return sendResponse(frameInfo);
+ }
+
+ chrome.webNavigation.getFrame(frameInfo, (details) => {
+ frameInfo.parentFrameId = details.parentFrameId;
+ sendResponse(frameInfo);
+ });
+
+ return true;
+ case 'forward-message-to-tab':
+ chrome.tabs.sendMessage(...request.args);
+ break;
+ case 'recording-state-request':
+ sendResponse({
+ isRecording: this.isRecording()
+ });
+ break;
+ case 'start-recording':
+ this._getCurrentTab((err, tab) => {
+ if (err) {
+ console.error(err);
+ } else {
+ this.start(tab.url);
+ }
+
+ sendResponse({
+ isRecording: this.isRecording()
+ });
+ });
+
+ return true;
+ case 'stop-recording':
+ this.stop();
+ sendResponse({
+ isRecording: this.isRecording()
+ });
+ break;
+ case 'cancel-recording':
+ this.cancel();
+ sendResponse({
+ isRecording: this.isRecording()
+ });
+ break;
+ case 'next-site':
+ if (this.isRecording()) {
+ console.error('Cannot go to next site when recording.');
+ return;
+ }
+ this._visitSite(this._nextSite());
+ break;
+ case 'action':
+ this._handleAction(request.data, sender);
+ break;
+ case 'actions':
+ for (let i = 0; i < request.data.length; i++) {
+ this._handleAction(request.data[i], sender);
+ }
+ break;
+ default:
+ console.error(`Unknown request type: ${request.type}`);
+ }
+ });
+ }
+
+ _getCurrentTab(cb) {
+ chrome.tabs.query({
+ active: true,
+ currentWindow: true
+ }, (tabs) => {
+ if (tabs.length === 0) {
+ cb(new Error('Unable to retrieve current tab.'));
+ } else {
+ cb(null, tabs[0]);
+ }
+ });
+ }
+
+ /**
+ * Add a validation action to the active action set from the current
+ * collection of typed fields.
+ */
+ _constructValidationAction() {
+ if (!this.isRecording()) {
+ console.warn('Actions cannot be added when recording is not active.');
+ return;
+ }
+
+ const typedFields =
+ Object.keys(this._typedFields).map(key => this._typedFields[key]);
+
+ console.log('Creating field type validation action: ', typedFields);
+
+ this._actionSet.addAction(new ValidateFields(typedFields));
+
+ this._typedFields = {};
+ }
+
+ /**
+ * Sets a field type for the last right-clicked element on the active tab
+ * (thus for the element for which the context menu was created).
+ *
+ * @param {String} fieldType Identifier of the editable input's field type
+ */
+ _setFieldTypeForEditable(fieldType) {
+ if (!this.isRecording()) {
+ console.warn('Field types cannot be set when recording is not active.');
+ return;
+ }
+
+ if (this._lastRightClick === null) {
+ return console.error('No right click data available');
+ }
+
+ if (fieldType === 'NONE') {
+ fieldType = null;
+ }
+
+ const selector = this._lastRightClick.selector;
+
+ console.log(`Field Type '${fieldType}' received for: ${selector}`);
+
+ this._typedFields[selector] = new TypedField(selector, fieldType);
+ }
+
+ /**
+ * Generates a email fill action, and enters equivalent data to the target
+ * element.
+ */
+ _fillEmail() {
+ if (!this.isRecording()) {
+ console.warn('Email can only be generated when recording is active.');
+ return;
+ }
+
+ if (this._lastRightClick === null) {
+ return console.error('No right click data available');
+ }
+
+ const clickData = this._lastRightClick;
+
+ chrome.tabs.sendMessage(
+ clickData.tabId, {
+ type: 'fill-email',
+ selector: clickData.selector._xPath
+ }, {
+ frameId: clickData.frameId
+ });
+ }
+
+ /**
+ * Generates a password fill action, and enters equivalent data to the target
+ * element.
+ */
+ _fillPassword() {
+ if (!this.isRecording()) {
+ console.warn('Password can only be generated when recording is active.');
+ return;
+ }
+
+ if (this._lastRightClick === null) {
+ return console.error('No right click data available');
+ }
+
+ const clickData = this._lastRightClick;
+
+ chrome.tabs.sendMessage(
+ clickData.tabId, {
+ type: 'fill-password',
+ selector: clickData.selector._xPath
+ }, {
+ frameId: clickData.frameId
+ });
+ }
+
+ /**
+ * Generate context menus for managing field types.
+ * A user must be able to select all available field types for an input
+ * element, and then generate a field validation action once all fields have
+ * been marked.
+ */
+ _createRecordingContextMenus() {
+ chrome.contextMenus.removeAll(() => {
+ const parentContextMenu = chrome.contextMenus.create({
+ title: 'Input Field Type',
+ contexts: ['all']
+ });
+ const specialFillContextMenu = chrome.contextMenus.create({
+ title: 'Special Fill',
+ contexts: ['editable']
+ });
+ chrome.contextMenus.create({
+ title: 'Fill Email',
+ contexts: ['editable'],
+ parentId: specialFillContextMenu,
+ onclick: (info, tab) => {
+ this._fillEmail();
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Fill Password',
+ contexts: ['editable'],
+ parentId: specialFillContextMenu,
+ onclick: (info, tab) => {
+ this._fillPassword();
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Validate Field Types',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this._constructValidationAction();
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Forget Field Types',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this._typedFields = {};
+ console.log('Field type knowledge reset.');
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Stop and Copy',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this.stop();
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Cancel',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this.cancel();
+ }
+ });
+
+ for (let i = 0; i < FIELD_TYPES.length; i++) {
+ const fieldType = FIELD_TYPES[i];
+
+ chrome.contextMenus.create({
+ title: fieldType,
+ parentId: parentContextMenu,
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this._setFieldTypeForEditable(fieldType);
+ }
+ });
+ }
+ });
+ }
+
+ _createIdleContextMenus() {
+ chrome.contextMenus.removeAll(() => {
+ chrome.contextMenus.create({
+ title: 'Start',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this.start(info.pageUrl);
+ }
+ });
+ chrome.contextMenus.create({
+ title: 'Next Site',
+ contexts: ['all'],
+ onclick: (info, tab) => {
+ this._visitSite(this._nextSite());
+ }
+ });
+ });
+ }
+
+ /**
+ * Retrieve the next recommended "top 100" website url.
+ * Note that this will also advance the site index for the next call.
+ *
+ * @return {String} Complete url for the next top 100 site
+ */
+ _nextSite() {
+ if (this._siteIndex >= SITES_TO_VISIT.length) {
+ console.error('All recommended sites have been visited.');
+ return;
+ }
+
+ const hostname = SITES_TO_VISIT[this._siteIndex++];
+ return `http://${hostname}`;
+ }
+
+ /**
+ * Redirect the currently active tab to a given url.
+ * @param {String} url Complete target url (including protocol)
+ */
+ _visitSite(url) {
+ chrome.tabs.update({
+ url: url
+ });
+ }
+}
+
+const actionRecorder = new ActionRecorder();

Powered by Google App Engine
This is Rietveld 408576698