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 */ 8 var 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 */ 20 test.util = {}; 21 22 /** 23 * Namespace for synchronous utility functions. 24 */ 25 test.util.sync = {}; 26 27 /** 28 * Namespace for asynchronous utility functions. 29 */ 30 test.util.async = {}; 31 32 /** 33 * Extension ID of the testing extension. 34 * @type {string} 35 * @const 36 */ 37 test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco'; 38 39 /** 40 * Interval of checking a condition in milliseconds. 41 * @type {number} 42 * @const 43 * @private 44 */ 45 test.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 */ 52 test.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 {string} path Path of the directory to be opened. 65 * @param {function(string)} callback Completion callback with the new window's 66 * App ID. 67 */ 68 test.util.async.openMainWindow = function(path, callback) { 69 var steps = [ 70 function() { 71 launchFileManager({defaultPath: path}, 72 undefined, // opt_type 73 undefined, // opt_id 74 steps.shift()); 75 }, 76 function(appId) { 77 test.util.repeatUntilTrue_(function() { 78 if (!appWindows[appId]) 79 return false; 80 var contentWindow = 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 */ 100 test.util.async.waitForWindow = function(appIdPrefix, callback) { 101 test.util.repeatUntilTrue_(function() { 102 for (var appId in appWindows) { 103 if (appId.indexOf(appIdPrefix) == 0 && 104 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 */ 121 test.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 */ 134 test.util.sync.getErrorCount = function() { 135 var totalCount = 0; 136 for (var appId in appWindows) { 137 var contentWindow = 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 */ 152 test.util.sync.resizeWindow = function(contentWindow, width, height) { 153 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 */ 163 test.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 */ 182 test.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 * Waits until the window is set to the specified dimensions. 200 * 201 * @param {Window} contentWindow Window to be tested. 202 * @param {number} width Requested width. 203 * @param {number} height Requested height. 204 * @param {function(Object)} callback Success callback with the dimensions. 205 */ 206 test.util.async.waitForWindowGeometry = function( 207 contentWindow, width, height, callback) { 208 test.util.repeatUntilTrue_(function() { 209 if (contentWindow.innerWidth == width && 210 contentWindow.innerHeight == height) { 211 callback({width: width, height: height}); 212 return true; 213 } 214 return false; 215 }); 216 }; 217 218 /** 219 * Waits for an element and returns it as an array of it's attributes. 220 * 221 * @param {Window} contentWindow Window to be tested. 222 * @param {string} targetQuery Query to specify the element. 223 * @param {?string} iframeQuery Iframe selector or null if no iframe. 224 * @param {boolean=} opt_inverse True if the function should return if the 225 * element disappears, instead of appearing. 226 * @param {function(Object)} callback Callback with a hash array of attributes 227 * and contents as text. 228 */ 229 test.util.async.waitForElement = function( 230 contentWindow, targetQuery, iframeQuery, opt_inverse, callback) { 231 test.util.repeatUntilTrue_(function() { 232 var doc = test.util.sync.getDocument_(contentWindow, iframeQuery); 233 if (!doc) 234 return false; 235 var element = doc.querySelector(targetQuery); 236 if (!element) 237 return !!opt_inverse; 238 var attributes = {}; 239 for (var i = 0; i < element.attributes.length; i++) { 240 attributes[element.attributes[i].nodeName] = 241 element.attributes[i].nodeValue; 242 } 243 var text = element.textContent; 244 callback({attributes: attributes, text: text}); 245 return !opt_inverse; 246 }); 247 }; 248 249 /** 250 * Calls getFileList until the number of displayed files is different from 251 * lengthBefore. 252 * 253 * @param {Window} contentWindow Window to be tested. 254 * @param {number} lengthBefore Number of items visible before. 255 * @param {function(Array.<Array.<string>>)} callback Change callback. 256 */ 257 test.util.async.waitForFileListChange = function( 258 contentWindow, lengthBefore, callback) { 259 test.util.repeatUntilTrue_(function() { 260 var files = test.util.sync.getFileList(contentWindow); 261 files.sort(); 262 var notReadyRows = files.filter(function(row) { 263 return row.filter(function(cell) { return cell == '...'; }).length; 264 }); 265 if (notReadyRows.length === 0 && 266 files.length !== lengthBefore && 267 files.length !== 0) { 268 callback(files); 269 return true; 270 } else { 271 return false; 272 } 273 }); 274 }; 275 276 /** 277 * Returns an array of items on the file manager's autocomplete list. 278 * 279 * @param {Window} contentWindow Window to be tested. 280 * @return {Array.<string>} Array of items. 281 */ 282 test.util.sync.getAutocompleteList = function(contentWindow) { 283 var list = contentWindow.document.querySelector('#autocomplete-list'); 284 var lines = list.querySelectorAll('li'); 285 var items = []; 286 for (var j = 0; j < lines.length; ++j) { 287 var line = lines[j]; 288 items.push(line.innerText); 289 } 290 return items; 291 }; 292 293 /** 294 * Performs autocomplete with the given query and waits until at least 295 * |numExpectedItems| items are shown, including the first item which 296 * always looks like "'<query>' - search Drive". 297 * 298 * @param {Window} contentWindow Window to be tested. 299 * @param {string} query Query used for autocomplete. 300 * @param {number} numExpectedItems number of items to be shown. 301 * @param {function(Array.<string>)} callback Change callback. 302 */ 303 test.util.async.performAutocompleteAndWait = function( 304 contentWindow, query, numExpectedItems, callback) { 305 // Dispatch a 'focus' event to the search box so that the autocomplete list 306 // is attached to the search box. Note that calling searchBox.focus() won't 307 // dispatch a 'focus' event. 308 var searchBox = contentWindow.document.querySelector('#search-box'); 309 var focusEvent = contentWindow.document.createEvent('Event'); 310 focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */); 311 searchBox.dispatchEvent(focusEvent); 312 313 // Change the value of the search box and dispatch an 'input' event so that 314 // the autocomplete query is processed. 315 searchBox.value = query; 316 var inputEvent = contentWindow.document.createEvent('Event'); 317 inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */); 318 searchBox.dispatchEvent(inputEvent); 319 320 test.util.repeatUntilTrue_(function() { 321 var items = test.util.sync.getAutocompleteList(contentWindow); 322 if (items.length >= numExpectedItems) { 323 callback(items); 324 return true; 325 } else { 326 return false; 327 } 328 }); 329 }; 330 331 /** 332 * Waits until a dialog with an OK button is shown and accepts it. 333 * 334 * @param {Window} contentWindow Window to be tested. 335 * @param {function()} callback Success callback. 336 */ 337 test.util.async.waitAndAcceptDialog = function(contentWindow, callback) { 338 test.util.repeatUntilTrue_(function() { 339 var button = contentWindow.document.querySelector('.cr-dialog-ok'); 340 if (!button) 341 return false; 342 button.click(); 343 // Wait until the dialog is removed from the DOM. 344 test.util.repeatUntilTrue_(function() { 345 if (contentWindow.document.querySelector('.cr-dialog-container')) 346 return false; 347 callback(); 348 return true; 349 }); 350 return true; 351 }); 352 }; 353 354 /** 355 * Fakes pressing the down arrow until the given |filename| is selected. 356 * 357 * @param {Window} contentWindow Window to be tested. 358 * @param {string} filename Name of the file to be selected. 359 * @return {boolean} True if file got selected, false otherwise. 360 */ 361 test.util.sync.selectFile = function(contentWindow, filename) { 362 var table = contentWindow.document.querySelector('#detail-table'); 363 var rows = table.querySelectorAll('li'); 364 for (var index = 0; index < rows.length; ++index) { 365 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false); 366 var selection = test.util.sync.getSelectedFiles(contentWindow); 367 if (selection.length === 1 && selection[0] === filename) 368 return true; 369 } 370 console.error('Failed to select file "' + filename + '"'); 371 return false; 372 }; 373 374 /** 375 * Selects a volume specified by its icon name 376 * 377 * @param {Window} contentWindow Window to be tested. 378 * @param {string} iconName Name of the volume icon. 379 * @param {function(boolean)} callback Callback function to notify the caller 380 * whether the target is found and mousedown and click events are sent. 381 */ 382 test.util.async.selectVolume = function(contentWindow, iconName, callback) { 383 var query = '[volume-type-icon=' + iconName + ']'; 384 var driveQuery = '[volume-type-icon=drive]'; 385 var isDriveSubVolume = iconName == 'drive_recent' || 386 iconName == 'drive_shared_with_me' || 387 iconName == 'drive_offline'; 388 var preSelection = false; 389 var steps = { 390 checkQuery: function() { 391 if (contentWindow.document.querySelector(query)) { 392 steps.sendEvents(); 393 return; 394 } 395 // If the target volume is sub-volume of drive, we must click 'drive' 396 // before clicking the sub-item. 397 if (!preSelection) { 398 if (!isDriveSubVolume) { 399 callback(false); 400 return; 401 } 402 if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) && 403 test.util.sync.fakeMouseClick(contentWindow, driveQuery))) { 404 callback(false); 405 return; 406 } 407 preSelection = true; 408 } 409 setTimeout(steps.checkQuery, 50); 410 }, 411 sendEvents: function() { 412 // To change the selected volume, we have to send both events 'mousedown' 413 // and 'click' to the navigation list. 414 callback(test.util.sync.fakeMouseDown(contentWindow, query) && 415 test.util.sync.fakeMouseClick(contentWindow, query)); 416 } 417 }; 418 steps.checkQuery(); 419 }; 420 421 /** 422 * Waits the contents of file list becomes to equal to expected contents. 423 * 424 * @param {Window} contentWindow Window to be tested. 425 * @param {Array.<Array.<string>>} expected Expected contents of file list. 426 * @param {boolean=} opt_orderCheck If it is true, this function also compares 427 * the order of files. 428 * @param {function()} callback Callback function to notify the caller that 429 * expected files turned up. 430 */ 431 test.util.async.waitForFiles = function( 432 contentWindow, expected, opt_orderCheck, callback) { 433 test.util.repeatUntilTrue_(function() { 434 var files = test.util.sync.getFileList(contentWindow); 435 if (!opt_orderCheck) 436 files.sort(); 437 if (chrome.test.checkDeepEq(expected, files)) { 438 callback(true); 439 return true; 440 } 441 return false; 442 }); 443 }; 444 445 /** 446 * Executes Javascript code on a webview and returns the result. 447 * 448 * @param {Window} contentWindow Window to be tested. 449 * @param {string} webViewQuery Selector for the web view. 450 * @param {string} code Javascript code to be executed within the web view. 451 * @param {function(*)} callback Callback function with results returned by the 452 * script. 453 */ 454 test.util.async.executeScriptInWebView = function( 455 contentWindow, webViewQuery, code, callback) { 456 var webView = contentWindow.document.querySelector(webViewQuery); 457 webView.executeScript({code: code}, callback); 458 }; 459 460 /** 461 * Sends an event to the element specified by |targetQuery|. 462 * 463 * @param {Window} contentWindow Window to be tested. 464 * @param {string} targetQuery Query to specify the element. 465 * @param {Event} event Event to be sent. 466 * @param {string=} opt_iframeQuery Optional iframe selector. 467 * @return {boolean} True if the event is sent to the target, false otherwise. 468 */ 469 test.util.sync.sendEvent = function( 470 contentWindow, targetQuery, event, opt_iframeQuery) { 471 var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery); 472 if (doc) { 473 var target = doc.querySelector(targetQuery); 474 if (target) { 475 target.dispatchEvent(event); 476 return true; 477 } 478 } 479 console.error('Target element for ' + targetQuery + ' not found.'); 480 return false; 481 }; 482 483 /** 484 * Sends an fake event having the specified type to the target query. 485 * 486 * @param {Window} contentWindow Window to be tested. 487 * @param {string} targetQuery Query to specify the element. 488 * @param {string} event Type of event. 489 * @return {boolean} True if the event is sent to the target, false otherwise. 490 */ 491 test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) { 492 return test.util.sync.sendEvent( 493 contentWindow, targetQuery, new Event(event)); 494 }; 495 496 /** 497 * Sends a fake key event to the element specified by |targetQuery| with the 498 * given |keyIdentifier| and optional |ctrl| modifier to the file manager. 499 * 500 * @param {Window} contentWindow Window to be tested. 501 * @param {string} targetQuery Query to specify the element. 502 * @param {string} keyIdentifier Identifier of the emulated key. 503 * @param {boolean} ctrl Whether CTRL should be pressed, or not. 504 * @param {string=} opt_iframeQuery Optional iframe selector. 505 * @return {boolean} True if the event is sent to the target, false otherwise. 506 */ 507 test.util.sync.fakeKeyDown = function( 508 contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) { 509 var event = new KeyboardEvent( 510 'keydown', 511 { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl }); 512 return test.util.sync.sendEvent( 513 contentWindow, targetQuery, event, opt_iframeQuery); 514 }; 515 516 /** 517 * Sends a fake mouse click event (left button, single click) to the element 518 * specified by |targetQuery|. 519 * 520 * @param {Window} contentWindow Window to be tested. 521 * @param {string} targetQuery Query to specify the element. 522 * @param {string=} opt_iframeQuery Optional iframe selector. 523 * @return {boolean} True if the event is sent to the target, false otherwise. 524 */ 525 test.util.sync.fakeMouseClick = function( 526 contentWindow, targetQuery, opt_iframeQuery) { 527 var event = new MouseEvent('click', { bubbles: true, detail: 1 }); 528 return test.util.sync.sendEvent( 529 contentWindow, targetQuery, event, opt_iframeQuery); 530 }; 531 532 /** 533 * Simulates a fake double click event (left button) to the element specified by 534 * |targetQuery|. 535 * 536 * @param {Window} contentWindow Window to be tested. 537 * @param {string} targetQuery Query to specify the element. 538 * @param {string=} opt_iframeQuery Optional iframe selector. 539 * @return {boolean} True if the event is sent to the target, false otherwise. 540 */ 541 test.util.sync.fakeMouseDoubleClick = function( 542 contentWindow, targetQuery, opt_iframeQuery) { 543 // Double click is always preceeded with a single click. 544 if (!test.util.sync.fakeMouseClick( 545 contentWindow, targetQuery, opt_iframeQuery)) { 546 return false; 547 } 548 549 // Send the second click event, but with detail equal to 2 (number of clicks) 550 // in a row. 551 var event = new MouseEvent('click', { bubbles: true, detail: 2 }); 552 if (!test.util.sync.sendEvent( 553 contentWindow, targetQuery, event, opt_iframeQuery)) { 554 return false; 555 } 556 557 // Send the double click event. 558 var event = new MouseEvent('dblclick', { bubbles: true }); 559 if (!test.util.sync.sendEvent( 560 contentWindow, targetQuery, event, opt_iframeQuery)) { 561 return false; 562 } 563 564 return true; 565 }; 566 567 /** 568 * Sends a fake mouse down event to the element specified by |targetQuery|. 569 * 570 * @param {Window} contentWindow Window to be tested. 571 * @param {string} targetQuery Query to specify the element. 572 * @param {string=} opt_iframeQuery Optional iframe selector. 573 * @return {boolean} True if the event is sent to the target, false otherwise. 574 */ 575 test.util.sync.fakeMouseDown = function( 576 contentWindow, targetQuery, opt_iframeQuery) { 577 var event = new MouseEvent('mousedown', { bubbles: true }); 578 return test.util.sync.sendEvent( 579 contentWindow, targetQuery, event, opt_iframeQuery); 580 }; 581 582 /** 583 * Sends a fake mouse up event to the element specified by |targetQuery|. 584 * 585 * @param {Window} contentWindow Window to be tested. 586 * @param {string} targetQuery Query to specify the element. 587 * @param {string=} opt_iframeQuery Optional iframe selector. 588 * @return {boolean} True if the event is sent to the target, false otherwise. 589 */ 590 test.util.sync.fakeMouseUp = function( 591 contentWindow, targetQuery, opt_iframeQuery) { 592 var event = new MouseEvent('mouseup', { bubbles: true }); 593 return test.util.sync.sendEvent( 594 contentWindow, targetQuery, event, opt_iframeQuery); 595 }; 596 597 /** 598 * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste). 599 * 600 * @param {Window} contentWindow Window to be tested. 601 * @param {string} filename Name of the file to be copied. 602 * @return {boolean} True if copying got simulated successfully. It does not 603 * say if the file got copied, or not. 604 */ 605 test.util.sync.copyFile = function(contentWindow, filename) { 606 if (!test.util.sync.selectFile(contentWindow, filename)) 607 return false; 608 // Ctrl+C and Ctrl+V 609 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true); 610 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true); 611 return true; 612 }; 613 614 /** 615 * Selects |filename| and fakes pressing the Delete key. 616 * 617 * @param {Window} contentWindow Window to be tested. 618 * @param {string} filename Name of the file to be deleted. 619 * @return {boolean} True if deleting got simulated successfully. It does not 620 * say if the file got deleted, or not. 621 */ 622 test.util.sync.deleteFile = function(contentWindow, filename) { 623 if (!test.util.sync.selectFile(contentWindow, filename)) 624 return false; 625 // Delete 626 test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false); 627 return true; 628 }; 629 630 /** 631 * Wait for the elements' style to be changed as the expected values. The 632 * queries argument is a list of object that have the query property and the 633 * styles property. The query property is a string query to specify the 634 * element. The styles property is a string map of the style name and its 635 * expected value. 636 * 637 * @param {Window} contentWindow Window to be tested. 638 * @param {Array.<object>} queries Queries that specifies the elements and 639 * expected styles. 640 * @param {function()} callback Callback function to be notified the change of 641 * the styles. 642 */ 643 test.util.async.waitForStyles = function(contentWindow, queries, callback) { 644 test.util.repeatUntilTrue_(function() { 645 for (var i = 0; i < queries.length; i++) { 646 var element = contentWindow.document.querySelector(queries[i].query); 647 var styles = queries[i].styles; 648 for (var name in styles) { 649 if (contentWindow.getComputedStyle(element)[name] != styles[name]) 650 return false; 651 } 652 } 653 callback(); 654 return true; 655 }); 656 }; 657 658 /** 659 * Execute a command on the document in the specified window. 660 * 661 * @param {Window} contentWindow Window to be tested. 662 * @param {string} command Command name. 663 * @return {boolean} True if the command is executed successfully. 664 */ 665 test.util.sync.execCommand = function(contentWindow, command) { 666 return contentWindow.document.execCommand(command); 667 }; 668 669 /** 670 * Registers message listener, which runs test utility functions. 671 */ 672 test.util.registerRemoteTestUtils = function() { 673 // Register the message listenr. 674 var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal : 675 chrome.extension.onMessageExternal; 676 // Return true for asynchronous functions and false for synchronous. 677 onMessage.addListener(function(request, sender, sendResponse) { 678 // Check the sender. 679 if (sender.id != test.util.TESTING_EXTENSION_ID) { 680 console.error('The testing extension must be white-listed.'); 681 return false; 682 } 683 // Set a global flag that we are in tests, so other components are aware 684 // of it. 685 window.IN_TEST = true; 686 // Check the function name. 687 if (!request.func || request.func[request.func.length - 1] == '_') { 688 request.func = ''; 689 } 690 // Prepare arguments. 691 var args = request.args.slice(); // shallow copy 692 if (request.appId) { 693 if (!appWindows[request.appId]) { 694 console.error('Specified window not found.'); 695 return false; 696 } 697 args.unshift(appWindows[request.appId].contentWindow); 698 } 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); 704 }; 705 console.debug('Waiting for the result of ' + request.func); 706 test.util.async[request.func].apply(null, args); 707 return true; 708 } else if (test.util.sync[request.func]) { 709 sendResponse(test.util.sync[request.func].apply(null, args)); 710 return false; 711 } else { 712 console.error('Invalid function name.'); 713 return false; 714 } 715 }); 716 }; 717 718 // Register the test utils. 719 test.util.registerRemoteTestUtils(); 720