blob: c247a15e28bd86c244e53749e3525d162eff3598 [file] [log] [blame]
// 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();