Home | History | Annotate | Download | only in browser_test
      1 // Copyright 2014 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  * @fileoverview
      7  * @suppress {checkTypes}  By default, JSCompile is not run on test files.
      8  *    However, you can modify |remoting_webapp_files.gypi| locally to include
      9  *    the test in the package to expedite local development.  This suppress
     10  *    is here so that JSCompile won't complain.
     11  *
     12  * Provides basic functionality for JavaScript based browser test.
     13  *
     14  * To define a browser test, create a class under the browserTest namespace.
     15  * You can pass arbitrary object literals to the browser test from the C++ test
     16  * harness as the test data.  Each browser test class should implement the run
     17  * method.
     18  * For example:
     19  *
     20  * browserTest.My_Test = function() {};
     21  * browserTest.My_Test.prototype.run(myObjectLiteral) = function() { ... };
     22  *
     23  * The browser test is async in nature.  It will keep running until
     24  * browserTest.fail("My error message.") or browserTest.pass() is called.
     25  *
     26  * For example:
     27  *
     28  * browserTest.My_Test.prototype.run(myObjectLiteral) = function() {
     29  *   window.setTimeout(function() {
     30  *     if (doSomething(myObjectLiteral)) {
     31  *       browserTest.pass();
     32  *     } else {
     33  *       browserTest.fail('My error message.');
     34  *     }
     35  *   }, 1000);
     36  * };
     37  *
     38  * You will then invoke the test in C++ by calling:
     39  *
     40  *   RunJavaScriptTest(web_content, "My_Test", "{"
     41  *    "pin: '123123'"
     42  *  "}");
     43  */
     44 
     45 'use strict';
     46 
     47 var browserTest = {};
     48 
     49 browserTest.init = function() {
     50   // The domAutomationController is used to communicate progress back to the
     51   // C++ calling code.  It will only exist if chrome is run with the flag
     52   // --dom-automation.  It is stubbed out here so that browser test can be run
     53   // under the regular app.
     54   browserTest.automationController_ = window.domAutomationController || {
     55     send: function(json) {
     56       var result = JSON.parse(json);
     57       if (result.succeeded) {
     58         console.log('Test Passed.');
     59       } else {
     60         console.error('Test Failed.\n' +
     61             result.error_message + '\n' + result.stack_trace);
     62       }
     63     }
     64   };
     65 };
     66 
     67 browserTest.expect = function(expr, message) {
     68   if (!expr) {
     69     message = (message) ? '<' + message + '>' : '';
     70     browserTest.fail('Expectation failed.' + message);
     71   }
     72 };
     73 
     74 browserTest.fail = function(error) {
     75   var error_message = error;
     76   var stack_trace = base.debug.callstack();
     77 
     78   if (error instanceof Error) {
     79     error_message = error.toString();
     80     stack_trace = error.stack;
     81   }
     82 
     83   // To run browserTest locally:
     84   // 1. Go to |remoting_webapp_files| and look for
     85   //    |remoting_webapp_js_browser_test_files| and uncomment it
     86   // 2. gclient runhooks
     87   // 3. rebuild the webapp
     88   // 4. Run it in the console browserTest.runTest(browserTest.MyTest, {});
     89   // 5. The line below will trap the test in the debugger in case of
     90   //    failure.
     91   debugger;
     92 
     93   browserTest.automationController_.send(JSON.stringify({
     94     succeeded: false,
     95     error_message: error_message,
     96     stack_trace: stack_trace
     97   }));
     98 };
     99 
    100 browserTest.pass = function() {
    101   browserTest.automationController_.send(JSON.stringify({
    102     succeeded: true,
    103     error_message: '',
    104     stack_trace: ''
    105   }));
    106 };
    107 
    108 browserTest.clickOnControl = function(id) {
    109   var element = document.getElementById(id);
    110   browserTest.expect(element);
    111   element.click();
    112 };
    113 
    114 /** @enum {number} */
    115 browserTest.Timeout = {
    116   NONE: -1,
    117   DEFAULT: 5000
    118 };
    119 
    120 browserTest.onUIMode = function(expectedMode, opt_timeout) {
    121   if (expectedMode == remoting.currentMode) {
    122     // If the current mode is the same as the expected mode, return a fulfilled
    123     // promise.  For some reason, if we fulfill the promise in the same
    124     // callstack, V8 will assert at V8RecursionScope.h(66) with
    125     // ASSERT(!ScriptForbiddenScope::isScriptForbidden()).
    126     // To avoid the assert, execute the callback in a different callstack.
    127     return base.Promise.sleep(0);
    128   }
    129 
    130   return new Promise (function(fulfill, reject) {
    131     var uiModeChanged = remoting.testEvents.Names.uiModeChanged;
    132     var timerId = null;
    133 
    134     if (opt_timeout === undefined) {
    135       opt_timeout = browserTest.Timeout.DEFAULT;
    136     }
    137 
    138     function onTimeout() {
    139       remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged);
    140       reject('Timeout waiting for ' + expectedMode);
    141     }
    142 
    143     function onUIModeChanged(mode) {
    144       if (mode == expectedMode) {
    145         remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged);
    146         window.clearTimeout(timerId);
    147         timerId = null;
    148         fulfill();
    149       }
    150     }
    151 
    152     if (opt_timeout != browserTest.Timeout.NONE) {
    153       timerId = window.setTimeout(onTimeout, opt_timeout);
    154     }
    155     remoting.testEvents.addEventListener(uiModeChanged, onUIModeChanged);
    156   });
    157 };
    158 
    159 browserTest.connectMe2Me = function() {
    160   var AppMode = remoting.AppMode;
    161   browserTest.clickOnControl('this-host-connect');
    162   return browserTest.onUIMode(AppMode.CLIENT_HOST_NEEDS_UPGRADE).then(
    163     function() {
    164       // On fulfilled.
    165       browserTest.clickOnControl('host-needs-update-connect-button');
    166     }, function() {
    167       // On time out.
    168       return Promise.resolve();
    169     }).then(function() {
    170       return browserTest.onUIMode(AppMode.CLIENT_PIN_PROMPT, 10000);
    171     });
    172 };
    173 
    174 browserTest.disconnect = function() {
    175   var AppMode = remoting.AppMode;
    176   remoting.disconnect();
    177   return browserTest.onUIMode(AppMode.CLIENT_SESSION_FINISHED_ME2ME).then(
    178     function() {
    179       browserTest.clickOnControl('client-finished-me2me-button');
    180       return browserTest.onUIMode(AppMode.HOME);
    181     });
    182 };
    183 
    184 browserTest.enterPIN = function(pin, opt_expectError) {
    185   // Wait for 500ms before hitting the PIN button. From experiment, sometimes
    186   // the PIN prompt does not dismiss without the timeout.
    187   var CONNECT_PIN_WAIT = 500;
    188 
    189   document.getElementById('pin-entry').value = pin;
    190 
    191   return base.Promise.sleep(CONNECT_PIN_WAIT).then(function() {
    192     browserTest.clickOnControl('pin-connect-button');
    193   }).then(function() {
    194     if (opt_expectError) {
    195       return browserTest.expectMe2MeError(remoting.Error.INVALID_ACCESS_CODE);
    196     } else {
    197       return browserTest.expectMe2MeConnected();
    198     }
    199   });
    200 };
    201 
    202 browserTest.expectMe2MeError = function(errorTag) {
    203   var AppMode = remoting.AppMode;
    204   var Timeout = browserTest.Timeout;
    205 
    206   var onConnected = browserTest.onUIMode(AppMode.IN_SESSION, Timeout.None);
    207   var onFailure = browserTest.onUIMode(AppMode.CLIENT_CONNECT_FAILED_ME2ME);
    208 
    209   onConnected = onConnected.then(function() {
    210     return Promise.reject(
    211         'Expected the Me2Me connection to fail.');
    212   });
    213 
    214   onFailure = onFailure.then(function() {
    215     var errorDiv = document.getElementById('connect-error-message');
    216     var actual = errorDiv.innerText;
    217     var expected = l10n.getTranslationOrError(errorTag);
    218 
    219     browserTest.clickOnControl('client-finished-me2me-button');
    220 
    221     if (actual != expected) {
    222       return Promise.reject('Unexpected failure. actual:' + actual +
    223                      ' expected:' + expected);
    224     }
    225   });
    226 
    227   return Promise.race([onConnected, onFailure]);
    228 };
    229 
    230 browserTest.expectMe2MeConnected = function() {
    231   var AppMode = remoting.AppMode;
    232   // Timeout if the session is not connected within 30 seconds.
    233   var SESSION_CONNECTION_TIMEOUT = 30000;
    234   var onConnected = browserTest.onUIMode(AppMode.IN_SESSION,
    235                                          SESSION_CONNECTION_TIMEOUT);
    236   var onFailure = browserTest.onUIMode(AppMode.CLIENT_CONNECT_FAILED_ME2ME,
    237                                        browserTest.Timeout.NONE);
    238   onFailure = onFailure.then(function() {
    239     var errorDiv = document.getElementById('connect-error-message');
    240     var errorMsg = errorDiv.innerText;
    241     return Promise.reject('Unexpected error - ' + errorMsg);
    242   });
    243   return Promise.race([onConnected, onFailure]);
    244 };
    245 
    246 browserTest.expectEvent = function(eventSource, event, timeoutMs,
    247                                    opt_expectedData) {
    248   return new Promise(function(fullfil, reject) {
    249     var verifyEventParameters = function(actualData) {
    250       if (opt_expectedData === undefined || opt_expectedData === actualData) {
    251         fullfil();
    252       } else {
    253         reject('Bad event data; expected ' + opt_expectedData +
    254                '; got ' + actualData);
    255       }
    256     };
    257     eventSource.addEventListener(event, verifyEventParameters);
    258     base.Promise.sleep(timeoutMs).then(function() {
    259       reject(Error('Event ' + event + ' not received after ' +
    260                    timeoutMs + 'ms.'));
    261     });
    262   });
    263 };
    264 
    265 browserTest.runTest = function(testClass, data) {
    266   try {
    267     var test = new testClass();
    268     browserTest.expect(typeof test.run == 'function');
    269     test.run(data);
    270   } catch (e) {
    271     browserTest.fail(e);
    272   }
    273 };
    274 
    275 browserTest.setupPIN = function(newPin) {
    276   var AppMode = remoting.AppMode;
    277   var HOST_SETUP_WAIT = 10000;
    278   var Timeout = browserTest.Timeout;
    279 
    280   return browserTest.onUIMode(AppMode.HOST_SETUP_ASK_PIN).then(function() {
    281     document.getElementById('daemon-pin-entry').value = newPin;
    282     document.getElementById('daemon-pin-confirm').value = newPin;
    283     browserTest.clickOnControl('daemon-pin-ok');
    284 
    285     var success = browserTest.onUIMode(AppMode.HOST_SETUP_DONE, Timeout.NONE);
    286     var failure = browserTest.onUIMode(AppMode.HOST_SETUP_ERROR, Timeout.NONE);
    287     failure = failure.then(function(){
    288       return Promise.reject('Unexpected host setup failure');
    289     });
    290     return Promise.race([success, failure]);
    291   }).then(function() {
    292     console.log('browserTest: PIN Setup is done.');
    293     browserTest.clickOnControl('host-config-done-dismiss');
    294 
    295     // On Linux, we restart the host after changing the PIN, need to sleep
    296     // for ten seconds before the host is ready for connection.
    297     return base.Promise.sleep(HOST_SETUP_WAIT);
    298   });
    299 };
    300 
    301 browserTest.isLocalHostStarted = function() {
    302   return new Promise(function(resolve) {
    303     remoting.hostController.getLocalHostState(function(state) {
    304       resolve(remoting.HostController.State.STARTED == state);
    305     });
    306   });
    307 };
    308 
    309 browserTest.ensureHostStartedWithPIN = function(pin) {
    310   // Return if host is already
    311   return browserTest.isLocalHostStarted().then(function(started){
    312     if (!started) {
    313       console.log('browserTest: Enabling remote connection.');
    314       browserTest.clickOnControl('start-daemon');
    315     } else {
    316       console.log('browserTest: Changing the PIN of the host to: ' + pin + '.');
    317       browserTest.clickOnControl('change-daemon-pin');
    318     }
    319     return browserTest.setupPIN(pin);
    320   });
    321 };
    322 
    323 // Called by Browser Test in C++
    324 browserTest.ensureRemoteConnectionEnabled = function(pin) {
    325   browserTest.ensureHostStartedWithPIN(pin).then(function(){
    326     browserTest.automationController_.send(true);
    327   }, function(errorMessage){
    328     console.error(errorMessage);
    329     browserTest.automationController_.send(false);
    330   });
    331 };
    332 
    333 browserTest.init();