Commit ac464a7c authored by Trent Apted's avatar Trent Apted Committed by Commit Bot

CrOS Files: Don't bake-in so much test code to background_scripts.js.

Instead, load it lazily the first time a testing extension connects
and wants to use the remote call APIs.

This avoids having to parse the testing code every time any of the
ui/file_manager apps start up (in release, or in tests). Note that
current tests (e.g. gallery) may start up a background page with this
test code up to *four times* for each test. After this change, only
the app under test will load the testing code.

There may still be some added latency. To help balance that, this
CL caches the result of RemoteCall.isStepByStepEnabled(). That makes
it consistent with the newly added function anyway.

This still distributes the testing code in release, which is not ideal.
Loading from a filesystem:// URL might avoid that in future. This should
probably also use an ES6 module.. Baby steps.

Bug: 903669
Cq-Include-Trybots: luci.chromium.try:closure_compilation
Change-Id: I7acf6b55b10fa775d40bae48b81b0cfd1859df56
Reviewed-on: https://chromium-review.googlesource.com/c/1322340
Commit-Queue: Trent Apted <tapted@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#606742}
parent 2ef7eef6
......@@ -51,6 +51,7 @@ js_type_check("closure_compile_module") {
":metadata_proxy",
":mock_volume_manager",
":progress_center",
":runtime_loaded_test_util",
":task_queue",
":test_util_base",
":volume_info_impl",
......@@ -259,14 +260,19 @@ js_library("task_queue") {
}
js_library("test_util_base") {
}
js_library("runtime_loaded_test_util") {
# TODO(tapted): Move this target to //ui/file_manager/base. It is used in the
# background page of all |related_apps|, but accessed via extension messaging.
# So it doesn't need to be visible as a closure dependency, except for the
# "unpacked" test framework.
# background page of all |related_apps|, but loaded at runtime by
# :test_util_base via extension messaging, so doesn't need to be depended on
# except by the closure compilation target. The exception is the "unpacked"
# test framework, which copies some testing functions into its test context.
visibility += [ "//ui/file_manager/file_manager/test/js:test_util" ]
deps = [
":app_windows",
":test_util_base",
"../../../externs:webview_tag",
"//ui/file_manager/base/js:error_counter",
]
......
// Copyright 2018 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.
/**
* @fileoverview Script loaded into the background page of a component
* extension under test at runtime to populate testing functionality.
*/
/**
* Extract the information of the given element.
* @param {Element} element Element to be extracted.
* @param {Window} contentWindow Window to be tested.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained. NOTE: Causes element style re-calculation.
* @return {{attributes:Object<string>, text:string,
* styles:(Object<string>|undefined), hidden:boolean}} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
function extractElementInfo(element, contentWindow, opt_styleNames) {
const attributes = {};
for (let i = 0; i < element.attributes.length; i++) {
attributes[element.attributes[i].nodeName] =
element.attributes[i].nodeValue;
}
const result = {
attributes: attributes,
text: element.textContent,
value: element.value,
// The hidden attribute is not in the element.attributes even if
// element.hasAttribute('hidden') is true.
hidden: !!element.hidden,
hasShadowRoot: !!element.shadowRoot
};
const styleNames = opt_styleNames || [];
assert(Array.isArray(styleNames));
if (!styleNames.length)
return result;
const styles = {};
const size = element.getBoundingClientRect();
const computedStyles = contentWindow.getComputedStyle(element);
for (let i = 0; i < styleNames.length; i++) {
styles[styleNames[i]] = computedStyles[styleNames[i]];
}
result.styles = styles;
// These attributes are set when element is <img> or <canvas>.
result.imageWidth = Number(element.width);
result.imageHeight = Number(element.height);
// These attributes are set in any element.
result.renderedWidth = size.width;
result.renderedHeight = size.height;
result.renderedTop = size.top;
result.renderedLeft = size.left;
return result;
}
/**
* Obtains window information.
*
* @return {Object<{innerWidth:number, innerHeight:number}>} Map window
* ID and window information.
*/
test.util.sync.getWindows = function() {
var windows = {};
for (var id in window.appWindows) {
var windowWrapper = window.appWindows[id];
windows[id] = {
outerWidth: windowWrapper.contentWindow.outerWidth,
outerHeight: windowWrapper.contentWindow.outerHeight
};
}
for (var id in window.background.dialogs) {
windows[id] = {
outerWidth: window.background.dialogs[id].outerWidth,
outerHeight: window.background.dialogs[id].outerHeight
};
}
return windows;
};
/**
* Closes the specified window.
*
* @param {string} appId AppId of window to be closed.
* @return {boolean} Result: True if success, false otherwise.
*/
test.util.sync.closeWindow = function(appId) {
if (appId in window.appWindows && window.appWindows[appId].contentWindow) {
window.appWindows[appId].close();
return true;
}
return false;
};
/**
* Gets total Javascript error count from background page and each app window.
* @return {number} Error count.
*/
test.util.sync.getErrorCount = function() {
var totalCount = window.JSErrorCount;
for (var appId in window.appWindows) {
var contentWindow = window.appWindows[appId].contentWindow;
if (contentWindow.JSErrorCount)
totalCount += contentWindow.JSErrorCount;
}
return totalCount;
};
/**
* Resizes the window to the specified dimensions.
*
* @param {Window} contentWindow Window to be tested.
* @param {number} width Window width.
* @param {number} height Window height.
* @return {boolean} True for success.
*/
test.util.sync.resizeWindow = function(contentWindow, width, height) {
window.appWindows[contentWindow.appID].resizeTo(width, height);
return true;
};
/**
* Maximizes the window.
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True for success.
*/
test.util.sync.maximizeWindow = function(contentWindow) {
window.appWindows[contentWindow.appID].maximize();
return true;
};
/**
* Restores the window state (maximized/minimized/etc...).
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True for success.
*/
test.util.sync.restoreWindow = function(contentWindow) {
window.appWindows[contentWindow.appID].restore();
return true;
};
/**
* Returns whether the window is miximized or not.
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True if the window is maximized now.
*/
test.util.sync.isWindowMaximized = function(contentWindow) {
return window.appWindows[contentWindow.appID].isMaximized();
};
/**
* Queries all elements.
*
* @param {!Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {!Array<{attributes:Object<string>, text:string,
* styles:Object<string>, hidden:boolean}>} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
test.util.sync.queryAllElements = function(
contentWindow, targetQuery, opt_styleNames) {
return test.util.sync.deepQueryAllElements(
contentWindow, [targetQuery], opt_styleNames);
};
/**
* Queries elements inside shadow DOM.
*
* @param {!Window} contentWindow Window to be tested.
* @param {!Array<string>} targetQuery Query to specify the element.
* |targetQuery[0]| specifies the first element(s). |targetQuery[1]| specifies
* elements inside the shadow DOM of the first element, and so on.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {!Array<{attributes:Object<string>, text:string,
* styles:Object<string>, hidden:boolean}>} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
test.util.sync.deepQueryAllElements = function(
contentWindow, targetQuery, opt_styleNames) {
if (!contentWindow.document)
return [];
var elems =
test.util.sync.deepQuerySelectorAll_(contentWindow.document, targetQuery);
return elems.map(function(element) {
return extractElementInfo(element, contentWindow, opt_styleNames);
});
};
/**
* Selects elements below |root|, possibly following shadow DOM subtree.
*
* @param {(!HTMLElement|!Document)} root Element to search from.
* @param {!Array<string>} targetQuery Query to specify the element.
* |targetQuery[0]| specifies the first element(s). |targetQuery[1]| specifies
* elements inside the shadow DOM of the first element, and so on.
* @return {!Array<!HTMLElement>} Matched elements.
*
* @private
*/
test.util.sync.deepQuerySelectorAll_ = function(root, targetQuery) {
var elems = Array.prototype.slice.call(root.querySelectorAll(targetQuery[0]));
var remaining = targetQuery.slice(1);
if (remaining.length === 0)
return elems;
var res = [];
for (var i = 0; i < elems.length; i++) {
if (elems[i].shadowRoot) {
res = res.concat(
test.util.sync.deepQuerySelectorAll_(elems[i].shadowRoot, remaining));
}
}
return res;
};
/**
* Executes a script in the context of the first <webview> element contained in
* the window, including shadow DOM subtrees if given, and returns the script
* result via the callback.
*
* @param {Window} contentWindow Window to be tested.
* @param {!Array<string>} targetQuery Query for the <webview> element.
* |targetQuery[0]| specifies the first element. |targetQuery[1]| specifies
* an element inside the shadow DOM of the first element, etc. The last
* targetQuery item must return the <webview> element.
* @param {string} script Javascript code to be executed within the <webview>.
* @param {function(*)} callback Callback function to be called with the
* result of the |script|.
*/
test.util.async.deepExecuteScriptInWebView = function(
contentWindow, targetQuery, script, callback) {
const webviews =
test.util.sync.deepQuerySelectorAll_(contentWindow.document, targetQuery);
if (!webviews || webviews.length !== 1)
throw new Error('<webview> not found: [' + targetQuery.join(',') + ']');
const webview = /** @type {WebView} */ (webviews[0]);
webview.executeScript({code: script}, callback);
};
/**
* Gets the information of the active element.
*
* @param {Window} contentWindow Window to be tested.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {?{attributes:Object<string>, text:string,
* styles:(Object<string>|undefined), hidden:boolean}} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values. If there is no
* active element, returns null.
*/
test.util.sync.getActiveElement = function(contentWindow, opt_styleNames) {
if (!contentWindow.document || !contentWindow.document.activeElement)
return null;
return extractElementInfo(
contentWindow.document.activeElement, contentWindow, opt_styleNames);
};
/**
* Assigns the text to the input element.
* @param {Window} contentWindow Window to be tested.
* @param {string} query Query for the input element.
* @param {string} text Text to be assigned.
*/
test.util.sync.inputText = function(contentWindow, query, text) {
var input = contentWindow.document.querySelector(query);
input.value = text;
};
/**
* Sends an event to the element specified by |targetQuery| or active element.
*
* @param {Window} contentWindow Window to be tested.
* @param {?string|Array<string>} targetQuery Query to specify the element.
* If this value is null, an event is dispatched to active element of the
* document.
* If targetQuery is an array, |targetQuery[0]| specifies the first
* element(s), |targetQuery[1]| specifies elements inside the shadow DOM of
* the first element, and so on.
* @param {!Event} event Event to be sent.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.sendEvent = function(contentWindow, targetQuery, event) {
if (!contentWindow.document)
return false;
let target;
if (targetQuery === null) {
target = contentWindow.document.activeElement;
} else if (typeof targetQuery === 'string') {
target = contentWindow.document.querySelector(targetQuery);
} else if (Array.isArray(targetQuery)) {
let elems = test.util.sync.deepQuerySelectorAll_(
contentWindow.document, targetQuery);
if (elems.length > 0)
target = elems[0];
}
if (!target)
return false;
target.dispatchEvent(event);
return true;
};
/**
* Sends an fake event having the specified type to the target query.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @param {string} eventType Type of event.
* @param {Object=} opt_additionalProperties Object contaning additional
* properties.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeEvent = function(
contentWindow, targetQuery, eventType, opt_additionalProperties) {
var event = new Event(
eventType,
/** @type {!EventInit} */ (opt_additionalProperties || {}));
if (opt_additionalProperties) {
for (var name in opt_additionalProperties) {
event[name] = opt_additionalProperties[name];
}
}
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Sends a fake key event to the element specified by |targetQuery| or active
* element with the given |key| and optional |ctrl,shift,alt| modifier.
*
* @param {Window} contentWindow Window to be tested.
* @param {?string} targetQuery Query to specify the element. If this value is
* null, key event is dispatched to active element of the document.
* @param {string} key DOM UI Events key value.
* @param {boolean} ctrl Whether CTRL should be pressed, or not.
* @param {boolean} shift whether SHIFT should be pressed, or not.
* @param {boolean} alt whether ALT should be pressed, or not.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeKeyDown = function(
contentWindow, targetQuery, key, ctrl, shift, alt) {
const event = new KeyboardEvent('keydown', {
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
key: key,
ctrlKey: ctrl,
shiftKey: shift,
altKey: alt
});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Simulates a fake mouse click (left button, single click) on the element
* specified by |targetQuery|. If the element has the click method, just calls
* it. Otherwise, this sends 'mouseover', 'mousedown', 'mouseup' and 'click'
* events in turns.
*
* @param {Window} contentWindow Window to be tested.
* @param {string|Array<string>} targetQuery Query to specify the element.
* If targetQuery is an array, |targetQuery[0]| specifies the first
* element(s), |targetQuery[1]| specifies elements inside the shadow DOM of
* the first element, and so on.
* @param {{shift: boolean, alt: boolean, ctrl: boolean}=} opt_keyModifiers Object
* contaning common key modifiers : shift, alt, and ctrl.
* @return {boolean} True if the all events are sent to the target, false
* otherwise.
*/
test.util.sync.fakeMouseClick = function(
contentWindow, targetQuery, opt_keyModifiers) {
const modifiers = opt_keyModifiers || {};
const props = {
bubbles: true,
detail: 1,
composed: true, // Allow the event to bubble past shadow DOM root.
ctrlKey: modifiers.ctrl,
shiftKey: modifiers.shift,
altKey: modifiers.alt,
};
const mouseOverEvent = new MouseEvent('mouseover', props);
const resultMouseOver =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseOverEvent);
const mouseDownEvent = new MouseEvent('mousedown', props);
const resultMouseDown =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent);
const mouseUpEvent = new MouseEvent('mouseup', props);
const resultMouseUp =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseUpEvent);
const clickEvent = new MouseEvent('click', props);
const resultClick =
test.util.sync.sendEvent(contentWindow, targetQuery, clickEvent);
return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
};
/**
* Simulates a fake mouse click (right button, single click) on the element
* specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false
* otherwise.
*/
test.util.sync.fakeMouseRightClick = function(contentWindow, targetQuery) {
const mouseDownEvent =
new MouseEvent('mousedown', {bubbles: true, button: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent)) {
return false;
}
const contextMenuEvent =
new MouseEvent('contextmenu', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, contextMenuEvent);
};
/**
* Simulates a fake touch event (touch start, touch end) on the element
* specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false
* otherwise.
*/
test.util.sync.fakeTouchClick = function(contentWindow, targetQuery) {
const touchStartEvent = new TouchEvent('touchstart');
if (!test.util.sync.sendEvent(contentWindow, targetQuery, touchStartEvent)) {
return false;
}
const mouseDownEvent =
new MouseEvent('mousedown', {bubbles: true, button: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent)) {
return false;
}
const touchEndEvent = new TouchEvent('touchend');
if (!test.util.sync.sendEvent(contentWindow, targetQuery, touchEndEvent)) {
return false;
}
const contextMenuEvent =
new MouseEvent('contextmenu', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, contextMenuEvent);
};
/**
* Simulates a fake double click event (left button) to the element specified by
* |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseDoubleClick = function(contentWindow, targetQuery) {
// Double click is always preceded with a single click.
if (!test.util.sync.fakeMouseClick(contentWindow, targetQuery)) {
return false;
}
// Send the second click event, but with detail equal to 2 (number of clicks)
// in a row.
let event =
new MouseEvent('click', {bubbles: true, detail: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, event)) {
return false;
}
// Send the double click event.
event = new MouseEvent('dblclick', {bubbles: true, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, event)) {
return false;
}
return true;
};
/**
* Sends a fake mouse down event to the element specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseDown = function(contentWindow, targetQuery) {
const event = new MouseEvent('mousedown', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Sends a fake mouse up event to the element specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseUp = function(contentWindow, targetQuery) {
const event = new MouseEvent('mouseup', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Focuses to the element specified by |targetQuery|. This method does not
* provide any guarantee whether the element is actually focused or not.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if focus method of the element has been called, false
* otherwise.
*/
test.util.sync.focus = function(contentWindow, targetQuery) {
var target = contentWindow.document &&
contentWindow.document.querySelector(targetQuery);
if (!target)
return false;
target.focus();
return true;
};
/**
* Obtains the list of notification ID.
* @param {function(Object<boolean>)} callback Callback function with
* results returned by the script.
*/
test.util.async.getNotificationIDs = function(callback) {
chrome.notifications.getAll(callback);
};
/**
* Opens the file URL. It emulates the interaction that Launcher search does
* from a search result, it triggers the background page's event listener that
* listens to evens from launcher_search_provider API.
*
* @param {string} fileURL File URL to open by Files app background dialog.
* @suppress {accessControls|missingProperties} Closure disallow calling private
* launcherSearch_, but here we just want to emulate the behaviour, so we don't
* need to make this attribute public. Also the interface
* "FileBrowserBackground" doesn't define the attributes "launcherSearch_" so we
* need to suppress missingProperties.
*/
test.util.sync.launcherSearchOpenResult = function(fileURL) {
window.background.launcherSearch_.onOpenResult_(fileURL);
};
/**
* Gets file entries just under the volume.
*
* @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
* @param {Array<string>} names File name list.
* @param {function(*)} callback Callback function with results returned by the
* script.
*/
test.util.async.getFilesUnderVolume = function(volumeType, names, callback) {
var displayRootPromise =
volumeManagerFactory.getInstance().then(function(volumeManager) {
var volumeInfo = volumeManager.getCurrentProfileVolumeInfo(volumeType);
return volumeInfo.resolveDisplayRoot();
});
var retrievePromise = displayRootPromise.then(function(displayRoot) {
var filesPromise = names.map(function(name) {
return new Promise(displayRoot.getFile.bind(displayRoot, name, {}));
});
return Promise.all(filesPromise)
.then(function(aa) {
return util.entriesToURLs(aa);
})
.catch(function() {
return [];
});
});
retrievePromise.then(callback);
};
/**
* Unmounts the specified volume.
*
* @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
* @param {function(boolean)} callback Function receives true on success.
*/
test.util.async.unmount = function(volumeType, callback) {
volumeManagerFactory.getInstance().then((volumeManager) => {
const volumeInfo = volumeManager.getCurrentProfileVolumeInfo(volumeType);
if (volumeInfo) {
volumeManager.unmount(
volumeInfo, callback.bind(null, true), callback.bind(null, false));
}
});
};
/**
* Remote call API handler. When loaded, this replaces the declaration in
* test_util_base.js.
* @param {*} request
* @param {function(*):void} sendResponse
*/
test.util.executeTestMessage = function(request, sendResponse) {
window.IN_TEST = true;
// Check the function name.
if (!request.func || request.func[request.func.length - 1] == '_') {
request.func = '';
}
// Prepare arguments.
if (!('args' in request))
throw new Error('Invalid request.');
var args = request.args.slice(); // shallow copy
if (request.appId) {
if (window.appWindows[request.appId]) {
args.unshift(window.appWindows[request.appId].contentWindow);
} else if (window.background.dialogs[request.appId]) {
args.unshift(window.background.dialogs[request.appId]);
} else {
console.error('Specified window not found: ' + request.appId);
return false;
}
}
// Call the test utility function and respond the result.
if (test.util.async[request.func]) {
args[test.util.async[request.func].length - 1] = function() {
console.debug('Received the result of ' + request.func);
sendResponse.apply(null, arguments);
};
console.debug('Waiting for the result of ' + request.func);
test.util.async[request.func].apply(null, args);
return true;
} else if (test.util.sync[request.func]) {
sendResponse(test.util.sync[request.func].apply(null, args));
return false;
} else {
console.error('Invalid function name.');
return false;
}
};
/**
* Returns the MetadataStats collected in MetadataModel, it will be serialized
* as a plain object when sending to test extension.
*
* @suppress {missingProperties} metadataStats is only defined for foreground
* Window so it isn't visible in the background. Here it will return as JSON
* object to test extension.
*/
test.util.sync.getMetadataStats = function(contentWindow) {
return contentWindow.fileManager.metadataModel.getStats();
};
/**
* Returns true when FileManager has finished loading, by checking the attribute
* "loaded" on its root element.
*/
test.util.sync.isFileManagerLoaded = function(contentWindow) {
if (contentWindow && contentWindow.fileManager &&
contentWindow.fileManager.ui)
return contentWindow.fileManager.ui.element.hasAttribute('loaded');
return false;
};
/**
* Reports to the given |callback| the number of volumes available in
* VolumeManager in the background page.
*
* @param {function(number)} callback Callback function to be called with the
* number of volumes.
*/
test.util.async.getVolumesCount = function(callback) {
return volumeManagerFactory.getInstance().then((volumeManager) => {
callback(volumeManager.volumeInfoList.length);
});
};
......@@ -7,61 +7,6 @@
*/
var test = test || {};
/**
* Extract the information of the given element.
* @param {Element} element Element to be extracted.
* @param {Window} contentWindow Window to be tested.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained. NOTE: Causes element style re-calculation.
* @return {{attributes:Object<string>, text:string,
* styles:(Object<string>|undefined), hidden:boolean}} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
function extractElementInfo(element, contentWindow, opt_styleNames) {
const attributes = {};
for (let i = 0; i < element.attributes.length; i++) {
attributes[element.attributes[i].nodeName] =
element.attributes[i].nodeValue;
}
const result = {
attributes: attributes,
text: element.textContent,
value: element.value,
// The hidden attribute is not in the element.attributes even if
// element.hasAttribute('hidden') is true.
hidden: !!element.hidden,
hasShadowRoot: !!element.shadowRoot
};
const styleNames = opt_styleNames || [];
assert(Array.isArray(styleNames));
if (!styleNames.length)
return result;
const styles = {};
const size = element.getBoundingClientRect();
const computedStyles = contentWindow.getComputedStyle(element);
for (let i = 0; i < styleNames.length; i++) {
styles[styleNames[i]] = computedStyles[styleNames[i]];
}
result.styles = styles;
// These attributes are set when element is <img> or <canvas>.
result.imageWidth = Number(element.width);
result.imageHeight = Number(element.height);
// These attributes are set in any element.
result.renderedWidth = size.width;
result.renderedHeight = size.height;
result.renderedTop = size.top;
result.renderedLeft = size.left;
return result;
}
/**
* Namespace for test utility functions.
*
......@@ -85,651 +30,94 @@ test.util.sync = {};
test.util.async = {};
/**
* Loaded at runtime and invoked by the external message listener to handle
* remote call requests.
* @type{?function(*, function(*): void)}
*/
test.util.executeTestMessage = null;
/**
* Registers message listener, which runs test utility functions.
*/
test.util.registerRemoteTestUtils = function() {
/**
* Responses that couldn't be sent while waiting for test scripts to load.
* Null if there is no load in progress.
* @type{Array<function(*)>}
*/
let responsesWaitingForLoad = null;
// Return true for asynchronous functions and false for synchronous.
chrome.runtime.onMessageExternal.addListener(function(
request, sender, sendResponse) {
/**
* List of extension ID of the testing extension.
* @type {Array<string>}
* @const
*/
test.util.TESTING_EXTENSION_IDS = [
const kTestingExtensionIds = [
'oobinhbdbiehknkpbpejbbpdbkdjmoco', // File Manager test extension.
'ejhcmmdhhpdhhgmifplfmjobgegbibkn', // Gallery test extension.
'ljoplibgfehghmibaoaepfagnmbbfiga', // Video Player test extension.
'ddabbgbggambiildohfagdkliahiecfl', // Audio Player test extension.
];
/**
* Obtains window information.
*
* @return {Object<{innerWidth:number, innerHeight:number}>} Map window
* ID and window information.
*/
test.util.sync.getWindows = function() {
var windows = {};
for (var id in window.appWindows) {
var windowWrapper = window.appWindows[id];
windows[id] = {
outerWidth: windowWrapper.contentWindow.outerWidth,
outerHeight: windowWrapper.contentWindow.outerHeight
};
}
for (var id in window.background.dialogs) {
windows[id] = {
outerWidth: window.background.dialogs[id].outerWidth,
outerHeight: window.background.dialogs[id].outerHeight
};
}
return windows;
};
/**
* Closes the specified window.
*
* @param {string} appId AppId of window to be closed.
* @return {boolean} Result: True if success, false otherwise.
*/
test.util.sync.closeWindow = function(appId) {
if (appId in window.appWindows &&
window.appWindows[appId].contentWindow) {
window.appWindows[appId].close();
return true;
}
return false;
};
/**
* Gets total Javascript error count from background page and each app window.
* @return {number} Error count.
*/
test.util.sync.getErrorCount = function() {
var totalCount = window.JSErrorCount;
for (var appId in window.appWindows) {
var contentWindow = window.appWindows[appId].contentWindow;
if (contentWindow.JSErrorCount)
totalCount += contentWindow.JSErrorCount;
}
return totalCount;
};
/**
* Resizes the window to the specified dimensions.
*
* @param {Window} contentWindow Window to be tested.
* @param {number} width Window width.
* @param {number} height Window height.
* @return {boolean} True for success.
*/
test.util.sync.resizeWindow = function(contentWindow, width, height) {
window.appWindows[contentWindow.appID].resizeTo(width, height);
return true;
};
/**
* Maximizes the window.
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True for success.
*/
test.util.sync.maximizeWindow = function(contentWindow) {
window.appWindows[contentWindow.appID].maximize();
return true;
};
/**
* Restores the window state (maximized/minimized/etc...).
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True for success.
*/
test.util.sync.restoreWindow = function(contentWindow) {
window.appWindows[contentWindow.appID].restore();
return true;
};
/**
* Returns whether the window is miximized or not.
* @param {Window} contentWindow Window to be tested.
* @return {boolean} True if the window is maximized now.
*/
test.util.sync.isWindowMaximized = function(contentWindow) {
return window.appWindows[contentWindow.appID].isMaximized();
};
/**
* Queries all elements.
*
* @param {!Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {!Array<{attributes:Object<string>, text:string,
* styles:Object<string>, hidden:boolean}>} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
test.util.sync.queryAllElements = function(
contentWindow, targetQuery, opt_styleNames) {
return test.util.sync.deepQueryAllElements(
contentWindow, [targetQuery], opt_styleNames);
};
/**
* Queries elements inside shadow DOM.
*
* @param {!Window} contentWindow Window to be tested.
* @param {!Array<string>} targetQuery Query to specify the element.
* |targetQuery[0]| specifies the first element(s). |targetQuery[1]| specifies
* elements inside the shadow DOM of the first element, and so on.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {!Array<{attributes:Object<string>, text:string,
* styles:Object<string>, hidden:boolean}>} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values.
*/
test.util.sync.deepQueryAllElements = function(
contentWindow, targetQuery, opt_styleNames) {
if (!contentWindow.document)
return [];
var elems =
test.util.sync.deepQuerySelectorAll_(contentWindow.document, targetQuery);
return elems.map(function(element) {
return extractElementInfo(element, contentWindow, opt_styleNames);
});
};
/**
* Selects elements below |root|, possibly following shadow DOM subtree.
*
* @param {(!HTMLElement|!Document)} root Element to search from.
* @param {!Array<string>} targetQuery Query to specify the element.
* |targetQuery[0]| specifies the first element(s). |targetQuery[1]| specifies
* elements inside the shadow DOM of the first element, and so on.
* @return {!Array<!HTMLElement>} Matched elements.
*
* @private
*/
test.util.sync.deepQuerySelectorAll_ = function(root, targetQuery) {
var elems = Array.prototype.slice.call(root.querySelectorAll(targetQuery[0]));
var remaining = targetQuery.slice(1);
if (remaining.length === 0)
return elems;
var res = [];
for (var i = 0; i < elems.length; i++) {
if (elems[i].shadowRoot) {
res = res.concat(
test.util.sync.deepQuerySelectorAll_(elems[i].shadowRoot, remaining));
}
}
return res;
};
/**
* Executes a script in the context of the first <webview> element contained in
* the window, including shadow DOM subtrees if given, and returns the script
* result via the callback.
*
* @param {Window} contentWindow Window to be tested.
* @param {!Array<string>} targetQuery Query for the <webview> element.
* |targetQuery[0]| specifies the first element. |targetQuery[1]| specifies
* an element inside the shadow DOM of the first element, etc. The last
* targetQuery item must return the <webview> element.
* @param {string} script Javascript code to be executed within the <webview>.
* @param {function(*)} callback Callback function to be called with the
* result of the |script|.
*/
test.util.async.deepExecuteScriptInWebView = function(
contentWindow, targetQuery, script, callback) {
const webviews =
test.util.sync.deepQuerySelectorAll_(contentWindow.document, targetQuery);
if (!webviews || webviews.length !== 1)
throw new Error('<webview> not found: [' + targetQuery.join(',') + ']');
const webview = /** @type {WebView} */ (webviews[0]);
webview.executeScript({code: script}, callback);
};
/**
* Gets the information of the active element.
*
* @param {Window} contentWindow Window to be tested.
* @param {Array<string>=} opt_styleNames List of CSS property name to be
* obtained.
* @return {?{attributes:Object<string>, text:string,
* styles:(Object<string>|undefined), hidden:boolean}} Element
* information that contains contentText, attribute names and
* values, hidden attribute, and style names and values. If there is no
* active element, returns null.
*/
test.util.sync.getActiveElement = function(contentWindow, opt_styleNames) {
if (!contentWindow.document || !contentWindow.document.activeElement)
return null;
return extractElementInfo(
contentWindow.document.activeElement, contentWindow, opt_styleNames);
};
/**
* Assigns the text to the input element.
* @param {Window} contentWindow Window to be tested.
* @param {string} query Query for the input element.
* @param {string} text Text to be assigned.
*/
test.util.sync.inputText = function(contentWindow, query, text) {
var input = contentWindow.document.querySelector(query);
input.value = text;
};
/**
* Sends an event to the element specified by |targetQuery| or active element.
*
* @param {Window} contentWindow Window to be tested.
* @param {?string|Array<string>} targetQuery Query to specify the element.
* If this value is null, an event is dispatched to active element of the
* document.
* If targetQuery is an array, |targetQuery[0]| specifies the first
* element(s), |targetQuery[1]| specifies elements inside the shadow DOM of
* the first element, and so on.
* @param {!Event} event Event to be sent.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.sendEvent = function(contentWindow, targetQuery, event) {
if (!contentWindow.document)
return false;
let target;
if (targetQuery === null) {
target = contentWindow.document.activeElement;
} else if (typeof targetQuery === 'string') {
target = contentWindow.document.querySelector(targetQuery);
} else if (Array.isArray(targetQuery)) {
let elems = test.util.sync.deepQuerySelectorAll_(
contentWindow.document, targetQuery);
if (elems.length > 0)
target = elems[0];
}
if (!target)
return false;
target.dispatchEvent(event);
return true;
};
/**
* Sends an fake event having the specified type to the target query.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @param {string} eventType Type of event.
* @param {Object=} opt_additionalProperties Object contaning additional
* properties.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeEvent = function(contentWindow,
targetQuery,
eventType,
opt_additionalProperties) {
var event = new Event(eventType,
/** @type {!EventInit} */ (opt_additionalProperties || {}));
if (opt_additionalProperties) {
for (var name in opt_additionalProperties) {
event[name] = opt_additionalProperties[name];
}
}
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
];
/**
* Sends a fake key event to the element specified by |targetQuery| or active
* element with the given |key| and optional |ctrl,shift,alt| modifier.
*
* @param {Window} contentWindow Window to be tested.
* @param {?string} targetQuery Query to specify the element. If this value is
* null, key event is dispatched to active element of the document.
* @param {string} key DOM UI Events key value.
* @param {boolean} ctrl Whether CTRL should be pressed, or not.
* @param {boolean} shift whether SHIFT should be pressed, or not.
* @param {boolean} alt whether ALT should be pressed, or not.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeKeyDown = function(
contentWindow, targetQuery, key, ctrl, shift, alt) {
const event = new KeyboardEvent('keydown', {
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
key: key,
ctrlKey: ctrl,
shiftKey: shift,
altKey: alt
});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Simulates a fake mouse click (left button, single click) on the element
* specified by |targetQuery|. If the element has the click method, just calls
* it. Otherwise, this sends 'mouseover', 'mousedown', 'mouseup' and 'click'
* events in turns.
*
* @param {Window} contentWindow Window to be tested.
* @param {string|Array<string>} targetQuery Query to specify the element.
* If targetQuery is an array, |targetQuery[0]| specifies the first
* element(s), |targetQuery[1]| specifies elements inside the shadow DOM of
* the first element, and so on.
* @param {{shift: boolean, alt: boolean, ctrl: boolean}=} opt_keyModifiers Object
* contaning common key modifiers : shift, alt, and ctrl.
* @return {boolean} True if the all events are sent to the target, false
* otherwise.
*/
test.util.sync.fakeMouseClick = function(contentWindow, targetQuery, opt_keyModifiers) {
const modifiers = opt_keyModifiers || {};
const props = {
bubbles: true,
detail: 1,
composed: true, // Allow the event to bubble past shadow DOM root.
ctrlKey: modifiers.ctrl,
shiftKey: modifiers.shift,
altKey: modifiers.alt,
};
const mouseOverEvent = new MouseEvent('mouseover', props);
const resultMouseOver =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseOverEvent);
const mouseDownEvent = new MouseEvent('mousedown', props);
const resultMouseDown =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent);
const mouseUpEvent = new MouseEvent('mouseup', props);
const resultMouseUp =
test.util.sync.sendEvent(contentWindow, targetQuery, mouseUpEvent);
const clickEvent = new MouseEvent('click', props);
const resultClick =
test.util.sync.sendEvent(contentWindow, targetQuery, clickEvent);
return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
};
/**
* Simulates a fake mouse click (right button, single click) on the element
* specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false
* otherwise.
*/
test.util.sync.fakeMouseRightClick = function(contentWindow, targetQuery) {
const mouseDownEvent =
new MouseEvent('mousedown', {bubbles: true, button: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent)) {
return false;
}
const contextMenuEvent =
new MouseEvent('contextmenu', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, contextMenuEvent);
};
/**
* Simulates a fake touch event (touch start, touch end) on the element
* specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false
* otherwise.
*/
test.util.sync.fakeTouchClick = function(contentWindow, targetQuery) {
const touchStartEvent = new TouchEvent('touchstart');
if (!test.util.sync.sendEvent(contentWindow, targetQuery, touchStartEvent)) {
return false;
}
const mouseDownEvent =
new MouseEvent('mousedown', {bubbles: true, button: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, mouseDownEvent)) {
return false;
}
const touchEndEvent = new TouchEvent('touchend');
if (!test.util.sync.sendEvent(contentWindow, targetQuery, touchEndEvent)) {
return false;
}
const contextMenuEvent =
new MouseEvent('contextmenu', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, contextMenuEvent);
};
/**
* Simulates a fake double click event (left button) to the element specified by
* |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseDoubleClick = function(contentWindow, targetQuery) {
// Double click is always preceded with a single click.
if (!test.util.sync.fakeMouseClick(contentWindow, targetQuery)) {
return false;
// Check the sender.
if (!sender.id || kTestingExtensionIds.indexOf(sender.id) === -1) {
// Silently return. Don't return false; that short-circuits the
// propagation of messages, and there are now other listeners that want to
// handle external messages.
return;
}
// Send the second click event, but with detail equal to 2 (number of clicks)
// in a row.
let event =
new MouseEvent('click', {bubbles: true, detail: 2, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, event)) {
return false;
if (window.IN_TEST) {
// If there are multiple foreground windows, the remote call API may have
// already been initialised by one of them, so just return true to tell
// the other window we are ready.
if ('enableTesting' in request) {
sendResponse(true);
return false; // No need to keep the connection alive.
}
// Send the double click event.
event = new MouseEvent('dblclick', {bubbles: true, composed: true});
if (!test.util.sync.sendEvent(contentWindow, targetQuery, event)) {
return false;
return test.util.executeTestMessage(request, sendResponse);
}
return true;
};
// When a valid test extension connects, the first message sent must be an
// enable tests request.
if (!('enableTesting' in request))
throw new Error('Expected enableTesting');
/**
* Sends a fake mouse down event to the element specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseDown = function(contentWindow, targetQuery) {
const event = new MouseEvent('mousedown', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Sends a fake mouse up event to the element specified by |targetQuery|.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if the event is sent to the target, false otherwise.
*/
test.util.sync.fakeMouseUp = function(contentWindow, targetQuery) {
const event = new MouseEvent('mouseup', {bubbles: true, composed: true});
return test.util.sync.sendEvent(contentWindow, targetQuery, event);
};
/**
* Focuses to the element specified by |targetQuery|. This method does not
* provide any guarantee whether the element is actually focused or not.
*
* @param {Window} contentWindow Window to be tested.
* @param {string} targetQuery Query to specify the element.
* @return {boolean} True if focus method of the element has been called, false
* otherwise.
*/
test.util.sync.focus = function(contentWindow, targetQuery) {
var target = contentWindow.document &&
contentWindow.document.querySelector(targetQuery);
if (!target)
return false;
target.focus();
if (responsesWaitingForLoad != null) {
// Loading started, but not complete. Queue the response.
responsesWaitingForLoad.push(sendResponse);
return true;
};
/**
* Obtains the list of notification ID.
* @param {function(Object<boolean>)} callback Callback function with
* results returned by the script.
*/
test.util.async.getNotificationIDs = function(callback) {
chrome.notifications.getAll(callback);
};
/**
* Opens the file URL. It emulates the interaction that Launcher search does
* from a search result, it triggers the background page's event listener that
* listens to evens from launcher_search_provider API.
*
* @param {string} fileURL File URL to open by Files app background dialog.
* @suppress {accessControls|missingProperties} Closure disallow calling private
* launcherSearch_, but here we just want to emulate the behaviour, so we don't
* need to make this attribute public. Also the interface
* "FileBrowserBackground" doesn't define the attributes "launcherSearch_" so we
* need to suppress missingProperties.
*/
test.util.sync.launcherSearchOpenResult = function(fileURL) {
window.background.launcherSearch_.onOpenResult_(fileURL);
};
/**
* Gets file entries just under the volume.
*
* @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
* @param {Array<string>} names File name list.
* @param {function(*)} callback Callback function with results returned by the
* script.
*/
test.util.async.getFilesUnderVolume = function(volumeType, names, callback) {
var displayRootPromise =
volumeManagerFactory.getInstance().then(function(volumeManager) {
var volumeInfo = volumeManager.getCurrentProfileVolumeInfo(volumeType);
return volumeInfo.resolveDisplayRoot();
});
var retrievePromise = displayRootPromise.then(function(displayRoot) {
var filesPromise = names.map(function(name) {
return new Promise(
displayRoot.getFile.bind(displayRoot, name, {}));
});
return Promise.all(filesPromise).then(function(aa) {
return util.entriesToURLs(aa);
}).catch(function() {
return [];
});
});
retrievePromise.then(callback);
};
/**
* Unmounts the specified volume.
*
* @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
* @param {function(boolean)} callback Function receives true on success.
*/
test.util.async.unmount = function(volumeType, callback) {
volumeManagerFactory.getInstance().then((volumeManager) => {
const volumeInfo = volumeManager.getCurrentProfileVolumeInfo(volumeType);
if (volumeInfo) {
volumeManager.unmount(
volumeInfo, callback.bind(null, true), callback.bind(null, false));
}
responsesWaitingForLoad = [];
let script = document.createElement('script');
document.body.appendChild(script);
script.onload = function() {
// The runtime load should have populated test.util with
// executeTestMessage, allowing it to be invoked on the next
// onMessageExternal call.
sendResponse(true);
responsesWaitingForLoad.forEach((queuedResponse) => {
queuedResponse(true);
});
};
responsesWaitingForLoad = null;
/**
* Registers message listener, which runs test utility functions.
*/
test.util.registerRemoteTestUtils = function() {
// Return true for asynchronous functions and false for synchronous.
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
// Check the sender.
if (!sender.id ||
test.util.TESTING_EXTENSION_IDS.indexOf(sender.id) === -1) {
// Silently return. Don't return false; that short-circuits the
// propagation of messages, and there are now other listeners that want to
// handle external messages.
return;
}
// Set a global flag that we are in tests, so other components are aware
// of it.
window.IN_TEST = true;
// Check the function name.
if (!request.func || request.func[request.func.length - 1] == '_') {
request.func = '';
}
// Prepare arguments.
if (!('args' in request))
throw new Error('Invalid request.');
var args = request.args.slice(); // shallow copy
if (request.appId) {
if (window.appWindows[request.appId]) {
args.unshift(window.appWindows[request.appId].contentWindow);
} else if (window.background.dialogs[request.appId]) {
args.unshift(window.background.dialogs[request.appId]);
} else {
console.error('Specified window not found: ' + request.appId);
return false;
}
}
// Call the test utility function and respond the result.
if (test.util.async[request.func]) {
args[test.util.async[request.func].length - 1] = function() {
console.debug('Received the result of ' + request.func);
sendResponse.apply(null, arguments);
};
console.debug('Waiting for the result of ' + request.func);
test.util.async[request.func].apply(null, args);
script.onerror = function(/** Event */ event) {
console.error('Script load failed ' + event);
throw new Error('Script load failed.');
};
const kFileManagerExtension =
'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
const kTestScriptUrl =
kFileManagerExtension + '/background/js/runtime_loaded_test_util.js';
console.log('Loading ' + kTestScriptUrl);
script.src = kTestScriptUrl;
return true;
} else if (test.util.sync[request.func]) {
sendResponse(test.util.sync[request.func].apply(null, args));
return false;
} else {
console.error('Invalid function name.');
return false;
}
});
};
/**
* Returns the MetadataStats collected in MetadataModel, it will be serialized
* as a plain object when sending to test extension.
*
* @suppress {missingProperties} metadataStats is only defined for foreground
* Window so it isn't visible in the background. Here it will return as JSON
* object to test extension.
*/
test.util.sync.getMetadataStats = function(contentWindow) {
return contentWindow.fileManager.metadataModel.getStats();
};
/**
* Returns true when FileManager has finished loading, by checking the attribute
* "loaded" on its root element.
*/
test.util.sync.isFileManagerLoaded = function(contentWindow) {
if (contentWindow && contentWindow.fileManager &&
contentWindow.fileManager.ui)
return contentWindow.fileManager.ui.element.hasAttribute('loaded');
return false;
};
/**
* Reports to the given |callback| the number of volumes available in
* VolumeManager in the background page.
*
* @param {function(number)} callback Callback function to be called with the
* number of volumes.
*/
test.util.async.getVolumesCount = function(callback) {
return volumeManagerFactory.getInstance().then((volumeManager) => {
callback(volumeManager.volumeInfoList.length);
});
};
......@@ -175,6 +175,7 @@
"foreground/elements/files_toggle_ripple.js",
"foreground/elements/files_tooltip.html",
"foreground/elements/files_tooltip.js",
"background/js/runtime_loaded_test_util.js",
"background/js/background_common_scripts.js",
"foreground/js/metadata/byte_reader.js",
"foreground/js/metadata/exif_parser.js",
......
......@@ -25,7 +25,7 @@ js_library("strings") {
js_library("test_util") {
deps = [
":chrome_file_manager_private_test_impl",
"../../background/js:test_util_base",
"../../background/js:runtime_loaded_test_util",
"../../foreground/js:constants",
"//ui/webui/resources/js:webui_resource_test",
]
......
......@@ -149,6 +149,13 @@ scripts += ['<script src="%s%s"></script>' % (ROOT, s) for s in [
includes2scripts('foreground/js/main_scripts.js')
includes2scripts('background/js/background_common_scripts.js')
includes2scripts('background/js/background_scripts.js')
# test_util_base.js in background_common_scripts.js loads this at runtime.
# However, test/js/test_util.js copies some functions from it into its own
# test context, so provide it here.
scripts += ['<script src="%s%s"></script>' %
(ROOT, 'background/js/runtime_loaded_test_util.js')]
main_html = replaceline(main_html, 'foreground/js/main_scripts.js', [
('<link rel="import" href="%s../../../third_party/polymer/v1_0/'
'components-chromium/polymer/polymer.html">' % ROOT),
......@@ -157,7 +164,6 @@ main_html = replaceline(main_html, 'foreground/js/main_scripts.js', [
"<script>var FILE_MANAGER_ROOT = '%s';</script>" % ROOT,
] + scripts)
# Get strings from grdp files. Remove any ph/ex elements before getting text.
# Parse private_api_strings.cc to match the string name to the grdp message.
strings = {}
......
......@@ -20,6 +20,7 @@
<!-- Common Scripts. -->
<include name="IDR_FILE_MANAGER_BACKGROUND_COMMON_JS" file="file_manager/background/js/background_common_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_BACKGROUND_RUNTIME_LOADED_TEST_UTIL_JS" file="file_manager/background/js/runtime_loaded_test_util.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_ANALYTICS_JS" file="../webui/resources/js/analytics.js" flattenhtml="false" type="BINDATA" />
<!-- Polymer elements -->
......
......@@ -25,16 +25,49 @@ function autoStep() {
*/
function RemoteCall(extensionId) {
this.extensionId_ = extensionId;
this.testRuntimeLoaded_ = false;
/**
* Tristate holding the cached result of isStepByStepEnabled_().
* @type{?bool}
*/
this.cachedStepByStepEnabled_ = null;
}
/**
* Checks whether step by step tests are enabled or not.
* @private
* @return {Promise<bool>}
*/
RemoteCall.isStepByStepEnabled = function() {
return new Promise(function(fulfill) {
RemoteCall.prototype.isStepByStepEnabled_ = function() {
if (this.cachedStepByStepEnabled_ != null)
return Promise.resolve(this.cachedStepByStepEnabled_);
return new Promise((fulfill) => {
chrome.commandLinePrivate.hasSwitch(
'enable-file-manager-step-by-step-tests', fulfill);
'enable-file-manager-step-by-step-tests', (/** bool */ result) => {
this.cachedStepByStepEnabled_ = result;
fulfill(result);
});
});
};
/**
* Asks the extension under test to load its testing functions.
* @private
* @return {Promise<bool>}
*/
RemoteCall.prototype.ensureLoaded_ = function() {
if (this.testRuntimeLoaded_)
return Promise.resolve(true);
return new Promise((fulfill) => {
chrome.runtime.sendMessage(
this.extensionId_, {enableTesting: true}, {}, (/** bool */ success) => {
chrome.test.assertTrue(success);
this.testRuntimeLoaded_ = success;
fulfill(success);
});
});
};
......@@ -50,12 +83,14 @@ RemoteCall.isStepByStepEnabled = function() {
* @return {Promise} Promise to be fulfilled with the result of the remote
* utility.
*/
RemoteCall.prototype.callRemoteTestUtil =
function(func, appId, args, opt_callback) {
return RemoteCall.isStepByStepEnabled().then(function(stepByStep) {
RemoteCall.prototype.callRemoteTestUtil = function(
func, appId, args, opt_callback) {
return this.ensureLoaded_()
.then(this.isStepByStepEnabled_.bind(this))
.then((stepByStep) => {
if (!stepByStep)
return false;
return new Promise(function(onFulfilled) {
return new Promise((onFulfilled) => {
console.info('Executing: ' + func + ' on ' + appId + ' with args: ');
console.info(args);
if (window.autostep !== true) {
......@@ -69,16 +104,11 @@ RemoteCall.prototype.callRemoteTestUtil =
onFulfilled(stepByStep);
}
});
}).then(function(stepByStep) {
return new Promise(function(onFulfilled) {
})
.then((stepByStep) => {
return new Promise((onFulfilled) => {
chrome.runtime.sendMessage(
this.extensionId_,
{
func: func,
appId: appId,
args: args
},
{},
this.extensionId_, {func: func, appId: appId, args: args}, {},
function(var_args) {
if (stepByStep) {
console.info('Returned value:');
......@@ -88,8 +118,8 @@ RemoteCall.prototype.callRemoteTestUtil =
opt_callback.apply(null, arguments);
onFulfilled(arguments[0]);
});
}.bind(this));
}.bind(this));
});
});
};
/**
......
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