1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * Namespace for test related things.
11 * Namespace for test utility functions.
13 * Public functions in the test.util.sync and the test.util.async namespaces are
14 * published to test cases and can be called by using callRemoteTestUtil. The
15 * arguments are serialized as JSON internally. If application ID is passed to
16 * callRemoteTestUtil, the content window of the application is added as the
17 * first argument. The functions in the test.util.async namespace are passed the
18 * callback function as the last argument.
23 * Namespace for synchronous utility functions.
28 * Namespace for asynchronous utility functions.
33 * Extension ID of the testing extension.
37 test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco';
40 * Opens the main Files.app's window and waits until it is ready.
42 * @param {Object} appState App state.
43 * @param {function(string)} callback Completion callback with the new window's
46 test.util.async.openMainWindow = function(appState, callback) {
47 launchFileManager(appState,
48 undefined, // opt_type
54 * Obtains window information.
56 * @return {Object.<string, {innerWidth:number, innerHeight:number}>} Map window
57 * ID and window information.
59 test.util.sync.getWindows = function() {
61 for (var id in background.appWindows) {
62 var windowWrapper = background.appWindows[id];
64 outerWidth: windowWrapper.contentWindow.outerWidth,
65 outerHeight: windowWrapper.contentWindow.outerHeight
68 for (var id in background.dialogs) {
70 outerWidth: background.dialogs[id].outerWidth,
71 outerHeight: background.dialogs[id].outerHeight
78 * Closes the specified window.
80 * @param {string} appId AppId of window to be closed.
81 * @return {boolean} Result: True if success, false otherwise.
83 test.util.sync.closeWindow = function(appId) {
84 if (appId in background.appWindows &&
85 background.appWindows[appId].contentWindow) {
86 background.appWindows[appId].close();
93 * Gets a document in the Files.app's window, including iframes.
95 * @param {Window} contentWindow Window to be used.
96 * @param {string=} opt_iframeQuery Query for the iframe.
97 * @return {Document=} Returns the found document or undefined if not found.
100 test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) {
101 if (opt_iframeQuery) {
102 var iframe = contentWindow.document.querySelector(opt_iframeQuery);
103 return iframe && iframe.contentWindow && iframe.contentWindow.document;
106 return contentWindow.document;
110 * Gets total Javascript error count from background page and each app window.
111 * @return {number} Error count.
113 test.util.sync.getErrorCount = function() {
114 var totalCount = JSErrorCount;
115 for (var appId in background.appWindows) {
116 var contentWindow = background.appWindows[appId].contentWindow;
117 if (contentWindow.JSErrorCount)
118 totalCount += contentWindow.JSErrorCount;
124 * Resizes the window to the specified dimensions.
126 * @param {Window} contentWindow Window to be tested.
127 * @param {number} width Window width.
128 * @param {number} height Window height.
129 * @return {boolean} True for success.
131 test.util.sync.resizeWindow = function(contentWindow, width, height) {
132 background.appWindows[contentWindow.appID].resizeTo(width, height);
137 * Returns an array with the files currently selected in the file manager.
138 * TODO(hirono): Integrate the method into getFileList method.
140 * @param {Window} contentWindow Window to be tested.
141 * @return {Array.<string>} Array of selected files.
143 test.util.sync.getSelectedFiles = function(contentWindow) {
144 var table = contentWindow.document.querySelector('#detail-table');
145 var rows = table.querySelectorAll('li');
147 for (var i = 0; i < rows.length; ++i) {
148 if (rows[i].hasAttribute('selected')) {
150 rows[i].querySelector('.filename-label').textContent);
157 * Returns an array with the files on the file manager's file list.
159 * @param {Window} contentWindow Window to be tested.
160 * @return {Array.<Array.<string>>} Array of rows.
162 test.util.sync.getFileList = function(contentWindow) {
163 var table = contentWindow.document.querySelector('#detail-table');
164 var rows = table.querySelectorAll('li');
166 for (var j = 0; j < rows.length; ++j) {
169 row.querySelector('.filename-label').textContent,
170 row.querySelector('.size').textContent,
171 row.querySelector('.type').textContent,
172 row.querySelector('.date').textContent
179 * Queries all elements.
181 * @param {Window} contentWindow Window to be tested.
182 * @param {string} targetQuery Query to specify the element.
183 * @param {?string} iframeQuery Iframe selector or null if no iframe.
184 * @param {Array.<string>=} opt_styleNames List of CSS property name to be
186 * @return {Array.<{attributes:Object.<string, string>, text:string,
187 * styles:Object.<string, string>, hidden:boolean}>} Element
188 * information that contains contentText, attribute names and
189 * values, hidden attribute, and style names and values.
191 test.util.sync.queryAllElements = function(
192 contentWindow, targetQuery, iframeQuery, opt_styleNames) {
193 var doc = test.util.sync.getDocument_(contentWindow, iframeQuery);
196 // The return value of querySelectorAll is not an array.
197 return Array.prototype.map.call(
198 doc.querySelectorAll(targetQuery),
201 for (var i = 0; i < element.attributes.length; i++) {
202 attributes[element.attributes[i].nodeName] =
203 element.attributes[i].nodeValue;
206 var styleNames = opt_styleNames || [];
207 var computedStyles = contentWindow.getComputedStyle(element);
208 for (var i = 0; i < styleNames.length; i++) {
209 styles[styleNames[i]] = computedStyles[styleNames[i]];
211 var text = element.textContent;
213 attributes: attributes,
216 // The hidden attribute is not in the element.attributes even if
217 // element.hasAttribute('hidden') is true.
218 hidden: !!element.hidden
224 * Assigns the text to the input element.
225 * @param {Window} contentWindow Window to be tested.
226 * @param {string} query Query for the input element.
227 * @param {string} text Text to be assigned.
229 test.util.sync.inputText = function(contentWindow, query, text) {
230 var input = contentWindow.document.querySelector(query);
235 * Fakes pressing the down arrow until the given |filename| is selected.
237 * @param {Window} contentWindow Window to be tested.
238 * @param {string} filename Name of the file to be selected.
239 * @return {boolean} True if file got selected, false otherwise.
241 test.util.sync.selectFile = function(contentWindow, filename) {
242 var rows = contentWindow.document.querySelectorAll('#detail-table li');
243 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Home', false);
244 for (var index = 0; index < rows.length; ++index) {
245 var selection = test.util.sync.getSelectedFiles(contentWindow);
246 if (selection.length === 1 && selection[0] === filename)
248 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false);
250 console.error('Failed to select file "' + filename + '"');
255 * Open the file by selectFile and fakeMouseDoubleClick.
257 * @param {Window} contentWindow Window to be tested.
258 * @param {string} filename Name of the file to be opened.
259 * @return {boolean} True if file got selected and a double click message is
260 * sent, false otherwise.
262 test.util.sync.openFile = function(contentWindow, filename) {
263 var query = '#file-list li.table-row[selected] .filename-label span';
264 return test.util.sync.selectFile(contentWindow, filename) &&
265 test.util.sync.fakeMouseDoubleClick(contentWindow, query);
269 * Selects a volume specified by its icon name
271 * @param {Window} contentWindow Window to be tested.
272 * @param {string} iconName Name of the volume icon.
273 * @param {function(boolean)} callback Callback function to notify the caller
274 * whether the target is found and mousedown and click events are sent.
276 test.util.async.selectVolume = function(contentWindow, iconName, callback) {
277 var query = '[volume-type-icon=' + iconName + ']';
278 var driveQuery = '[volume-type-icon=drive]';
279 var isDriveSubVolume = iconName == 'drive_recent' ||
280 iconName == 'drive_shared_with_me' ||
281 iconName == 'drive_offline';
282 var preSelection = false;
284 checkQuery: function() {
285 if (contentWindow.document.querySelector(query)) {
289 // If the target volume is sub-volume of drive, we must click 'drive'
290 // before clicking the sub-item.
292 if (!isDriveSubVolume) {
296 if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) &&
297 test.util.sync.fakeMouseClick(contentWindow, driveQuery))) {
303 setTimeout(steps.checkQuery, 50);
305 sendEvents: function() {
306 // To change the selected volume, we have to send both events 'mousedown'
307 // and 'click' to the navigation list.
308 callback(test.util.sync.fakeMouseDown(contentWindow, query) &&
309 test.util.sync.fakeMouseClick(contentWindow, query));
316 * Executes Javascript code on a webview and returns the result.
318 * @param {Window} contentWindow Window to be tested.
319 * @param {string} webViewQuery Selector for the web view.
320 * @param {string} code Javascript code to be executed within the web view.
321 * @param {function(*)} callback Callback function with results returned by the
324 test.util.async.executeScriptInWebView = function(
325 contentWindow, webViewQuery, code, callback) {
326 var webView = contentWindow.document.querySelector(webViewQuery);
327 webView.executeScript({code: code}, callback);
331 * Sends an event to the element specified by |targetQuery|.
333 * @param {Window} contentWindow Window to be tested.
334 * @param {string} targetQuery Query to specify the element.
335 * @param {Event} event Event to be sent.
336 * @param {string=} opt_iframeQuery Optional iframe selector.
337 * @return {boolean} True if the event is sent to the target, false otherwise.
339 test.util.sync.sendEvent = function(
340 contentWindow, targetQuery, event, opt_iframeQuery) {
341 var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery);
343 var target = doc.querySelector(targetQuery);
345 target.dispatchEvent(event);
349 console.error('Target element for ' + targetQuery + ' not found.');
354 * Sends an fake event having the specified type to the target query.
356 * @param {Window} contentWindow Window to be tested.
357 * @param {string} targetQuery Query to specify the element.
358 * @param {string} eventType Type of event.
359 * @param {Object=} opt_additionalProperties Object contaning additional
361 * @return {boolean} True if the event is sent to the target, false otherwise.
363 test.util.sync.fakeEvent = function(contentWindow,
366 opt_additionalProperties) {
367 var event = new Event(eventType, opt_additionalProperties || {});
368 if (opt_additionalProperties) {
369 for (var name in opt_additionalProperties) {
370 event[name] = opt_additionalProperties[name];
373 return test.util.sync.sendEvent(contentWindow, targetQuery, event);
377 * Sends a fake key event to the element specified by |targetQuery| with the
378 * given |keyIdentifier| and optional |ctrl| modifier to the file manager.
380 * @param {Window} contentWindow Window to be tested.
381 * @param {string} targetQuery Query to specify the element.
382 * @param {string} keyIdentifier Identifier of the emulated key.
383 * @param {boolean} ctrl Whether CTRL should be pressed, or not.
384 * @param {string=} opt_iframeQuery Optional iframe selector.
385 * @return {boolean} True if the event is sent to the target, false otherwise.
387 test.util.sync.fakeKeyDown = function(
388 contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) {
389 var event = new KeyboardEvent(
391 { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl });
392 return test.util.sync.sendEvent(
393 contentWindow, targetQuery, event, opt_iframeQuery);
397 * Simulates a fake mouse click (left button, single click) on the element
398 * specified by |targetQuery|. If the element has the click method, just calls
399 * it. Otherwise, this sends 'mouseover', 'mousedown', 'mouseup' and 'click'
402 * @param {Window} contentWindow Window to be tested.
403 * @param {string} targetQuery Query to specify the element.
404 * @param {string=} opt_iframeQuery Optional iframe selector.
405 * @return {boolean} True if the all events are sent to the target, false
408 test.util.sync.fakeMouseClick = function(
409 contentWindow, targetQuery, opt_iframeQuery) {
410 var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1});
411 var resultMouseOver = test.util.sync.sendEvent(
412 contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery);
413 var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1});
414 var resultMouseDown = test.util.sync.sendEvent(
415 contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery);
416 var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1});
417 var resultMouseUp = test.util.sync.sendEvent(
418 contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery);
419 var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1});
420 var resultClick = test.util.sync.sendEvent(
421 contentWindow, targetQuery, clickEvent, opt_iframeQuery);
422 return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
426 * Simulates a fake mouse click (right button, single click) on the element
427 * specified by |targetQuery|.
429 * @param {Window} contentWindow Window to be tested.
430 * @param {string} targetQuery Query to specify the element.
431 * @param {string=} opt_iframeQuery Optional iframe selector.
432 * @return {boolean} True if the event is sent to the target, false
435 test.util.sync.fakeMouseRightClick = function(
436 contentWindow, targetQuery, opt_iframeQuery) {
437 var contextMenuEvent = new MouseEvent('contextmenu', {bubbles: true});
438 var result = test.util.sync.sendEvent(
439 contentWindow, targetQuery, contextMenuEvent, opt_iframeQuery);
444 * Simulates a fake double click event (left button) to the element specified by
447 * @param {Window} contentWindow Window to be tested.
448 * @param {string} targetQuery Query to specify the element.
449 * @param {string=} opt_iframeQuery Optional iframe selector.
450 * @return {boolean} True if the event is sent to the target, false otherwise.
452 test.util.sync.fakeMouseDoubleClick = function(
453 contentWindow, targetQuery, opt_iframeQuery) {
454 // Double click is always preceded with a single click.
455 if (!test.util.sync.fakeMouseClick(
456 contentWindow, targetQuery, opt_iframeQuery)) {
460 // Send the second click event, but with detail equal to 2 (number of clicks)
462 var event = new MouseEvent('click', { bubbles: true, detail: 2 });
463 if (!test.util.sync.sendEvent(
464 contentWindow, targetQuery, event, opt_iframeQuery)) {
468 // Send the double click event.
469 var event = new MouseEvent('dblclick', { bubbles: true });
470 if (!test.util.sync.sendEvent(
471 contentWindow, targetQuery, event, opt_iframeQuery)) {
479 * Sends a fake mouse down event to the element specified by |targetQuery|.
481 * @param {Window} contentWindow Window to be tested.
482 * @param {string} targetQuery Query to specify the element.
483 * @param {string=} opt_iframeQuery Optional iframe selector.
484 * @return {boolean} True if the event is sent to the target, false otherwise.
486 test.util.sync.fakeMouseDown = function(
487 contentWindow, targetQuery, opt_iframeQuery) {
488 var event = new MouseEvent('mousedown', { bubbles: true });
489 return test.util.sync.sendEvent(
490 contentWindow, targetQuery, event, opt_iframeQuery);
494 * Sends a fake mouse up event to the element specified by |targetQuery|.
496 * @param {Window} contentWindow Window to be tested.
497 * @param {string} targetQuery Query to specify the element.
498 * @param {string=} opt_iframeQuery Optional iframe selector.
499 * @return {boolean} True if the event is sent to the target, false otherwise.
501 test.util.sync.fakeMouseUp = function(
502 contentWindow, targetQuery, opt_iframeQuery) {
503 var event = new MouseEvent('mouseup', { bubbles: true });
504 return test.util.sync.sendEvent(
505 contentWindow, targetQuery, event, opt_iframeQuery);
509 * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
511 * @param {Window} contentWindow Window to be tested.
512 * @param {string} filename Name of the file to be copied.
513 * @return {boolean} True if copying got simulated successfully. It does not
514 * say if the file got copied, or not.
516 test.util.sync.copyFile = function(contentWindow, filename) {
517 if (!test.util.sync.selectFile(contentWindow, filename))
520 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
521 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
526 * Selects |filename| and fakes pressing the Delete key.
528 * @param {Window} contentWindow Window to be tested.
529 * @param {string} filename Name of the file to be deleted.
530 * @return {boolean} True if deleting got simulated successfully. It does not
531 * say if the file got deleted, or not.
533 test.util.sync.deleteFile = function(contentWindow, filename) {
534 if (!test.util.sync.selectFile(contentWindow, filename))
537 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
542 * Execute a command on the document in the specified window.
544 * @param {Window} contentWindow Window to be tested.
545 * @param {string} command Command name.
546 * @return {boolean} True if the command is executed successfully.
548 test.util.sync.execCommand = function(contentWindow, command) {
549 return contentWindow.document.execCommand(command);
553 * Override the installWebstoreItem method in private api for test.
555 * @param {Window} contentWindow Window to be tested.
556 * @param {string} expectedItemId Item ID to be called this method with.
557 * @param {?string} intendedError Error message to be returned when the item id
558 * matches. 'null' represents no error.
559 * @return {boolean} Always return true.
561 test.util.sync.overrideInstallWebstoreItemApi =
562 function(contentWindow, expectedItemId, intendedError) {
563 var setLastError = function(message) {
564 contentWindow.chrome.runtime.lastError =
565 message ? {message: message} : null;
568 var installWebstoreItem = function(itemId, silentInstallation, callback) {
569 setTimeout(function() {
570 if (itemId !== expectedItemId) {
571 setLastError('Invalid Chrome Web Store item ID');
576 setLastError(intendedError);
581 test.util.executedTasks_ = [];
582 contentWindow.chrome.fileBrowserPrivate.installWebstoreItem =
588 * Override the task-related methods in private api for test.
590 * @param {Window} contentWindow Window to be tested.
591 * @param {Array.<Object>} taskList List of tasks to be returned in
592 * fileBrowserPrivate.getFileTasks().
593 * @return {boolean} Always return true.
595 test.util.sync.overrideTasks = function(contentWindow, taskList) {
596 var getFileTasks = function(urls, onTasks) {
597 // Call onTask asynchronously (same with original getFileTasks).
598 setTimeout(function() {
603 var executeTask = function(taskId, url) {
604 test.util.executedTasks_.push(taskId);
607 var setDefaultTask = function(taskId) {
608 for (var i = 0; i < taskList.length; i++) {
609 taskList[i].isDefault = taskList[i].taskId === taskId;
613 test.util.executedTasks_ = [];
614 contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks;
615 contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask;
616 contentWindow.chrome.fileBrowserPrivate.setDefaultTask = setDefaultTask;
621 * Obtains the list of executed tasks.
622 * @param {Window} contentWindow Window to be tested.
623 * @return {Array.<string>} List of executed task ID.
625 test.util.sync.getExecutedTasks = function(contentWindow) {
626 if (!test.util.executedTasks_) {
627 console.error('Please call overrideTasks() first.');
630 return test.util.executedTasks_;
634 * Invoke chrome.fileBrowserPrivate.visitDesktop(profileId) to cause window
637 * @param {Window} contentWindow Window to be tested.
638 * @param {string} profileId Destination profile's ID.
639 * @return {boolean} Always return true.
641 test.util.sync.visitDesktop = function(contentWindow, profileId) {
642 contentWindow.chrome.fileBrowserPrivate.visitDesktop(profileId);
647 * Runs the 'Move to profileId' menu.
649 * @param {Window} contentWindow Window to be tested.
650 * @param {string} profileId Destination profile's ID.
651 * @return {boolean} True if the menu is found and run.
653 test.util.sync.runVisitDesktopMenu = function(contentWindow, profileId) {
654 var list = contentWindow.document.querySelectorAll('.visit-desktop');
655 for (var i = 0; i < list.length; ++i) {
656 if (list[i].label.indexOf(profileId) != -1) {
657 var activateEvent = contentWindow.document.createEvent('Event');
658 activateEvent.initEvent('activate');
659 list[i].dispatchEvent(activateEvent);
667 * Registers message listener, which runs test utility functions.
669 test.util.registerRemoteTestUtils = function() {
670 // Register the message listener.
671 var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
672 chrome.extension.onMessageExternal;
673 // Return true for asynchronous functions and false for synchronous.
674 onMessage.addListener(function(request, sender, sendResponse) {
676 if (sender.id != test.util.TESTING_EXTENSION_ID) {
677 console.error('The testing extension must be white-listed.');
680 // Set a global flag that we are in tests, so other components are aware
682 window.IN_TEST = true;
683 // Check the function name.
684 if (!request.func || request.func[request.func.length - 1] == '_') {
687 // Prepare arguments.
688 var args = request.args.slice(); // shallow copy
690 if (background.appWindows[request.appId]) {
691 args.unshift(background.appWindows[request.appId].contentWindow);
692 } else if (background.dialogs[request.appId]) {
693 args.unshift(background.dialogs[request.appId]);
695 console.error('Specified window not found: ' + request.appId);
699 // Call the test utility function and respond the result.
700 if (test.util.async[request.func]) {
701 args[test.util.async[request.func].length - 1] = function() {
702 console.debug('Received the result of ' + request.func);
703 sendResponse.apply(null, arguments);
705 console.debug('Waiting for the result of ' + request.func);
706 test.util.async[request.func].apply(null, args);
708 } else if (test.util.sync[request.func]) {
709 sendResponse(test.util.sync[request.func].apply(null, args));
712 console.error('Invalid function name.');
718 // Register the test utils.
719 test.util.registerRemoteTestUtils();