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.
8 * Provides basic functionality for JavaScript based browser test.
10 * To define a browser test, create a class under the browserTest namespace.
11 * You can pass arbitrary object literals to the browser test from the C++ test
12 * harness as the test data. Each browser test class should implement the run
16 * browserTest.My_Test = function() {};
17 * browserTest.My_Test.prototype.run(myObjectLiteral) = function() { ... };
19 * The browser test is async in nature. It will keep running until
20 * browserTest.fail("My error message.") or browserTest.pass() is called.
24 * browserTest.My_Test.prototype.run(myObjectLiteral) = function() {
25 * window.setTimeout(function() {
26 * if (doSomething(myObjectLiteral)) {
29 * browserTest.fail('My error message.');
34 * You will then invoke the test in C++ by calling:
36 * RunJavaScriptTest(web_content, "My_Test", "{"
43 /** @suppress {duplicate} */
44 var browserTest = browserTest || {};
46 /** @type {window.DomAutomationController} */
47 browserTest.automationController_ = null;
51 * @suppress {checkTypes|reportUnknownTypes}
53 browserTest.init = function() {
54 // The domAutomationController is used to communicate progress back to the
55 // C++ calling code. It will only exist if chrome is run with the flag
56 // --dom-automation. It is stubbed out here so that browser test can be run
57 // under the regular app.
58 if (window.domAutomationController) {
59 /** @type {window.DomAutomationController} */
60 browserTest.automationController_ = window.domAutomationController;
62 browserTest.automationController_ = {
63 send: function(json) {
64 var result = JSON.parse(json);
65 if (result.succeeded) {
66 console.log('Test Passed.');
68 console.error('Test Failed.\n' +
69 result.error_message + '\n' + result.stack_trace);
77 * Fails the C++ calling browser test with |message| if |expr| is false.
79 * @param {string=} opt_message
82 browserTest.expect = function(expr, opt_message) {
84 var message = (opt_message) ? '<' + opt_message + '>' : '';
85 browserTest.fail('Expectation failed.' + opt_message);
90 * @param {string|Error} error
93 browserTest.fail = function(error) {
94 var error_message = error;
95 var stack_trace = new base.Callstack().toString();
97 if (error instanceof Error) {
98 error_message = error.toString();
99 stack_trace = error.stack;
102 console.error(error_message);
104 // To run browserTest locally:
105 // 1. Go to |remoting_webapp_files| and look for
106 // |remoting_webapp_js_browser_test_files| and uncomment it
107 // 2. gclient runhooks
108 // 3. rebuild the webapp
109 // 4. Run it in the console browserTest.runTest(browserTest.MyTest, {});
110 // 5. The line below will trap the test in the debugger in case of
114 browserTest.automationController_.send(JSON.stringify({
116 error_message: error_message,
117 stack_trace: stack_trace
124 browserTest.pass = function() {
125 browserTest.automationController_.send(JSON.stringify({
133 * @param {string} id The id or the selector of the element.
136 browserTest.clickOnControl = function(id) {
137 var element = document.getElementById(id);
139 element = document.querySelector(id);
141 browserTest.expect(element, 'No such element: ' + id);
146 * @param {remoting.AppMode} expectedMode
147 * @param {number=} opt_timeout
150 browserTest.onUIMode = function(expectedMode, opt_timeout) {
151 if (expectedMode == remoting.currentMode) {
152 // If the current mode is the same as the expected mode, return a fulfilled
153 // promise. For some reason, if we fulfill the promise in the same
154 // callstack, V8 will assert at V8RecursionScope.h(66) with
155 // ASSERT(!ScriptForbiddenScope::isScriptForbidden()).
156 // To avoid the assert, execute the callback in a different callstack.
157 return base.Promise.sleep(0);
160 return new Promise (function(fulfill, reject) {
161 var uiModeChanged = remoting.testEvents.Names.uiModeChanged;
164 if (opt_timeout === undefined) {
165 opt_timeout = browserTest.Timeout.DEFAULT;
168 function onTimeout() {
169 remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged);
170 reject('Timeout waiting for ' + expectedMode);
173 /** @param {remoting.AppMode} mode */
174 function onUIModeChanged(mode) {
175 if (mode == expectedMode) {
176 remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged);
177 window.clearTimeout(timerId);
183 if (opt_timeout != browserTest.Timeout.NONE) {
184 timerId = window.setTimeout(onTimeout,
185 /** @type {number} */ (opt_timeout));
187 remoting.testEvents.addEventListener(uiModeChanged, onUIModeChanged);
194 browserTest.connectMe2Me = function() {
195 var AppMode = remoting.AppMode;
196 // The one second timeout is necessary because the click handler of
197 // 'this-host-connect' is registered asynchronously.
198 return base.Promise.sleep(1000).then(function() {
199 browserTest.clickOnControl('local-host-connect-button');
201 return browserTest.onUIMode(AppMode.CLIENT_HOST_NEEDS_UPGRADE);
204 browserTest.clickOnControl('#host-needs-update-dialog .connect-button');
207 return Promise.resolve();
209 return browserTest.onUIMode(AppMode.CLIENT_PIN_PROMPT, 10000);
216 browserTest.disconnect = function() {
217 console.assert(remoting.app instanceof remoting.DesktopRemoting,
218 '|remoting.app| is not an instance of DesktopRemoting.');
219 var drApp = /** @type {remoting.DesktopRemoting} */ (remoting.app);
220 var mode = drApp.getConnectionMode();
222 var AppMode = remoting.AppMode;
223 var finishedMode = AppMode.CLIENT_SESSION_FINISHED_ME2ME;
224 var finishedButton = 'client-finished-me2me-button';
225 if (mode === remoting.DesktopRemoting.Mode.IT2ME) {
226 finishedMode = AppMode.CLIENT_SESSION_FINISHED_IT2ME;
227 finishedButton = 'client-finished-it2me-button';
230 var activity = remoting.app.getActivity();
232 return Promise.resolve();
237 return browserTest.onUIMode(finishedMode).then(function() {
238 browserTest.clickOnControl(finishedButton);
239 return browserTest.onUIMode(AppMode.HOME);
244 * @param {string} pin
245 * @param {boolean=} opt_expectError
248 browserTest.enterPIN = function(pin, opt_expectError) {
249 // Wait for 500ms before hitting the PIN button. From experiment, sometimes
250 // the PIN prompt does not dismiss without the timeout.
251 var CONNECT_PIN_WAIT = 500;
253 document.getElementById('pin-entry').value = pin;
255 return base.Promise.sleep(CONNECT_PIN_WAIT).then(function() {
256 browserTest.clickOnControl('pin-connect-button');
258 if (opt_expectError) {
259 return browserTest.expectConnectionError(
260 remoting.DesktopRemoting.Mode.ME2ME,
261 [remoting.Error.Tag.INVALID_ACCESS_CODE]);
263 return browserTest.expectConnected();
269 * @param {remoting.DesktopRemoting.Mode} connectionMode
270 * @param {Array<remoting.Error.Tag>} errorTags
273 browserTest.expectConnectionError = function(connectionMode, errorTags) {
274 var AppMode = remoting.AppMode;
275 var Timeout = browserTest.Timeout;
277 // Timeout if the session is not failed within 30 seconds.
278 var SESSION_CONNECTION_TIMEOUT = 30000;
280 var finishButton = 'client-finished-me2me-button';
281 var failureMode = AppMode.CLIENT_CONNECT_FAILED_ME2ME;
283 if (connectionMode == remoting.DesktopRemoting.Mode.IT2ME) {
284 finishButton = 'client-finished-it2me-button';
285 failureMode = AppMode.CLIENT_CONNECT_FAILED_IT2ME;
288 var onConnected = browserTest.onUIMode(AppMode.IN_SESSION, Timeout.NONE);
289 var onFailure = browserTest.onUIMode(failureMode, SESSION_CONNECTION_TIMEOUT);
291 onConnected = onConnected.then(function() {
292 return Promise.reject(
293 'Expected the connection to fail.');
296 onFailure = onFailure.then(function() {
297 /** @type {Element} */
298 var errorDiv = document.getElementById('connect-error-message');
299 var actual = errorDiv.innerText;
300 var expected = errorTags.map(function(/** string */errorTag) {
301 return l10n.getTranslationOrError(errorTag);
303 browserTest.clickOnControl(finishButton);
305 if (expected.indexOf(actual) === -1) {
306 return Promise.reject('Unexpected failure. actual: ' + actual +
307 ' expected: ' + expected.join(','));
311 return Promise.race([onConnected, onFailure]);
317 browserTest.expectConnected = function() {
318 var AppMode = remoting.AppMode;
319 // Timeout if the session is not connected within 30 seconds.
320 var SESSION_CONNECTION_TIMEOUT = 30000;
321 var onConnected = browserTest.onUIMode(AppMode.IN_SESSION,
322 SESSION_CONNECTION_TIMEOUT);
323 var onFailure = browserTest.onUIMode(AppMode.CLIENT_CONNECT_FAILED_ME2ME,
324 browserTest.Timeout.NONE);
325 onFailure = onFailure.then(function() {
326 var errorDiv = document.getElementById('connect-error-message');
327 var errorMsg = errorDiv.innerText;
328 return Promise.reject('Unexpected error - ' + errorMsg);
330 return Promise.race([onConnected, onFailure]);
334 * @param {base.EventSource} eventSource
335 * @param {string} event
336 * @param {number} timeoutMs
337 * @param {*=} opt_expectedData
340 browserTest.expectEvent = function(eventSource, event, timeoutMs,
342 return new Promise(function(fullfil, reject) {
343 /** @param {string=} actualData */
344 var verifyEventParameters = function(actualData) {
345 if (opt_expectedData === undefined || opt_expectedData === actualData) {
348 reject('Bad event data; expected ' + opt_expectedData +
349 '; got ' + actualData);
352 eventSource.addEventListener(event, verifyEventParameters);
353 base.Promise.sleep(timeoutMs).then(function() {
354 reject(Error('Event ' + event + ' not received after ' +
361 * @param {Function} testClass
364 * @suppress {checkTypes|checkVars|reportUnknownTypes}
366 browserTest.runTest = function(testClass, data) {
368 var test = new testClass();
369 browserTest.expect(typeof test.run == 'function');
371 } catch (/** @type {Error} */ e) {
377 * @param {string} newPin
380 browserTest.setupPIN = function(newPin) {
381 var AppMode = remoting.AppMode;
382 var HOST_SETUP_WAIT = 10000;
383 var Timeout = browserTest.Timeout;
385 return browserTest.onUIMode(AppMode.HOST_SETUP_ASK_PIN).then(function() {
386 document.getElementById('daemon-pin-entry').value = newPin;
387 document.getElementById('daemon-pin-confirm').value = newPin;
388 browserTest.clickOnControl('daemon-pin-ok');
390 var success = browserTest.onUIMode(AppMode.HOST_SETUP_DONE, Timeout.NONE);
391 var failure = browserTest.onUIMode(AppMode.HOST_SETUP_ERROR, Timeout.NONE);
392 failure = failure.then(function(){
393 return Promise.reject('Unexpected host setup failure');
395 return Promise.race([success, failure]);
397 console.log('browserTest: PIN Setup is done.');
398 browserTest.clickOnControl('host-config-done-dismiss');
400 // On Linux, we restart the host after changing the PIN, need to sleep
401 // for ten seconds before the host is ready for connection.
402 return base.Promise.sleep(HOST_SETUP_WAIT);
407 * @return {Promise<boolean>}
409 browserTest.isLocalHostStarted = function() {
410 return new Promise(function(resolve) {
411 remoting.hostController.getLocalHostState(function(state) {
412 resolve(remoting.HostController.State.STARTED == state);
418 * @param {string} pin
421 browserTest.ensureHostStartedWithPIN = function(pin) {
422 // Return if host is already
423 return browserTest.isLocalHostStarted().then(
424 /** @param {boolean} started */
427 console.log('browserTest: Enabling remote connection.');
428 browserTest.clickOnControl('.start-daemon');
430 console.log('browserTest: Changing the PIN of the host to: ' +
432 browserTest.clickOnControl('.change-daemon-pin');
434 return browserTest.setupPIN(pin);
439 * Called by Browser Test in C++
440 * @param {string} pin
441 * @suppress {checkTypes}
443 browserTest.ensureRemoteConnectionEnabled = function(pin) {
444 browserTest.ensureHostStartedWithPIN(pin).then(function() {
446 }, function(reason) {
447 browserTest.fail(reason);