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();