Commit 856e676d authored by Yiming Zhou's avatar Yiming Zhou Committed by Commit Bot

Introducing improvements for the Action Recorder Extension.

1. Improved the xpath generator. The generator will produce even more concise xpaths. This improvement helps to make test recipes readable to humans.
2. Changed the way the extension queries for an iframe's context. Prior to this change, the extension would construct a path from the iframe to the top level frame by making a query at each ancestor frame node. However, InProcessBrowserTest does not need a path to go from the top level frame to a descendant iframe. Therefore, in this change the extension only makes one query at the parent frame node.
3. Moved the frame context query action to the front of the start recording workflow. This move cuts one message roundtrip from the extension's background script to the extension's content script.
4. Made the extension jot down a target element's visibility state when recording an action. Prior to this change, the extension assumes that every target element is visible, enabled and on the top of the page. However, the extension's complex action recording logic sometimes catches actions on invisible or partially obscured elements.
5. Added refined logic to the extension to distinguish between a user typing action and a Chrome autofill action. Prior to this change the extension simply assumes that Chrome always autofills every autofill-able field. If a user types inside an autofillable field, the extension will mistakenly record an autofill action. With this change the extension uses keyboard events to detect when a user types inside a field, eliminating the false positive.
6. Began implementing new features to capture Chrome Password Manager actions.

Bug: 855284
Change-Id: Ic7ff3af95cdc9f308c3ad061a3506fced150b4f8
Reviewed-on: https://chromium-review.googlesource.com/1132540
Commit-Queue: Yiming Zhou <uwyiming@google.com>
Reviewed-by: default avatarJared Saul <jsaul@google.com>
Reviewed-by: default avatarMathieu Perreault <mathp@chromium.org>
Cr-Commit-Position: refs/heads/master@{#574385}
parent c18d1990
......@@ -288,26 +288,80 @@
});
}
function getIframeContext(tabId, frameId, iframeLocation) {
function getIframeContext(tabId, frameId) {
return new Promise((resolve, reject) => {
if (frameId === 0) {
resolve({ isIframe: false });
} else {
chrome.webNavigation.getFrame(
{ tabId: tabId, frameId: frameId }, (details) => {
if (chrome.runtime.lastError) {
reject(`Unable to query for frame info on frame ${frameId}`);
} else {
sendMessageToTab(tabId, {
type: RecorderMsgEnum.GET_IFRAME_XPATH,
frameId: frameId,
location: iframeLocation
}, { frameId: details.parentFrameId }).then((context) => {
resolve(context);
}).catch((message) => {
reject(message);
});
let context = { isIframe: true };
getAllFramesInTab(tabId)
.then((details) => {
let targetFrame;
for (let index = 0; index < details.length; index++) {
if (details[index].frameId === frameId) {
targetFrame = details[index];
break;
}
}
// Send a message to the parent frame and see if the iframe has a
// 'name' attribute.
sendMessageToTab(tabId, {
type: RecorderMsgEnum.GET_IFRAME_NAME,
url: targetFrame.url
}, {
frameId: targetFrame.parentFrameId
})
.then((frameName) => {
if (frameName !== '') {
context.browserTest = { name: frameName };
resolve(context);
} else {
const targetFrameUrl = new URL(targetFrame.url);
// The frame does not have a 'name' attribute. Check if the
// frame has a unique combination of scheme, host and port.
//
// The Captured Site automation framework can identify an
// iframe by its scheme + host + port, provided this
// information combination is unique. Identifying an iframe
// through its scheme + host + port is more preferable than
// identifying an iframe through its URL. An URL will
// frequently contain parameters, and many websites use random
// number generator or date generator to create these
// parameters. For example, in the following URL
//
// https://payment.bhphotovideo.com/static/desktop/v2.0/
// index.html
// #paypageId=aLGNuLSTJVwgEiCn&cartID=333334444
// &receiverID=77777777-7777-4777-b777-777777888888
// &uuid=77777777-7777-4777-b777-778888888888
//
// The site created the parameters cartID, receiverID and uuid
// using random number generators. These parameters will have
// different values every time the browser loads the page.
// Therefore automation will not be able to identify an iframe
// that loads this URL.
let frameHostAndSchemeIsUnique = true;
for (let index = 0; index < details.length; index++) {
const url = new URL(details[index].url);
if (details[index].frameId !== targetFrame.frameId &&
targetFrameUrl.protocol === url.protocol &&
targetFrameUrl.host === url.host) {
frameHostAndSchemeIsUnique = false;
break;
}
}
if (frameHostAndSchemeIsUnique) {
context.browserTest = {
schemeAndHost:
`${targetFrameUrl.protocol}//${targetFrameUrl.host}`
};
resolve(context);
} else {
context.browserTest = { url: targetFrame.url };
resolve(context);
}
}
});
});
}
});
......@@ -354,8 +408,14 @@
function startRecordingOnTabAndFrame(tabId, frameId) {
const ret =
sendMessageToTab(tabId, { type: RecorderMsgEnum.START },
{ frameId: frameId })
getIframeContext(tabId, frameId)
.then((context) => {
return sendMessageToTab(tabId,
{ type: RecorderMsgEnum.START,
frameContext: context
},
{ frameId: frameId });
})
.then((response) => {
if (!response) {
return Promise.reject(
......@@ -546,9 +606,9 @@
.then((details) => {
let recordingStartedOnRootFramePromise;
details.forEach((frame) => {
// Th extension has no need and no permission to inject script
// into a blank page.
if (frame.url !== 'about:blank') {
// The extension has no need and no permission to inject script
// into 'about:' pages, such as the 'about:blank' page.
if (!frame.url.startsWith('about:')) {
const promise =
startRecordingOnTabAndFrame(tab.id, frame.frameId);
if (frame.frameId === 0) {
......@@ -596,10 +656,10 @@
chrome.webNavigation.onCompleted.addListener((details) => {
getRecordingTabId().then((tabId) => {
if (details.tabId === tabId &&
// Skip recording on about:blank. No meaningful user interaction will
// occur on a blank page. Plus, this extension has no permission to
// access about:blank.
details.url !== 'about:blank') {
// Skip recording on 'about:' pages. No meaningful user interaction
// occur on 'about:'' pages such as the blank page. Plus, this
// extension has no permission to access 'about:' pages.
!details.url.startsWith('about:')) {
startRecordingOnTabAndFrame(tabId, details.frameId)
.then(() => getRecordingState())
.then((state) => {
......
......@@ -31,9 +31,9 @@ const RecorderMsgEnum = {
START: 'start-recording',
STOP: 'stop-recording',
CANCEL: 'cancel-recording',
GET_FRAME_CONTEXT: 'get-frame-context',
GET_IFRAME_XPATH: 'get-iframe-xpath',
GET_IFRAME_NAME: 'get-iframe-name',
ADD_ACTION: 'record-action',
MEMORIZE_PASSWORD_FORM: 'memorize-password-form',
};
const Local_Storage_Vars = {
......@@ -49,4 +49,4 @@ const Indexed_DB_Vars = {
ATTRIBUTES: 'Attributes',
NAME: 'name',
URL: 'url'
};
\ No newline at end of file
};
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