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