|  | // 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<remoting.Error.Tag>} 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<boolean>} | 
|  | */ | 
|  | 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(); |