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. 4 5/** 6 * Namespace for test related things. 7 */ 8var test = test || {}; 9 10/** 11 * Namespace for test utility functions. 12 * 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. 19 */ 20test.util = {}; 21 22/** 23 * Namespace for synchronous utility functions. 24 */ 25test.util.sync = {}; 26 27/** 28 * Namespace for asynchronous utility functions. 29 */ 30test.util.async = {}; 31 32/** 33 * Extension ID of the testing extension. 34 * @type {string} 35 * @const 36 */ 37test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco'; 38 39/** 40 * Interval of checking a condition in milliseconds. 41 * @type {number} 42 * @const 43 * @private 44 */ 45test.util.WAITTING_INTERVAL_ = 50; 46 47/** 48 * Repeats the function until it returns true. 49 * @param {function()} closure Function expected to return true. 50 * @private 51 */ 52test.util.repeatUntilTrue_ = function(closure) { 53 var step = function() { 54 if (closure()) 55 return; 56 setTimeout(step, test.util.WAITTING_INTERVAL_); 57 }; 58 step(); 59}; 60 61/** 62 * Opens the main Files.app's window and waits until it is ready. 63 * 64 * @param {Object} appState App state. 65 * @param {function(string)} callback Completion callback with the new window's 66 * App ID. 67 */ 68test.util.async.openMainWindow = function(appState, callback) { 69 var steps = [ 70 function() { 71 launchFileManager(appState, 72 undefined, // opt_type 73 undefined, // opt_id 74 steps.shift()); 75 }, 76 function(appId) { 77 test.util.repeatUntilTrue_(function() { 78 if (!background.appWindows[appId]) 79 return false; 80 var contentWindow = background.appWindows[appId].contentWindow; 81 var table = contentWindow.document.querySelector('#detail-table'); 82 if (!table) 83 return false; 84 callback(appId); 85 return true; 86 }); 87 } 88 ]; 89 steps.shift()(); 90}; 91 92/** 93 * Waits for a window with the specified App ID prefix. Eg. `files` will match 94 * windows such as files#0, files#1, etc. 95 * 96 * @param {string} appIdPrefix ID prefix of the requested window. 97 * @param {function(string)} callback Completion callback with the new window's 98 * App ID. 99 */ 100test.util.async.waitForWindow = function(appIdPrefix, callback) { 101 test.util.repeatUntilTrue_(function() { 102 for (var appId in background.appWindows) { 103 if (appId.indexOf(appIdPrefix) == 0 && 104 background.appWindows[appId].contentWindow) { 105 callback(appId); 106 return true; 107 } 108 } 109 return false; 110 }); 111}; 112 113/** 114 * Gets a document in the Files.app's window, including iframes. 115 * 116 * @param {Window} contentWindow Window to be used. 117 * @param {string=} opt_iframeQuery Query for the iframe. 118 * @return {Document=} Returns the found document or undefined if not found. 119 * @private 120 */ 121test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) { 122 if (opt_iframeQuery) { 123 var iframe = contentWindow.document.querySelector(opt_iframeQuery); 124 return iframe && iframe.contentWindow && iframe.contentWindow.document; 125 } 126 127 return contentWindow.document; 128}; 129 130/** 131 * Gets total Javascript error count from each app window. 132 * @return {number} Error count. 133 */ 134test.util.sync.getErrorCount = function() { 135 var totalCount = JSErrorCount; 136 for (var appId in background.appWindows) { 137 var contentWindow = background.appWindows[appId].contentWindow; 138 if (contentWindow.JSErrorCount) 139 totalCount += contentWindow.JSErrorCount; 140 } 141 return totalCount; 142}; 143 144/** 145 * Resizes the window to the specified dimensions. 146 * 147 * @param {Window} contentWindow Window to be tested. 148 * @param {number} width Window width. 149 * @param {number} height Window height. 150 * @return {boolean} True for success. 151 */ 152test.util.sync.resizeWindow = function(contentWindow, width, height) { 153 background.appWindows[contentWindow.appID].resizeTo(width, height); 154 return true; 155}; 156 157/** 158 * Returns an array with the files currently selected in the file manager. 159 * 160 * @param {Window} contentWindow Window to be tested. 161 * @return {Array.<string>} Array of selected files. 162 */ 163test.util.sync.getSelectedFiles = function(contentWindow) { 164 var table = contentWindow.document.querySelector('#detail-table'); 165 var rows = table.querySelectorAll('li'); 166 var selected = []; 167 for (var i = 0; i < rows.length; ++i) { 168 if (rows[i].hasAttribute('selected')) { 169 selected.push( 170 rows[i].querySelector('.filename-label').textContent); 171 } 172 } 173 return selected; 174}; 175 176/** 177 * Returns an array with the files on the file manager's file list. 178 * 179 * @param {Window} contentWindow Window to be tested. 180 * @return {Array.<Array.<string>>} Array of rows. 181 */ 182test.util.sync.getFileList = function(contentWindow) { 183 var table = contentWindow.document.querySelector('#detail-table'); 184 var rows = table.querySelectorAll('li'); 185 var fileList = []; 186 for (var j = 0; j < rows.length; ++j) { 187 var row = rows[j]; 188 fileList.push([ 189 row.querySelector('.filename-label').textContent, 190 row.querySelector('.size').textContent, 191 row.querySelector('.type').textContent, 192 row.querySelector('.date').textContent 193 ]); 194 } 195 return fileList; 196}; 197 198/** 199 * Checkes if the given label and path of the volume are selected. 200 * @param {Window} contentWindow Window to be tested. 201 * @param {string} label Correct label the selected volume should have. 202 * @param {string} path Correct path the selected volume should have. 203 * @return {boolean} True for success. 204 */ 205test.util.sync.checkSelectedVolume = function(contentWindow, label, path) { 206 var list = contentWindow.document.querySelector('#navigation-list'); 207 var rows = list.querySelectorAll('li'); 208 var selected = []; 209 for (var i = 0; i < rows.length; ++i) { 210 if (rows[i].hasAttribute('selected')) 211 selected.push(rows[i]); 212 } 213 // Selected item must be one. 214 if (selected.length !== 1) 215 return false; 216 217 if (selected[0].modelItem.path !== path || 218 selected[0].querySelector('.root-label').textContent !== label) { 219 return false; 220 } 221 222 return true; 223}; 224 225/** 226 * Waits until the window is set to the specified dimensions. 227 * 228 * @param {Window} contentWindow Window to be tested. 229 * @param {number} width Requested width. 230 * @param {number} height Requested height. 231 * @param {function(Object)} callback Success callback with the dimensions. 232 */ 233test.util.async.waitForWindowGeometry = function( 234 contentWindow, width, height, callback) { 235 test.util.repeatUntilTrue_(function() { 236 if (contentWindow.innerWidth == width && 237 contentWindow.innerHeight == height) { 238 callback({width: width, height: height}); 239 return true; 240 } 241 return false; 242 }); 243}; 244 245/** 246 * Waits for an element and returns it as an array of it's attributes. 247 * 248 * @param {Window} contentWindow Window to be tested. 249 * @param {string} targetQuery Query to specify the element. 250 * @param {?string} iframeQuery Iframe selector or null if no iframe. 251 * @param {boolean=} opt_inverse True if the function should return if the 252 * element disappears, instead of appearing. 253 * @param {function(Object)} callback Callback with a hash array of attributes 254 * and contents as text. 255 */ 256test.util.async.waitForElement = function( 257 contentWindow, targetQuery, iframeQuery, opt_inverse, callback) { 258 test.util.repeatUntilTrue_(function() { 259 var doc = test.util.sync.getDocument_(contentWindow, iframeQuery); 260 if (!doc) 261 return false; 262 var element = doc.querySelector(targetQuery); 263 if (!element) 264 return !!opt_inverse; 265 var attributes = {}; 266 for (var i = 0; i < element.attributes.length; i++) { 267 attributes[element.attributes[i].nodeName] = 268 element.attributes[i].nodeValue; 269 } 270 var text = element.textContent; 271 callback({attributes: attributes, text: text}); 272 return !opt_inverse; 273 }); 274}; 275 276/** 277 * Calls getFileList until the number of displayed files is different from 278 * lengthBefore. 279 * 280 * @param {Window} contentWindow Window to be tested. 281 * @param {number} lengthBefore Number of items visible before. 282 * @param {function(Array.<Array.<string>>)} callback Change callback. 283 */ 284test.util.async.waitForFileListChange = function( 285 contentWindow, lengthBefore, callback) { 286 test.util.repeatUntilTrue_(function() { 287 var files = test.util.sync.getFileList(contentWindow); 288 files.sort(); 289 var notReadyRows = files.filter(function(row) { 290 return row.filter(function(cell) { return cell == '...'; }).length; 291 }); 292 if (notReadyRows.length === 0 && 293 files.length !== lengthBefore && 294 files.length !== 0) { 295 callback(files); 296 return true; 297 } else { 298 return false; 299 } 300 }); 301}; 302 303/** 304 * Returns an array of items on the file manager's autocomplete list. 305 * 306 * @param {Window} contentWindow Window to be tested. 307 * @return {Array.<string>} Array of items. 308 */ 309test.util.sync.getAutocompleteList = function(contentWindow) { 310 var list = contentWindow.document.querySelector('#autocomplete-list'); 311 var lines = list.querySelectorAll('li'); 312 var items = []; 313 for (var j = 0; j < lines.length; ++j) { 314 var line = lines[j]; 315 items.push(line.innerText); 316 } 317 return items; 318}; 319 320/** 321 * Performs autocomplete with the given query and waits until at least 322 * |numExpectedItems| items are shown, including the first item which 323 * always looks like "'<query>' - search Drive". 324 * 325 * @param {Window} contentWindow Window to be tested. 326 * @param {string} query Query used for autocomplete. 327 * @param {number} numExpectedItems number of items to be shown. 328 * @param {function(Array.<string>)} callback Change callback. 329 */ 330test.util.async.performAutocompleteAndWait = function( 331 contentWindow, query, numExpectedItems, callback) { 332 // Dispatch a 'focus' event to the search box so that the autocomplete list 333 // is attached to the search box. Note that calling searchBox.focus() won't 334 // dispatch a 'focus' event. 335 var searchBox = contentWindow.document.querySelector('#search-box input'); 336 var focusEvent = contentWindow.document.createEvent('Event'); 337 focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */); 338 searchBox.dispatchEvent(focusEvent); 339 340 // Change the value of the search box and dispatch an 'input' event so that 341 // the autocomplete query is processed. 342 searchBox.value = query; 343 var inputEvent = contentWindow.document.createEvent('Event'); 344 inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */); 345 searchBox.dispatchEvent(inputEvent); 346 347 test.util.repeatUntilTrue_(function() { 348 var items = test.util.sync.getAutocompleteList(contentWindow); 349 if (items.length >= numExpectedItems) { 350 callback(items); 351 return true; 352 } else { 353 return false; 354 } 355 }); 356}; 357 358/** 359 * Waits until a dialog with an OK button is shown and accepts it. 360 * 361 * @param {Window} contentWindow Window to be tested. 362 * @param {function()} callback Success callback. 363 */ 364test.util.async.waitAndAcceptDialog = function(contentWindow, callback) { 365 test.util.repeatUntilTrue_(function() { 366 var button = contentWindow.document.querySelector('.cr-dialog-ok'); 367 if (!button) 368 return false; 369 button.click(); 370 // Wait until the dialog is removed from the DOM. 371 test.util.repeatUntilTrue_(function() { 372 if (contentWindow.document.querySelector('.cr-dialog-container')) 373 return false; 374 callback(); 375 return true; 376 }); 377 return true; 378 }); 379}; 380 381/** 382 * Fakes pressing the down arrow until the given |filename| is selected. 383 * 384 * @param {Window} contentWindow Window to be tested. 385 * @param {string} filename Name of the file to be selected. 386 * @return {boolean} True if file got selected, false otherwise. 387 */ 388test.util.sync.selectFile = function(contentWindow, filename) { 389 var table = contentWindow.document.querySelector('#detail-table'); 390 var rows = table.querySelectorAll('li'); 391 for (var index = 0; index < rows.length; ++index) { 392 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false); 393 var selection = test.util.sync.getSelectedFiles(contentWindow); 394 if (selection.length === 1 && selection[0] === filename) 395 return true; 396 } 397 console.error('Failed to select file "' + filename + '"'); 398 return false; 399}; 400 401/** 402 * Open the file by selectFile and fakeMouseDoubleClick. 403 * 404 * @param {Window} contentWindow Window to be tested. 405 * @param {string} filename Name of the file to be opened. 406 * @return {boolean} True if file got selected and a double click message is 407 * sent, false otherwise. 408 */ 409test.util.sync.openFile = function(contentWindow, filename) { 410 var query = '#file-list li.table-row[selected] .filename-label span'; 411 return test.util.sync.selectFile(contentWindow, filename) && 412 test.util.sync.fakeMouseDoubleClick(contentWindow, query); 413}; 414 415/** 416 * Selects a volume specified by its icon name 417 * 418 * @param {Window} contentWindow Window to be tested. 419 * @param {string} iconName Name of the volume icon. 420 * @param {function(boolean)} callback Callback function to notify the caller 421 * whether the target is found and mousedown and click events are sent. 422 */ 423test.util.async.selectVolume = function(contentWindow, iconName, callback) { 424 var query = '[volume-type-icon=' + iconName + ']'; 425 var driveQuery = '[volume-type-icon=drive]'; 426 var isDriveSubVolume = iconName == 'drive_recent' || 427 iconName == 'drive_shared_with_me' || 428 iconName == 'drive_offline'; 429 var preSelection = false; 430 var steps = { 431 checkQuery: function() { 432 if (contentWindow.document.querySelector(query)) { 433 steps.sendEvents(); 434 return; 435 } 436 // If the target volume is sub-volume of drive, we must click 'drive' 437 // before clicking the sub-item. 438 if (!preSelection) { 439 if (!isDriveSubVolume) { 440 callback(false); 441 return; 442 } 443 if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) && 444 test.util.sync.fakeMouseClick(contentWindow, driveQuery))) { 445 callback(false); 446 return; 447 } 448 preSelection = true; 449 } 450 setTimeout(steps.checkQuery, 50); 451 }, 452 sendEvents: function() { 453 // To change the selected volume, we have to send both events 'mousedown' 454 // and 'click' to the navigation list. 455 callback(test.util.sync.fakeMouseDown(contentWindow, query) && 456 test.util.sync.fakeMouseClick(contentWindow, query)); 457 } 458 }; 459 steps.checkQuery(); 460}; 461 462/** 463 * Waits the contents of file list becomes to equal to expected contents. 464 * 465 * @param {Window} contentWindow Window to be tested. 466 * @param {Array.<Array.<string>>} expected Expected contents of file list. 467 * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options 468 * Options of the comparison. If orderCheck is true, it also compares the 469 * order of files. If ignoreLastModifiedTime is true, it compares the file 470 * without its last modified time. 471 * @param {function()} callback Callback function to notify the caller that 472 * expected files turned up. 473 */ 474test.util.async.waitForFiles = function( 475 contentWindow, expected, opt_options, callback) { 476 var options = opt_options || {}; 477 test.util.repeatUntilTrue_(function() { 478 var files = test.util.sync.getFileList(contentWindow); 479 if (!options.orderCheck) { 480 files.sort(); 481 expected.sort(); 482 } 483 if (options.ignoreLastModifiedTime) { 484 for (var i = 0; i < Math.min(files.length, expected.length); i++) { 485 files[i][3] = ''; 486 expected[i][3] = ''; 487 } 488 } 489 if (chrome.test.checkDeepEq(expected, files)) { 490 callback(true); 491 return true; 492 } 493 return false; 494 }); 495}; 496 497/** 498 * Executes Javascript code on a webview and returns the result. 499 * 500 * @param {Window} contentWindow Window to be tested. 501 * @param {string} webViewQuery Selector for the web view. 502 * @param {string} code Javascript code to be executed within the web view. 503 * @param {function(*)} callback Callback function with results returned by the 504 * script. 505 */ 506test.util.async.executeScriptInWebView = function( 507 contentWindow, webViewQuery, code, callback) { 508 var webView = contentWindow.document.querySelector(webViewQuery); 509 webView.executeScript({code: code}, callback); 510}; 511 512/** 513 * Sends an event to the element specified by |targetQuery|. 514 * 515 * @param {Window} contentWindow Window to be tested. 516 * @param {string} targetQuery Query to specify the element. 517 * @param {Event} event Event to be sent. 518 * @param {string=} opt_iframeQuery Optional iframe selector. 519 * @return {boolean} True if the event is sent to the target, false otherwise. 520 */ 521test.util.sync.sendEvent = function( 522 contentWindow, targetQuery, event, opt_iframeQuery) { 523 var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery); 524 if (doc) { 525 var target = doc.querySelector(targetQuery); 526 if (target) { 527 target.dispatchEvent(event); 528 return true; 529 } 530 } 531 console.error('Target element for ' + targetQuery + ' not found.'); 532 return false; 533}; 534 535/** 536 * Sends an fake event having the specified type to the target query. 537 * 538 * @param {Window} contentWindow Window to be tested. 539 * @param {string} targetQuery Query to specify the element. 540 * @param {string} event Type of event. 541 * @return {boolean} True if the event is sent to the target, false otherwise. 542 */ 543test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) { 544 return test.util.sync.sendEvent( 545 contentWindow, targetQuery, new Event(event)); 546}; 547 548/** 549 * Sends a fake key event to the element specified by |targetQuery| with the 550 * given |keyIdentifier| and optional |ctrl| modifier to the file manager. 551 * 552 * @param {Window} contentWindow Window to be tested. 553 * @param {string} targetQuery Query to specify the element. 554 * @param {string} keyIdentifier Identifier of the emulated key. 555 * @param {boolean} ctrl Whether CTRL should be pressed, or not. 556 * @param {string=} opt_iframeQuery Optional iframe selector. 557 * @return {boolean} True if the event is sent to the target, false otherwise. 558 */ 559test.util.sync.fakeKeyDown = function( 560 contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) { 561 var event = new KeyboardEvent( 562 'keydown', 563 { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl }); 564 return test.util.sync.sendEvent( 565 contentWindow, targetQuery, event, opt_iframeQuery); 566}; 567 568/** 569 * Simulates a fake mouse click (left button, single click) on the element 570 * specified by |targetQuery|. This sends 'mouseover', 'mousedown', 'mouseup' 571 * and 'click' events in turns. 572 * 573 * @param {Window} contentWindow Window to be tested. 574 * @param {string} targetQuery Query to specify the element. 575 * @param {string=} opt_iframeQuery Optional iframe selector. 576 * @return {boolean} True if the all events are sent to the target, false 577 * otherwise. 578 */ 579test.util.sync.fakeMouseClick = function( 580 contentWindow, targetQuery, opt_iframeQuery) { 581 var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1}); 582 var resultMouseOver = test.util.sync.sendEvent( 583 contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery); 584 var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1}); 585 var resultMouseDown = test.util.sync.sendEvent( 586 contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery); 587 var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1}); 588 var resultMouseUp = test.util.sync.sendEvent( 589 contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery); 590 var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1}); 591 var resultClick = test.util.sync.sendEvent( 592 contentWindow, targetQuery, clickEvent, opt_iframeQuery); 593 return resultMouseOver && resultMouseDown && resultMouseUp && resultClick; 594}; 595 596/** 597 * Simulates a fake double click event (left button) to the element specified by 598 * |targetQuery|. 599 * 600 * @param {Window} contentWindow Window to be tested. 601 * @param {string} targetQuery Query to specify the element. 602 * @param {string=} opt_iframeQuery Optional iframe selector. 603 * @return {boolean} True if the event is sent to the target, false otherwise. 604 */ 605test.util.sync.fakeMouseDoubleClick = function( 606 contentWindow, targetQuery, opt_iframeQuery) { 607 // Double click is always preceded with a single click. 608 if (!test.util.sync.fakeMouseClick( 609 contentWindow, targetQuery, opt_iframeQuery)) { 610 return false; 611 } 612 613 // Send the second click event, but with detail equal to 2 (number of clicks) 614 // in a row. 615 var event = new MouseEvent('click', { bubbles: true, detail: 2 }); 616 if (!test.util.sync.sendEvent( 617 contentWindow, targetQuery, event, opt_iframeQuery)) { 618 return false; 619 } 620 621 // Send the double click event. 622 var event = new MouseEvent('dblclick', { bubbles: true }); 623 if (!test.util.sync.sendEvent( 624 contentWindow, targetQuery, event, opt_iframeQuery)) { 625 return false; 626 } 627 628 return true; 629}; 630 631/** 632 * Sends a fake mouse down event to the element specified by |targetQuery|. 633 * 634 * @param {Window} contentWindow Window to be tested. 635 * @param {string} targetQuery Query to specify the element. 636 * @param {string=} opt_iframeQuery Optional iframe selector. 637 * @return {boolean} True if the event is sent to the target, false otherwise. 638 */ 639test.util.sync.fakeMouseDown = function( 640 contentWindow, targetQuery, opt_iframeQuery) { 641 var event = new MouseEvent('mousedown', { bubbles: true }); 642 return test.util.sync.sendEvent( 643 contentWindow, targetQuery, event, opt_iframeQuery); 644}; 645 646/** 647 * Sends a fake mouse up event to the element specified by |targetQuery|. 648 * 649 * @param {Window} contentWindow Window to be tested. 650 * @param {string} targetQuery Query to specify the element. 651 * @param {string=} opt_iframeQuery Optional iframe selector. 652 * @return {boolean} True if the event is sent to the target, false otherwise. 653 */ 654test.util.sync.fakeMouseUp = function( 655 contentWindow, targetQuery, opt_iframeQuery) { 656 var event = new MouseEvent('mouseup', { bubbles: true }); 657 return test.util.sync.sendEvent( 658 contentWindow, targetQuery, event, opt_iframeQuery); 659}; 660 661/** 662 * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste). 663 * 664 * @param {Window} contentWindow Window to be tested. 665 * @param {string} filename Name of the file to be copied. 666 * @return {boolean} True if copying got simulated successfully. It does not 667 * say if the file got copied, or not. 668 */ 669test.util.sync.copyFile = function(contentWindow, filename) { 670 if (!test.util.sync.selectFile(contentWindow, filename)) 671 return false; 672 // Ctrl+C and Ctrl+V 673 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true); 674 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true); 675 return true; 676}; 677 678/** 679 * Selects |filename| and fakes pressing the Delete key. 680 * 681 * @param {Window} contentWindow Window to be tested. 682 * @param {string} filename Name of the file to be deleted. 683 * @return {boolean} True if deleting got simulated successfully. It does not 684 * say if the file got deleted, or not. 685 */ 686test.util.sync.deleteFile = function(contentWindow, filename) { 687 if (!test.util.sync.selectFile(contentWindow, filename)) 688 return false; 689 // Delete 690 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false); 691 return true; 692}; 693 694/** 695 * Wait for the elements' style to be changed as the expected values. The 696 * queries argument is a list of object that have the query property and the 697 * styles property. The query property is a string query to specify the 698 * element. The styles property is a string map of the style name and its 699 * expected value. 700 * 701 * @param {Window} contentWindow Window to be tested. 702 * @param {Array.<object>} queries Queries that specifies the elements and 703 * expected styles. 704 * @param {function()} callback Callback function to be notified the change of 705 * the styles. 706 */ 707test.util.async.waitForStyles = function(contentWindow, queries, callback) { 708 test.util.repeatUntilTrue_(function() { 709 for (var i = 0; i < queries.length; i++) { 710 var element = contentWindow.document.querySelector(queries[i].query); 711 var styles = queries[i].styles; 712 for (var name in styles) { 713 if (contentWindow.getComputedStyle(element)[name] != styles[name]) 714 return false; 715 } 716 } 717 callback(); 718 return true; 719 }); 720}; 721 722/** 723 * Execute a command on the document in the specified window. 724 * 725 * @param {Window} contentWindow Window to be tested. 726 * @param {string} command Command name. 727 * @return {boolean} True if the command is executed successfully. 728 */ 729test.util.sync.execCommand = function(contentWindow, command) { 730 return contentWindow.document.execCommand(command); 731}; 732 733/** 734 * Override the installWebstoreItem method in private api for test. 735 * 736 * @param {Window} contentWindow Window to be tested. 737 * @param {string} expectedItemId Item ID to be called this method with. 738 * @param {?string} intendedError Error message to be returned when the item id 739 * matches. 'null' represents no error. 740 * @return {boolean} Always return true. 741 */ 742test.util.sync.overrideInstallWebstoreItemApi = 743 function(contentWindow, expectedItemId, intendedError) { 744 var setLastError = function(message) { 745 contentWindow.chrome.runtime.lastError = 746 message ? {message: message} : null; 747 }; 748 749 var installWebstoreItem = function(itemId, callback) { 750 setTimeout(function() { 751 if (itemId !== expectedItemId) { 752 setLastError('Invalid Chrome Web Store item ID'); 753 callback(); 754 return; 755 } 756 757 setLastError(intendedError); 758 callback(); 759 }); 760 }; 761 762 test.util.executedTasks_ = []; 763 contentWindow.chrome.fileBrowserPrivate.installWebstoreItem = 764 installWebstoreItem; 765 return true; 766}; 767 768/** 769 * Override the task-related methods in private api for test. 770 * 771 * @param {Window} contentWindow Window to be tested. 772 * @param {Array.<Object>} taskList List of tasks to be returned in 773 * fileBrowserPrivate.getFileTasks(). 774 * @return {boolean} Always return true. 775 */ 776test.util.sync.overrideTasks = function(contentWindow, taskList) { 777 var getFileTasks = function(urls, mime, onTasks) { 778 // Call onTask asynchronously (same with original getFileTasks). 779 setTimeout(function() { 780 onTasks(taskList); 781 }); 782 }; 783 784 var executeTask = function(taskId, url) { 785 test.util.executedTasks_.push(taskId); 786 }; 787 788 test.util.executedTasks_ = []; 789 contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks; 790 contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask; 791 return true; 792}; 793 794/** 795 * Check if Files.app has ordered to execute the given task or not yet. This 796 * method must be used with test.util.sync.overrideTasks(). 797 * 798 * @param {Window} contentWindow Window to be tested. 799 * @param {string} taskId Taskid of the task which should be executed. 800 * @param {function()} callback Callback function to be notified the order of 801 * the execution. 802 */ 803test.util.async.waitUntilTaskExecutes = 804 function(contentWindow, taskId, callback) { 805 if (!test.util.executedTasks_) { 806 console.error('Please call overrideTasks() first.'); 807 return; 808 } 809 810 test.util.repeatUntilTrue_(function() { 811 if (test.util.executedTasks_.indexOf(taskId) === -1) 812 return false; 813 callback(); 814 return true; 815 }); 816}; 817 818/** 819 * Registers message listener, which runs test utility functions. 820 */ 821test.util.registerRemoteTestUtils = function() { 822 // Register the message listener. 823 var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal : 824 chrome.extension.onMessageExternal; 825 // Return true for asynchronous functions and false for synchronous. 826 onMessage.addListener(function(request, sender, sendResponse) { 827 // Check the sender. 828 if (sender.id != test.util.TESTING_EXTENSION_ID) { 829 console.error('The testing extension must be white-listed.'); 830 return false; 831 } 832 // Set a global flag that we are in tests, so other components are aware 833 // of it. 834 window.IN_TEST = true; 835 // Check the function name. 836 if (!request.func || request.func[request.func.length - 1] == '_') { 837 request.func = ''; 838 } 839 // Prepare arguments. 840 var args = request.args.slice(); // shallow copy 841 if (request.appId) { 842 if (!background.appWindows[request.appId]) { 843 console.error('Specified window not found.'); 844 return false; 845 } 846 args.unshift(background.appWindows[request.appId].contentWindow); 847 } 848 // Call the test utility function and respond the result. 849 if (test.util.async[request.func]) { 850 args[test.util.async[request.func].length - 1] = function() { 851 console.debug('Received the result of ' + request.func); 852 sendResponse.apply(null, arguments); 853 }; 854 console.debug('Waiting for the result of ' + request.func); 855 test.util.async[request.func].apply(null, args); 856 return true; 857 } else if (test.util.sync[request.func]) { 858 sendResponse(test.util.sync[request.func].apply(null, args)); 859 return false; 860 } else { 861 console.error('Invalid function name.'); 862 return false; 863 } 864 }); 865}; 866 867// Register the test utils. 868test.util.registerRemoteTestUtils(); 869