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} message
82 browserTest
.expect = function(expr
, message
) {
84 message
= (message
) ? '<' + message
+ '>' : '';
85 browserTest
.fail('Expectation failed.' + message
);
90 * @param {string|Error} error
93 browserTest
.fail = function(error
) {
94 var error_message
= error
;
95 var stack_trace
= base
.debug
.callstack();
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 var AppMode
= remoting
.AppMode
;
218 var finishedMode
= AppMode
.CLIENT_SESSION_FINISHED_ME2ME
;
219 var finishedButton
= 'client-finished-me2me-button';
220 if (remoting
.app
.getConnectionMode() === remoting
.Application
.Mode
.IT2ME
) {
221 finishedMode
= AppMode
.CLIENT_SESSION_FINISHED_IT2ME
;
222 finishedButton
= 'client-finished-it2me-button';
225 var activity
= remoting
.app
.getActivity();
227 return Promise
.resolve();
232 return browserTest
.onUIMode(finishedMode
).then(function() {
233 browserTest
.clickOnControl(finishedButton
);
234 return browserTest
.onUIMode(AppMode
.HOME
);
239 * @param {string} pin
240 * @param {?boolean} opt_expectError
243 browserTest
.enterPIN = function(pin
, opt_expectError
) {
244 // Wait for 500ms before hitting the PIN button. From experiment, sometimes
245 // the PIN prompt does not dismiss without the timeout.
246 var CONNECT_PIN_WAIT
= 500;
248 document
.getElementById('pin-entry').value
= pin
;
250 return base
.Promise
.sleep(CONNECT_PIN_WAIT
).then(function() {
251 browserTest
.clickOnControl('pin-connect-button');
253 if (opt_expectError
) {
254 return browserTest
.expectConnectionError(
255 remoting
.Application
.Mode
.ME2ME
,
256 [remoting
.Error
.Tag
.INVALID_ACCESS_CODE
]);
258 return browserTest
.expectConnected();
264 * @param {remoting.Application.Mode} connectionMode
265 * @param {Array<remoting.Error.Tag>} errorTags
268 browserTest
.expectConnectionError = function(connectionMode
, errorTags
) {
269 var AppMode
= remoting
.AppMode
;
270 var Timeout
= browserTest
.Timeout
;
272 var finishButton
= 'client-finished-me2me-button';
274 if (connectionMode
== remoting
.Application
.Mode
.IT2ME
) {
275 finishButton
= 'client-finished-it2me-button';
278 var onConnected
= browserTest
.onUIMode(AppMode
.IN_SESSION
, Timeout
.NONE
);
279 var onFailure
= Promise
.race([
280 browserTest
.onUIMode(AppMode
.CLIENT_CONNECT_FAILED_ME2ME
),
281 browserTest
.onUIMode(AppMode
.CLIENT_CONNECT_FAILED_IT2ME
)]);
283 onConnected
= onConnected
.then(function() {
284 return Promise
.reject(
285 'Expected the connection to fail.');
288 onFailure
= onFailure
.then(function() {
289 /** @type {Element} */
290 var errorDiv
= document
.getElementById('connect-error-message');
291 var actual
= errorDiv
.innerText
;
292 var expected
= errorTags
.map(function(/** string */errorTag
) {
293 return l10n
.getTranslationOrError(errorTag
);
295 browserTest
.clickOnControl(finishButton
);
297 if (expected
.indexOf(actual
) === -1) {
298 return Promise
.reject('Unexpected failure. actual: ' + actual
+
299 ' expected: ' + expected
.join(','));
303 return Promise
.race([onConnected
, onFailure
]);
309 browserTest
.expectConnected = function() {
310 var AppMode
= remoting
.AppMode
;
311 // Timeout if the session is not connected within 30 seconds.
312 var SESSION_CONNECTION_TIMEOUT
= 30000;
313 var onConnected
= browserTest
.onUIMode(AppMode
.IN_SESSION
,
314 SESSION_CONNECTION_TIMEOUT
);
315 var onFailure
= browserTest
.onUIMode(AppMode
.CLIENT_CONNECT_FAILED_ME2ME
,
316 browserTest
.Timeout
.NONE
);
317 onFailure
= onFailure
.then(function() {
318 var errorDiv
= document
.getElementById('connect-error-message');
319 var errorMsg
= errorDiv
.innerText
;
320 return Promise
.reject('Unexpected error - ' + errorMsg
);
322 return Promise
.race([onConnected
, onFailure
]);
326 * @param {base.EventSource} eventSource
327 * @param {string} event
328 * @param {number} timeoutMs
329 * @param {*=} opt_expectedData
332 browserTest
.expectEvent = function(eventSource
, event
, timeoutMs
,
334 return new Promise(function(fullfil
, reject
) {
335 /** @param {string=} actualData */
336 var verifyEventParameters = function(actualData
) {
337 if (opt_expectedData
=== undefined || opt_expectedData
=== actualData
) {
340 reject('Bad event data; expected ' + opt_expectedData
+
341 '; got ' + actualData
);
344 eventSource
.addEventListener(event
, verifyEventParameters
);
345 base
.Promise
.sleep(timeoutMs
).then(function() {
346 reject(Error('Event ' + event
+ ' not received after ' +
353 * @param {Function} testClass
356 * @suppress {checkTypes|checkVars|reportUnknownTypes}
358 browserTest
.runTest = function(testClass
, data
) {
360 var test
= new testClass();
361 browserTest
.expect(typeof test
.run
== 'function');
363 } catch (/** @type {Error} */ e
) {
369 * @param {string} newPin
372 browserTest
.setupPIN = function(newPin
) {
373 var AppMode
= remoting
.AppMode
;
374 var HOST_SETUP_WAIT
= 10000;
375 var Timeout
= browserTest
.Timeout
;
377 return browserTest
.onUIMode(AppMode
.HOST_SETUP_ASK_PIN
).then(function() {
378 document
.getElementById('daemon-pin-entry').value
= newPin
;
379 document
.getElementById('daemon-pin-confirm').value
= newPin
;
380 browserTest
.clickOnControl('daemon-pin-ok');
382 var success
= browserTest
.onUIMode(AppMode
.HOST_SETUP_DONE
, Timeout
.NONE
);
383 var failure
= browserTest
.onUIMode(AppMode
.HOST_SETUP_ERROR
, Timeout
.NONE
);
384 failure
= failure
.then(function(){
385 return Promise
.reject('Unexpected host setup failure');
387 return Promise
.race([success
, failure
]);
389 console
.log('browserTest: PIN Setup is done.');
390 browserTest
.clickOnControl('host-config-done-dismiss');
392 // On Linux, we restart the host after changing the PIN, need to sleep
393 // for ten seconds before the host is ready for connection.
394 return base
.Promise
.sleep(HOST_SETUP_WAIT
);
399 * @return {Promise<boolean>}
401 browserTest
.isLocalHostStarted = function() {
402 return new Promise(function(resolve
) {
403 remoting
.hostController
.getLocalHostState(function(state
) {
404 resolve(remoting
.HostController
.State
.STARTED
== state
);
410 * @param {string} pin
413 browserTest
.ensureHostStartedWithPIN = function(pin
) {
414 // Return if host is already
415 return browserTest
.isLocalHostStarted().then(
416 /** @param {boolean} started */
419 console
.log('browserTest: Enabling remote connection.');
420 browserTest
.clickOnControl('.start-daemon');
422 console
.log('browserTest: Changing the PIN of the host to: ' +
424 browserTest
.clickOnControl('.change-daemon-pin');
426 return browserTest
.setupPIN(pin
);
431 * Called by Browser Test in C++
432 * @param {string} pin
433 * @suppress {checkTypes}
435 browserTest
.ensureRemoteConnectionEnabled = function(pin
) {
436 browserTest
.ensureHostStartedWithPIN(pin
).then(function() {
438 }, function(reason
) {
439 browserTest
.fail(reason
);