// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * * Provides basic functionality for JavaScript based browser test. * * To define a browser test, create a class under the browserTest namespace. * You can pass arbitrary object literals to the browser test from the C++ test * harness as the test data. Each browser test class should implement the run * method. * For example: * * browserTest.My_Test = function() {}; * browserTest.My_Test.prototype.run(myObjectLiteral) = function() { ... }; * * The browser test is async in nature. It will keep running until * browserTest.fail("My error message.") or browserTest.pass() is called. * * For example: * * browserTest.My_Test.prototype.run(myObjectLiteral) = function() { * window.setTimeout(function() { * if (doSomething(myObjectLiteral)) { * browserTest.pass(); * } else { * browserTest.fail('My error message.'); * } * }, 1000); * }; * * You will then invoke the test in C++ by calling: * * RunJavaScriptTest(web_content, "My_Test", "{" * "pin: '123123'" * "}"); */ 'use strict'; /** @suppress {duplicate} */ var browserTest = browserTest || {}; /** @type {window.DomAutomationController} */ browserTest.automationController_ = null; /** * @return {void} * @suppress {checkTypes|reportUnknownTypes} */ browserTest.init = function() { // The domAutomationController is used to communicate progress back to the // C++ calling code. It will only exist if chrome is run with the flag // --dom-automation. It is stubbed out here so that browser test can be run // under the regular app. if (window.domAutomationController) { /** @type {window.DomAutomationController} */ browserTest.automationController_ = window.domAutomationController; } else { browserTest.automationController_ = { send: function(json) { var result = JSON.parse(json); if (result.succeeded) { console.log('Test Passed.'); } else { console.error('Test Failed.\n' + result.error_message + '\n' + result.stack_trace); } } }; }; }; /** * Fails the C++ calling browser test with |message| if |expr| is false. * @param {*} expr * @param {string=} opt_message * @return {void} */ browserTest.expect = function(expr, opt_message) { if (!expr) { var message = (opt_message) ? '<' + opt_message + '>' : ''; browserTest.fail('Expectation failed.' + opt_message); } }; /** * @param {string|Error} error * @return {void} */ browserTest.fail = function(error) { var error_message = error; var stack_trace = new base.Callstack().toString(); if (error instanceof Error) { error_message = error.toString(); stack_trace = error.stack; } console.error(error_message); // To run browserTest locally: // 1. Go to |remoting_webapp_files| and look for // |remoting_webapp_js_browser_test_files| and uncomment it // 2. gclient runhooks // 3. rebuild the webapp // 4. Run it in the console browserTest.runTest(browserTest.MyTest, {}); // 5. The line below will trap the test in the debugger in case of // failure. debugger; browserTest.automationController_.send(JSON.stringify({ succeeded: false, error_message: error_message, stack_trace: stack_trace })); }; /** * @return {void} */ browserTest.pass = function() { browserTest.automationController_.send(JSON.stringify({ succeeded: true, error_message: '', stack_trace: '' })); }; /** * @param {string} id The id or the selector of the element. * @return {void} */ browserTest.clickOnControl = function(id) { var element = document.getElementById(id); if (!element) { element = document.querySelector(id); } browserTest.expect(element, 'No such element: ' + id); element.click(); }; /** * @param {remoting.AppMode} expectedMode * @param {number=} opt_timeout * @return {Promise} */ browserTest.onUIMode = function(expectedMode, opt_timeout) { if (expectedMode == remoting.currentMode) { // If the current mode is the same as the expected mode, return a fulfilled // promise. For some reason, if we fulfill the promise in the same // callstack, V8 will assert at V8RecursionScope.h(66) with // ASSERT(!ScriptForbiddenScope::isScriptForbidden()). // To avoid the assert, execute the callback in a different callstack. return base.Promise.sleep(0); } return new Promise (function(fulfill, reject) { var uiModeChanged = remoting.testEvents.Names.uiModeChanged; var timerId = null; if (opt_timeout === undefined) { opt_timeout = browserTest.Timeout.DEFAULT; } function onTimeout() { remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged); reject('Timeout waiting for ' + expectedMode); } /** @param {remoting.AppMode} mode */ function onUIModeChanged(mode) { if (mode == expectedMode) { remoting.testEvents.removeEventListener(uiModeChanged, onUIModeChanged); window.clearTimeout(timerId); timerId = null; fulfill(true); } } if (opt_timeout != browserTest.Timeout.NONE) { timerId = window.setTimeout(onTimeout, /** @type {number} */ (opt_timeout)); } remoting.testEvents.addEventListener(uiModeChanged, onUIModeChanged); }); }; /** * @return {Promise} */ browserTest.connectMe2Me = function() { var AppMode = remoting.AppMode; // The one second timeout is necessary because the click handler of // 'this-host-connect' is registered asynchronously. return base.Promise.sleep(1000).then(function() { browserTest.clickOnControl('local-host-connect-button'); }).then(function(){ return browserTest.onUIMode(AppMode.CLIENT_HOST_NEEDS_UPGRADE); }).then(function() { // On fulfilled. browserTest.clickOnControl('#host-needs-update-dialog .connect-button'); }, function() { // On time out. return Promise.resolve(); }).then(function() { return browserTest.onUIMode(AppMode.CLIENT_PIN_PROMPT, 10000); }); }; /** * @return {Promise} */ browserTest.disconnect = function() { console.assert(remoting.app instanceof remoting.DesktopRemoting, '|remoting.app| is not an instance of DesktopRemoting.'); var drApp = /** @type {remoting.DesktopRemoting} */ (remoting.app); var mode = drApp.getConnectionMode(); var AppMode = remoting.AppMode; var finishedMode = AppMode.CLIENT_SESSION_FINISHED_ME2ME; var finishedButton = 'client-finished-me2me-button'; if (mode === remoting.DesktopRemoting.Mode.IT2ME) { finishedMode = AppMode.CLIENT_SESSION_FINISHED_IT2ME; finishedButton = 'client-finished-it2me-button'; } var activity = remoting.app.getActivity(); if (!activity) { return Promise.resolve(); } activity.stop(); return browserTest.onUIMode(finishedMode).then(function() { browserTest.clickOnControl(finishedButton); return browserTest.onUIMode(AppMode.HOME); }); }; /** * @param {string} pin * @param {boolean=} opt_expectError * @return {Promise} */ browserTest.enterPIN = function(pin, opt_expectError) { // Wait for 500ms before hitting the PIN button. From experiment, sometimes // the PIN prompt does not dismiss without the timeout. var CONNECT_PIN_WAIT = 500; document.getElementById('pin-entry').value = pin; return base.Promise.sleep(CONNECT_PIN_WAIT).then(function() { browserTest.clickOnControl('pin-connect-button'); }).then(function() { if (opt_expectError) { return browserTest.expectConnectionError( remoting.DesktopRemoting.Mode.ME2ME, [remoting.Error.Tag.INVALID_ACCESS_CODE]); } else { return browserTest.expectConnected(); } }); }; /** * @param {remoting.DesktopRemoting.Mode} connectionMode * @param {Array} errorTags * @return {Promise} */ browserTest.expectConnectionError = function(connectionMode, errorTags) { var AppMode = remoting.AppMode; var Timeout = browserTest.Timeout; // Timeout if the session is not failed within 30 seconds. var SESSION_CONNECTION_TIMEOUT = 30000; var finishButton = 'client-finished-me2me-button'; var failureMode = AppMode.CLIENT_CONNECT_FAILED_ME2ME; if (connectionMode == remoting.DesktopRemoting.Mode.IT2ME) { finishButton = 'client-finished-it2me-button'; failureMode = AppMode.CLIENT_CONNECT_FAILED_IT2ME; } var onConnected = browserTest.onUIMode(AppMode.IN_SESSION, Timeout.NONE); var onFailure = browserTest.onUIMode(failureMode, SESSION_CONNECTION_TIMEOUT); onConnected = onConnected.then(function() { return Promise.reject( 'Expected the connection to fail.'); }); onFailure = onFailure.then(function() { /** @type {Element} */ var errorDiv = document.getElementById('connect-error-message'); var actual = errorDiv.innerText; var expected = errorTags.map(function(/** string */errorTag) { return l10n.getTranslationOrError(errorTag); }); browserTest.clickOnControl(finishButton); if (expected.indexOf(actual) === -1) { return Promise.reject('Unexpected failure. actual: ' + actual + ' expected: ' + expected.join(',')); } }); return Promise.race([onConnected, onFailure]); }; /** * @return {Promise} */ browserTest.expectConnected = function() { var AppMode = remoting.AppMode; // Timeout if the session is not connected within 30 seconds. var SESSION_CONNECTION_TIMEOUT = 30000; var onConnected = browserTest.onUIMode(AppMode.IN_SESSION, SESSION_CONNECTION_TIMEOUT); var onFailure = browserTest.onUIMode(AppMode.CLIENT_CONNECT_FAILED_ME2ME, browserTest.Timeout.NONE); onFailure = onFailure.then(function() { var errorDiv = document.getElementById('connect-error-message'); var errorMsg = errorDiv.innerText; return Promise.reject('Unexpected error - ' + errorMsg); }); return Promise.race([onConnected, onFailure]); }; /** * @param {base.EventSource} eventSource * @param {string} event * @param {number} timeoutMs * @param {*=} opt_expectedData * @return {Promise} */ browserTest.expectEvent = function(eventSource, event, timeoutMs, opt_expectedData) { return new Promise(function(fullfil, reject) { /** @param {string=} actualData */ var verifyEventParameters = function(actualData) { if (opt_expectedData === undefined || opt_expectedData === actualData) { fullfil(true); } else { reject('Bad event data; expected ' + opt_expectedData + '; got ' + actualData); } }; eventSource.addEventListener(event, verifyEventParameters); base.Promise.sleep(timeoutMs).then(function() { reject(Error('Event ' + event + ' not received after ' + timeoutMs + 'ms.')); }); }); }; /** * @param {Function} testClass * @param {*} data * @return {void} * @suppress {checkTypes|checkVars|reportUnknownTypes} */ browserTest.runTest = function(testClass, data) { try { var test = new testClass(); browserTest.expect(typeof test.run == 'function'); test.run(data); } catch (/** @type {Error} */ e) { browserTest.fail(e); } }; /** * @param {string} newPin * @return {Promise} */ browserTest.setupPIN = function(newPin) { var AppMode = remoting.AppMode; var HOST_SETUP_WAIT = 10000; var Timeout = browserTest.Timeout; return browserTest.onUIMode(AppMode.HOST_SETUP_ASK_PIN).then(function() { document.getElementById('daemon-pin-entry').value = newPin; document.getElementById('daemon-pin-confirm').value = newPin; browserTest.clickOnControl('daemon-pin-ok'); var success = browserTest.onUIMode(AppMode.HOST_SETUP_DONE, Timeout.NONE); var failure = browserTest.onUIMode(AppMode.HOST_SETUP_ERROR, Timeout.NONE); failure = failure.then(function(){ return Promise.reject('Unexpected host setup failure'); }); return Promise.race([success, failure]); }).then(function() { console.log('browserTest: PIN Setup is done.'); browserTest.clickOnControl('host-config-done-dismiss'); // On Linux, we restart the host after changing the PIN, need to sleep // for ten seconds before the host is ready for connection. return base.Promise.sleep(HOST_SETUP_WAIT); }); }; /** * @return {Promise} */ browserTest.isLocalHostStarted = function() { return new Promise(function(resolve) { remoting.hostController.getLocalHostState(function(state) { resolve(remoting.HostController.State.STARTED == state); }); }); }; /** * @param {string} pin * @return {Promise} */ browserTest.ensureHostStartedWithPIN = function(pin) { // Return if host is already return browserTest.isLocalHostStarted().then( /** @param {boolean} started */ function(started){ if (!started) { console.log('browserTest: Enabling remote connection.'); browserTest.clickOnControl('.start-daemon'); } else { console.log('browserTest: Changing the PIN of the host to: ' + pin + '.'); browserTest.clickOnControl('.change-daemon-pin'); } return browserTest.setupPIN(pin); }); }; /** * Called by Browser Test in C++ * @param {string} pin * @suppress {checkTypes} */ browserTest.ensureRemoteConnectionEnabled = function(pin) { browserTest.ensureHostStartedWithPIN(pin).then(function() { browserTest.pass(); }, function(reason) { browserTest.fail(reason); }); }; browserTest.init();