Commit 23005b6b authored by kelvinp@chromium.org's avatar kelvinp@chromium.org

Hangouts remote desktop part III - It2MeService

This CL
- Introduces an It2MeService component that listens to incoming connection requests between Hangouts and the webapp and establish a channel between them.
- It enables launching an IT2Me helper session from Hangouts

Review URL: https://codereview.chromium.org/468693002

Cr-Commit-Position: refs/heads/master@{#289538}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@289538 0039d316-1c4b-4281-b951-d872f2087c98
parent b8e70ea6
......@@ -327,7 +327,7 @@
'destination': '<(output_dir)',
'files': [
'<@(webapp_js_files)',
'<@(remoting_webapp_unittest_cases)',
'<@(remoting_webapp_unittest_js_files)',
'<@(remoting_webapp_unittest_additional_files)'
],
},
......@@ -339,7 +339,7 @@
'webapp/build-html.py',
'<(remoting_webapp_unittest_template_main)',
'<@(webapp_js_files)',
'<@(remoting_webapp_unittest_cases)'
'<@(remoting_webapp_unittest_js_files)'
],
'outputs': [
'<(output_dir)/unittest.html',
......@@ -353,7 +353,7 @@
# instrumentedjs flag or else GYP will ignore the files in the
# exclude list.
'--exclude-js', '<@(remoting_webapp_unittest_exclude_files)',
'--js', '<@(remoting_webapp_unittest_cases)',
'--js', '<@(remoting_webapp_unittest_js_files)',
'--instrument-js', '<@(webapp_js_files)',
],
},
......
......@@ -139,11 +139,14 @@
'webapp/event_handlers.js',
],
# The unit test cases for the webapp
'remoting_webapp_unittest_cases': [
'remoting_webapp_unittest_js_files': [
'webapp/js_proto/chrome_proto.js',
'webapp/unittests/chrome_mocks.js',
'webapp/unittests/base_unittest.js',
'webapp/unittests/l10n_unittest.js',
'webapp/unittests/menu_button_unittest.js',
'webapp/unittests/it2me_helper_channel_unittest.js',
'webapp/unittests/it2me_service_unittest.js'
],
'remoting_webapp_unittest_additional_files': [
'webapp/menu_button.css',
......@@ -177,7 +180,9 @@
'webapp/client_session.js',
'webapp/typecheck.js',
'webapp/background/app_launcher.js',
'webapp/background/background.js'
'webapp/background/background.js',
'webapp/background/it2me_helper_channel.js',
'webapp/background/it2me_service.js',
],
# The JavaScript files required by wcs_sandbox.html.
......
......@@ -61,7 +61,7 @@ remoting.V1AppLauncher.prototype.launch = function(opt_launchArgs) {
if (!tab) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(tab.id);
resolve(String(tab.id));
}
});
});
......
......@@ -43,6 +43,29 @@ function initializeAppV2(appLauncher) {
);
}
/**
* The background service is responsible for listening to incoming connection
* requests from Hangouts and the webapp.
*
* @param {remoting.AppLauncher} appLauncher
*/
function initializeBackgroundService(appLauncher) {
function initializeIt2MeService() {
/** @type {remoting.It2MeService} */
remoting.it2meService = new remoting.It2MeService(appLauncher);
remoting.it2meService.init();
}
chrome.runtime.onSuspend.addListener(function() {
base.debug.assert(remoting.it2meService != null);
remoting.it2meService.dispose();
remoting.it2meService = null;
});
chrome.runtime.onSuspendCanceled.addListener(initializeIt2MeService);
initializeIt2MeService();
}
function main() {
/** @type {remoting.AppLauncher} */
var appLauncher = new remoting.V1AppLauncher();
......@@ -50,6 +73,7 @@ function main() {
appLauncher = new remoting.V2AppLauncher();
initializeAppV2(appLauncher);
}
initializeBackgroundService(appLauncher);
}
window.addEventListener('load', main, false);
......
// 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
*
* It2MeHelperChannel relays messages between Hangouts and Chrome Remote Desktop
* (webapp) for the helper (the Hangouts participant who is giving remote
* assistance).
*
* It runs in the background page and contains two chrome.runtime.Port objects,
* respresenting connections to the webapp and hangout, respectively.
*
* Connection is always initiated from Hangouts.
*
* Hangout It2MeHelperChannel Chrome Remote Desktop
* |-----runtime.connect() ------>| |
* |------connect message-------->| |
* | |-------appLauncher.launch()---->|
* | |<------runtime.connect()------- |
* | |<-----sessionStateChanged------ |
* |<----sessionStateChanged------| |
*
* Disconnection can be initiated from either side:
* 1. In the normal flow initiated from hangout
* Hangout It2MeHelperChannel Chrome Remote Desktop
* |-----disconnect message------>| |
* |<-sessionStateChanged(CLOSED)-| |
* | |-----appLauncher.close()------>|
*
* 2. In the normal flow initiated from webapp
* Hangout It2MeHelperChannel Chrome Remote Desktop
* | |<-sessionStateChanged(CLOSED)--|
* | |<--------port.disconnect()-----|
* |<--------port.disconnect()----| |
*
* 2. If hangout crashes
* Hangout It2MeHelperChannel Chrome Remote Desktop
* |---------port.disconnect()--->| |
* | |--------port.disconnect()----->|
* | |------appLauncher.close()----->|
*
* 3. If webapp crashes
* Hangout It2MeHelperChannel Chrome Remote Desktop
* | |<-------port.disconnect()------|
* |<-sessionStateChanged(FAILED)-| |
* |<--------port.disconnect()----| |
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @param {remoting.AppLauncher} appLauncher
* @param {chrome.runtime.Port} hangoutPort Represents an active connection to
* Hangouts.
* @param {function(remoting.It2MeHelperChannel)} onDisconnectCallback Callback
* to notify when the connection is torn down. IT2MeService uses this
* callback to dispose of the channel object.
* @constructor
*/
remoting.It2MeHelperChannel =
function(appLauncher, hangoutPort, onDisconnectCallback) {
/**
* @type {remoting.AppLauncher}
* @private
*/
this.appLauncher_ = appLauncher;
/**
* @type {chrome.runtime.Port}
* @private
*/
this.hangoutPort_ = hangoutPort;
/**
* @type {chrome.runtime.Port}
* @private
*/
this.webappPort_ = null;
/**
* @type {string}
* @private
*/
this.instanceId_ = '';
/**
* @type {remoting.ClientSession.State}
* @private
*/
this.sessionState_ = remoting.ClientSession.State.CONNECTING;
/**
* @type {?function(remoting.It2MeHelperChannel)}
* @private
*/
this.onDisconnectCallback_ = onDisconnectCallback;
this.onWebappMessageRef_ = this.onWebappMessage_.bind(this);
this.onWebappDisconnectRef_ = this.onWebappDisconnect_.bind(this);
this.onHangoutMessageRef_ = this.onHangoutMessage_.bind(this);
this.onHangoutDisconnectRef_ = this.onHangoutDisconnect_.bind(this);
};
/** @enum {string} */
remoting.It2MeHelperChannel.HangoutMessageTypes = {
CONNECT: 'connect',
DISCONNECT: 'disconnect'
};
/** @enum {string} */
remoting.It2MeHelperChannel.WebappMessageTypes = {
SESSION_STATE_CHANGED: 'sessionStateChanged'
};
remoting.It2MeHelperChannel.prototype.init = function() {
this.hangoutPort_.onMessage.addListener(this.onHangoutMessageRef_);
this.hangoutPort_.onDisconnect.addListener(this.onHangoutDisconnectRef_);
};
/** @return {string} */
remoting.It2MeHelperChannel.prototype.instanceId = function() {
return this.instanceId_;
};
/**
* @param {{method:string, data:Object.<string,*>}} message
* @return {boolean} whether the message is handled or not.
* @private
*/
remoting.It2MeHelperChannel.prototype.onHangoutMessage_ = function(message) {
try {
var MessageTypes = remoting.It2MeHelperChannel.HangoutMessageTypes;
switch (message.method) {
case MessageTypes.CONNECT:
this.launchWebapp_(message);
return true;
case MessageTypes.DISCONNECT:
this.closeWebapp_(message);
return true;
}
} catch(e) {
var error = /** @type {Error} */ e;
console.error(error);
this.hangoutPort_.postMessage({
method: message.method + 'Response',
error: error.message
});
}
return false;
};
/**
* Disconnect the existing connection to the helpee.
*
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelperChannel.prototype.closeWebapp_ =
function(message) {
// TODO(kelvinp): Closing the v2 app currently doesn't disconnect the IT2me
// session (crbug.com/402137), so send an explicit notification to Hangouts.
this.sessionState_ = remoting.ClientSession.State.CLOSED;
this.hangoutPort_.postMessage({
method: 'sessionStateChanged',
state: this.sessionState_
});
this.appLauncher_.close(this.instanceId_);
};
/**
* Launches the web app.
*
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelperChannel.prototype.launchWebapp_ =
function(message) {
var accessCode = getStringAttr(message, 'accessCode');
if (!accessCode) {
throw new Error('Access code is missing');
}
// Launch the webapp.
this.appLauncher_.launch({
mode: 'hangout',
accessCode: accessCode
}).then(
/**
* @this {remoting.It2MeHelperChannel}
* @param {string} instanceId
*/
function(instanceId){
this.instanceId_ = instanceId;
}.bind(this));
};
/**
* @private
*/
remoting.It2MeHelperChannel.prototype.onHangoutDisconnect_ = function() {
this.appLauncher_.close(this.instanceId_);
this.unhookPorts_();
};
/**
* @param {chrome.runtime.Port} port The port represents a connection to the
* webapp.
* @param {string} id The id of the tab or window that is hosting the webapp.
*/
remoting.It2MeHelperChannel.prototype.onWebappConnect = function(port, id) {
base.debug.assert(id === this.instanceId_);
base.debug.assert(this.hangoutPort_ !== null);
// Hook listeners.
port.onMessage.addListener(this.onWebappMessageRef_);
port.onDisconnect.addListener(this.onWebappDisconnectRef_);
this.webappPort_ = port;
};
/** @param {chrome.runtime.Port} port The webapp port. */
remoting.It2MeHelperChannel.prototype.onWebappDisconnect_ = function(port) {
// If the webapp port got disconnected while the session is still connected,
// treat it as an error.
var States = remoting.ClientSession.State;
if (this.sessionState_ === States.CONNECTING ||
this.sessionState_ === States.CONNECTED) {
this.sessionState_ = States.FAILED;
this.hangoutPort_.postMessage({
method: 'sessionStateChanged',
state: this.sessionState_
});
}
this.unhookPorts_();
};
/**
* @param {{method:string, data:Object.<string,*>}} message
* @private
*/
remoting.It2MeHelperChannel.prototype.onWebappMessage_ = function(message) {
try {
console.log('It2MeHelperChannel id=' + this.instanceId_ +
' incoming message method=' + message.method);
var MessageTypes = remoting.It2MeHelperChannel.WebappMessageTypes;
switch (message.method) {
case MessageTypes.SESSION_STATE_CHANGED:
var state = getNumberAttr(message, 'state');
this.sessionState_ =
/** @type {remoting.ClientSession.State} */ state;
this.hangoutPort_.postMessage(message);
return true;
}
throw new Error('Unknown message method=' + message.method);
} catch(e) {
var error = /** @type {Error} */ e;
console.error(error);
this.webappPort_.postMessage({
method: message.method + 'Response',
error: error.message
});
}
return false;
};
remoting.It2MeHelperChannel.prototype.unhookPorts_ = function() {
if (this.webappPort_) {
this.webappPort_.onMessage.removeListener(this.onWebappMessageRef_);
this.webappPort_.onDisconnect.removeListener(this.onWebappDisconnectRef_);
this.webappPort_.disconnect();
this.webappPort_ = null;
}
if (this.hangoutPort_) {
this.hangoutPort_.onMessage.removeListener(this.onHangoutMessageRef_);
this.hangoutPort_.onDisconnect.removeListener(this.onHangoutDisconnectRef_);
this.hangoutPort_.disconnect();
this.hangoutPort_ = null;
}
if (this.onDisconnectCallback_) {
this.onDisconnectCallback_(this);
this.onDisconnectCallback_ = null;
}
};
// 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
* It2MeService listens to incoming connections requests from Hangouts
* and the webapp and creates a It2MeHelperChannel between them.
* It supports multiple helper sessions, but only a single helpee.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @param {remoting.AppLauncher} appLauncher
* @constructor
*/
remoting.It2MeService = function(appLauncher) {
/**
* @type {remoting.AppLauncher}
* @private
*/
this.appLauncher_ = appLauncher;
/**
* @type {Array.<remoting.It2MeHelperChannel>}
* @private
*/
this.helpers_ = [];
/** @private */
this.helpee_ = null;
this.onWebappConnectRef_ = this.onWebappConnect_.bind(this);
this.onMessageExternalRef_ = this.onMessageExternal_.bind(this);
this.onConnectExternalRef_ = this.onConnectExternal_.bind(this);
};
/** @enum {string} */
remoting.It2MeService.ConnectionTypes = {
HELPER_HANGOUT: 'it2me.helper.hangout',
HELPEE_HANGOUT: 'it2me.helpee.hangout',
HELPER_WEBAPP: 'it2me.helper.webapp'
};
/**
* Starts listening to external connection from Hangouts and the webapp.
*/
remoting.It2MeService.prototype.init = function() {
chrome.runtime.onConnect.addListener(this.onWebappConnectRef_);
chrome.runtime.onMessageExternal.addListener(this.onMessageExternalRef_);
chrome.runtime.onConnectExternal.addListener(this.onConnectExternalRef_);
};
remoting.It2MeService.prototype.dispose = function() {
chrome.runtime.onConnect.removeListener(this.onWebappConnectRef_);
chrome.runtime.onMessageExternal.removeListener(
this.onMessageExternalRef_);
chrome.runtime.onConnectExternal.removeListener(
this.onConnectExternalRef_);
};
/**
* This function is called when a runtime message is received from an external
* web page (hangout) or extension.
*
* @param {{method:string, data:Object.<string,*>}} message
* @param {chrome.runtime.MessageSender} sender
* @param {function(*):void} sendResponse
* @private
*/
remoting.It2MeService.prototype.onMessageExternal_ =
function(message, sender, sendResponse) {
try {
var method = message.method;
if (method == 'hello') {
// The hello message is used by hangouts to detect whether the app is
// installed and what features are supported.
sendResponse({
method: 'helloResponse',
supportedFeatures: ['it2me']
});
return true;
}
throw new Error('Unknown method: ' + method);
} catch (e) {
var error = /** @type {Error} */ e;
console.error(error);
sendResponse({
method: message.method + 'Response',
error: error.message
});
}
return false;
};
/**
* This function is called when Hangouts connects via chrome.runtime.connect.
* Only web pages that are white-listed in the manifest are allowed to connect.
*
* @param {chrome.runtime.Port} port
* @private
*/
remoting.It2MeService.prototype.onConnectExternal_ = function(port) {
var ConnectionTypes = remoting.It2MeService.ConnectionTypes;
try {
switch (port.name) {
case ConnectionTypes.HELPER_HANGOUT:
this.handleExternalHelperConnection_(port);
return true;
default:
throw new Error('Unsupported port - ' + port.name);
}
} catch (e) {
var error = /**@type {Error} */ e;
console.error(error);
port.disconnect();
}
return false;
};
/**
* @param {chrome.runtime.Port} port
* @private
*/
remoting.It2MeService.prototype.onWebappConnect_ = function(port) {
try {
console.log('Incoming helper connection from webapp.');
// The senderId (tabId or windowId) of the webapp is embedded in the port
// name with the format port_name@senderId.
var parts = port.name.split('@');
var portName = parts[0];
var senderId = parts[1];
var ConnectionTypes = remoting.It2MeService.ConnectionTypes;
if (portName === ConnectionTypes.HELPER_WEBAPP && senderId !== undefined) {
for (var i = 0; i < this.helpers_.length; i++) {
var helper = this.helpers_[i];
if (helper.instanceId() === senderId) {
helper.onWebappConnect(port, senderId);
return;
}
}
}
throw new Error('No matching hangout connection found for ' + port.name);
} catch (e) {
var error = /** @type {Error} */ e;
console.error(error);
port.disconnect();
}
};
/**
* @param {remoting.It2MeHelperChannel} helper
*/
remoting.It2MeService.prototype.onHelperChannelDisconnected = function(helper) {
for (var i = 0; i < this.helpers_.length; i++) {
if (helper === this.helpers_[i]) {
this.helpers_.splice(i, 1);
}
}
};
/**
* @param {chrome.runtime.Port} port
* @private
*/
remoting.It2MeService.prototype.handleExternalHelperConnection_ =
function(port) {
if (this.helpee_) {
console.error(
'Cannot start a helper session while a helpee session is in process.');
port.disconnect();
}
console.log('Incoming helper connection from Hangouts');
var helper = new remoting.It2MeHelperChannel(
this.appLauncher_, port, this.onHelperChannelDisconnected.bind(this));
helper.init();
this.helpers_.push(helper);
};
......@@ -137,8 +137,8 @@ function showConnectError_(errorTag) {
if (mode == remoting.ClientSession.Mode.IT2ME) {
remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
remoting.hangoutSessionEvents.raiseEvent(
remoting.hangoutSessionEvents.sessionStateChanged,
remoting.ClientSession.State.FAILED
remoting.hangoutSessionEvents.sessionStateChanged,
remoting.ClientSession.State.FAILED
);
} else {
remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
......@@ -325,8 +325,8 @@ remoting.onConnected = function(clientSession) {
remoting.clipboard.startSession();
updateStatistics_();
remoting.hangoutSessionEvents.raiseEvent(
remoting.hangoutSessionEvents.sessionStateChanged,
remoting.ClientSession.State.CONNECTED
remoting.hangoutSessionEvents.sessionStateChanged,
remoting.ClientSession.State.CONNECTED
);
if (remoting.connector.pairingRequested) {
/**
......
......@@ -17,17 +17,25 @@ var remoting = remoting || {};
/**
* @constructor
* @param {string} senderId id of the current tab or window.
*/
remoting.HangoutSession = function() {
remoting.HangoutSession = function(senderId) {
/**
* @private
* @type {chrome.runtime.Port}
*/
this.port_ = null;
/**
* @private
* @type {string}
*/
this.senderId_ = senderId;
};
remoting.HangoutSession.prototype.init = function() {
this.port_ = chrome.runtime.connect({name: 'it2me.helper.webapp'});
var portName = 'it2me.helper.webapp@' + this.senderId_;
this.port_ = chrome.runtime.connect({name: portName});
remoting.hangoutSessionEvents.addEventListener(
remoting.hangoutSessionEvents.sessionStateChanged,
......@@ -44,6 +52,8 @@ remoting.HangoutSession.prototype.onSessionStateChanged_ = function(state) {
} catch (e) {
// postMessage will throw an exception if the port is disconnected.
// We can safely ignore this exception.
var error = /** @type {Error} */ e;
console.error(error);
} finally {
if (state === State.FAILED || state === State.CLOSED) {
// close the current window
......
......@@ -60,6 +60,8 @@ chrome.runtime = {
/** @type {chrome.Event} */
onSuspend: null,
/** @type {chrome.Event} */
onSuspendCanceled: null,
/** @type {chrome.Event} */
onConnect: null,
/** @type {chrome.Event} */
onConnectExternal: null,
......
......@@ -184,8 +184,8 @@ MediaSource.prototype.addSourceBuffer = function(format) {}
var Promise = function (init) {};
/**
* @param {function(*=) : (Promise|void)} onFulfill
* @param {function(*=) : (Promise|void)} onReject
* @param {function(?=) : (Promise|void)} onFulfill
* @param {function(?=) : (Promise|void)=} onReject
* @return {Promise}
*/
Promise.prototype.then = function (onFulfill, onReject) {};
......
......@@ -14,9 +14,15 @@
{% else %}
"background": {
"page": "background.html"
}
}
{% endif %}
},
{% if webapp_type == 'v1' %}
"background": {
"page": "background.html",
"persistent": false
},
{% endif %}
"icons": {
"128": "chromoting128.webp",
"48": "chromoting48.webp",
......@@ -54,7 +60,11 @@
"pages": [ "wcs_sandbox.html" ]
},
{% endif %}
"externally_connectable": {
"matches": [
"https://*.talkgadget.google.com/*"
]
},
"permissions": [
"{{ OAUTH2_ACCOUNTS_HOST }}/*",
"{{ OAUTH2_API_BASE_URL }}/*",
......
......@@ -133,23 +133,52 @@ remoting.init = function() {
button.disabled = true;
}
/**
* @return {Promise} A promise that resolves to the id of the current
* containing tab/window.
*/
var getCurrentId = function () {
if (remoting.isAppsV2) {
return Promise.resolve(chrome.app.window.current().id);
}
/**
* @param {function(*=):void} resolve
* @param {function(*=):void} reject
*/
return new Promise(function(resolve, reject) {
/** @param {chrome.Tab} tab */
chrome.tabs.getCurrent(function(tab){
if (tab) {
resolve(String(tab.id));
}
reject('Cannot retrieve the current tab.');
});
});
};
var onLoad = function() {
// Parse URL parameters.
var urlParams = getUrlParameters_();
if ('mode' in urlParams) {
if (urlParams['mode'] == 'me2me') {
if (urlParams['mode'] === 'me2me') {
var hostId = urlParams['hostId'];
remoting.connectMe2Me(hostId);
return;
} else if (urlParams['mode'] == 'hangout') {
var accessCode = urlParams['accessCode'];
remoting.ensureSessionConnector_();
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
remoting.connector.connectIT2Me(accessCode);
document.body.classList.add('hangout-remote-desktop');
var hangoutSession = new remoting.HangoutSession();
hangoutSession.init();
} else if (urlParams['mode'] === 'hangout') {
/** @param {*} id */
getCurrentId().then(function(id) {
/** @type {string} */
var accessCode = urlParams['accessCode'];
remoting.ensureSessionConnector_();
remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
remoting.connector.connectIT2Me(accessCode);
document.body.classList.add('hangout-remote-desktop');
var senderId = /** @type {string} */ String(id);
var hangoutSession = new remoting.HangoutSession(senderId);
hangoutSession.init();
});
return;
}
}
......
// 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.
// This file contains various mock objects for the chrome platform to make
// unit testing easier.
(function(scope){
var chromeMocks = {};
chromeMocks.Event = function() {
this.listeners_ = [];
};
chromeMocks.Event.prototype.addListener = function(callback) {
this.listeners_.push(callback);
};
chromeMocks.Event.prototype.removeListener = function(callback) {
for (var i = 0; i < this.listeners_.length; i++) {
if (this.listeners_[i] === callback) {
this.listeners_.splice(i, 1);
break;
}
}
};
chromeMocks.Event.prototype.mock$fire = function(data) {
this.listeners_.forEach(function(listener){
listener(data);
});
};
chromeMocks.runtime = {};
chromeMocks.runtime.Port = function() {
this.onMessage = new chromeMocks.Event();
this.onDisconnect = new chromeMocks.Event();
this.name = '';
this.sender = null;
};
chromeMocks.runtime.Port.prototype.disconnect = function() {};
chromeMocks.runtime.Port.prototype.postMessage = function() {};
scope.chromeMocks = chromeMocks;
})(window);
// 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.
(function() {
'use strict';
var appLauncher = null;
var hangoutPort = null;
var webappPort = null;
var helperChannel = null;
var disconnectCallback = null;
module('It2MeHelperChannel', {
setup: function() {
// App Launcher.
appLauncher = {
launch: function () {
return promiseResolveSynchronous('tabId');
},
close: function () {}
};
appLauncher.launch = sinon.spy(appLauncher, 'launch');
appLauncher.close = sinon.spy(appLauncher, 'close');
// HangoutPort.
hangoutPort = new chromeMocks.runtime.Port();
hangoutPort.postMessage = sinon.spy(hangoutPort, 'postMessage');
hangoutPort.disconnect = sinon.spy(hangoutPort, 'disconnect');
// WebappPort.
webappPort = new chromeMocks.runtime.Port();
webappPort.sender = {
tab : {
id : 'tabId'
}
};
webappPort.postMessage = sinon.spy(webappPort, 'postMessage');
webappPort.disconnect = sinon.spy(webappPort, 'disconnect');
// disconnect callback
disconnectCallback = sinon.spy();
// HelperChannel.
helperChannel = new remoting.It2MeHelperChannel(
appLauncher, hangoutPort, disconnectCallback);
helperChannel.init();
hangoutPort.onMessage.mock$fire({
method: remoting.It2MeHelperChannel.HangoutMessageTypes.CONNECT,
accessCode: "123412341234"
});
},
});
function promiseResolveSynchronous(value) {
return {
then: function(callback) {
callback('tabId');
}
};
}
test('onHangoutMessage_(|connect|) should launch the webapp',
function() {
sinon.assert.called(appLauncher.launch);
QUnit.equal(helperChannel.instanceId(), 'tabId');
});
test('onWebappMessage() should forward messages to hangout', function() {
// Execute.
helperChannel.onWebappConnect(webappPort);
webappPort.onMessage.mock$fire({
method:'sessionStateChanged',
state:remoting.ClientSession.State.CONNECTING
});
webappPort.onMessage.mock$fire({
method:'sessionStateChanged',
state:remoting.ClientSession.State.CONNECTED
});
// Verify events are forwarded.
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state:remoting.ClientSession.State.CONNECTING
});
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state:remoting.ClientSession.State.CONNECTED
});
});
test('should notify hangout when the webapp crashes', function() {
// Execute.
helperChannel.onWebappConnect(webappPort);
webappPort.onDisconnect.mock$fire();
// Verify events are forwarded.
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state: remoting.ClientSession.State.FAILED
});
sinon.assert.called(hangoutPort.disconnect);
sinon.assert.calledOnce(disconnectCallback);
});
test('should notify hangout when the session is ended', function() {
// Execute.
helperChannel.onWebappConnect(webappPort);
webappPort.onMessage.mock$fire({
method:'sessionStateChanged',
state:remoting.ClientSession.State.CLOSED
});
webappPort.onDisconnect.mock$fire();
// Verify events are forwarded.
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state:remoting.ClientSession.State.CLOSED
});
sinon.assert.called(hangoutPort.disconnect);
sinon.assert.calledOnce(disconnectCallback);
});
test('should notify hangout when the session has error', function() {
helperChannel.onWebappConnect(webappPort);
webappPort.onMessage.mock$fire({
method:'sessionStateChanged',
state:remoting.ClientSession.State.FAILED
});
webappPort.onDisconnect.mock$fire();
// Verify events are forwarded.
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state:remoting.ClientSession.State.FAILED
});
sinon.assert.called(hangoutPort.disconnect);
sinon.assert.calledOnce(disconnectCallback);
});
test('onHangoutMessages_(disconnect) should close the webapp', function() {
// Execute.
helperChannel.onWebappConnect(webappPort);
hangoutPort.onMessage.mock$fire({
method: remoting.It2MeHelperChannel.HangoutMessageTypes.DISCONNECT
});
sinon.assert.calledOnce(appLauncher.close);
// Webapp will respond by disconnecting the port
webappPort.onDisconnect.mock$fire();
// Verify events are forwarded.
sinon.assert.calledWith(hangoutPort.postMessage, {
method:'sessionStateChanged',
state:remoting.ClientSession.State.CLOSED
});
sinon.assert.called(webappPort.disconnect);
sinon.assert.called(hangoutPort.disconnect);
});
test('should close the webapp when hangout crashes', function() {
// Execute.
helperChannel.onWebappConnect(webappPort);
hangoutPort.onDisconnect.mock$fire();
sinon.assert.calledOnce(appLauncher.close);
sinon.assert.calledOnce(disconnectCallback);
sinon.assert.called(hangoutPort.disconnect);
sinon.assert.called(webappPort.disconnect);
});
})();
// 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.
(function() {
'use strict';
var appLauncher = null;
var hangoutPort = null;
var webappPort = null;
var it2meService = null;
function createPort(name, senderId) {
var port = new chromeMocks.runtime.Port();
port.name = (senderId) ? name +'@' + senderId : name;
port.postMessage = sinon.spy(port, 'postMessage');
port.disconnect = sinon.spy(port, 'disconnect');
return port;
}
function promiseResolveSynchronous(value) {
return {
then: function(callback) {
callback(value);
}
};
}
module('It2MeService', {
setup: function() {
// App Launcher.
appLauncher = {
launch: function () {
return promiseResolveSynchronous('tabId');
},
close: function () {}
};
// HangoutPort.
hangoutPort = createPort('it2me.helper.hangout');
it2meService = new remoting.It2MeService(appLauncher);
it2meService.onConnectExternal_(hangoutPort);
webappPort = createPort('it2me.helper.webapp', 'tabId');
}
});
test('should establish a channel two way channel when the webapp connects',
function() {
// Hangout ---- connect ----> It2MeService.
hangoutPort.onMessage.mock$fire({
method: 'connect',
accessCode: "123412341234"
});
// Webapp ---- connect ----> It2MeService.
it2meService.onWebappConnect_(webappPort);
// Webapp ---- sessionStateChanged ----> It2MeService.
webappPort.onMessage.mock$fire({
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CONNECTED
});
// verify that hangout can receive message events.
sinon.assert.calledWith(hangoutPort.postMessage, {
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CONNECTED
});
hangoutPort.onDisconnect.mock$fire();
QUnit.equal(it2meService.helpers_.length, 0);
});
test('should handle multiple helper connections', function() {
// Hangout ---- connect ----> It2MeService.
hangoutPort.onMessage.mock$fire({
method: 'connect',
accessCode: "123412341234"
});
// Hangout2 ---- connect ----> It2MeService.
var hangoutPort2 = createPort('it2me.helper.hangout');
it2meService.onConnectExternal_(hangoutPort2);
appLauncher.launch = function () {
return promiseResolveSynchronous('tabId2');
};
hangoutPort2.onMessage.mock$fire({
method: 'connect',
accessCode: "123412341234"
});
it2meService.onWebappConnect_(webappPort);
var webappPort2 = createPort('it2me.helper.webapp', 'tabId2');
it2meService.onWebappConnect_(webappPort2);
webappPort.onMessage.mock$fire({
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CONNECTED
});
// verify that hangout can receive message events from webapp 1
sinon.assert.calledWith(hangoutPort.postMessage, {
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CONNECTED
});
webappPort2.onMessage.mock$fire({
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CLOSED
});
// verify that hangout can receive message events from webapp 2.
sinon.assert.calledWith(hangoutPort2.postMessage, {
method: 'sessionStateChanged',
state: remoting.ClientSession.State.CLOSED
});
});
test('should reject unknown connection', function() {
it2meService.onWebappConnect_(webappPort);
sinon.assert.called(webappPort.disconnect);
var randomPort = createPort('unsupported.port.name');
it2meService.onConnectExternal_(randomPort);
sinon.assert.called(randomPort.disconnect);
});
test('messageExternal("hello") should return supportedFeatures', function() {
var response = null;
function callback(msg) {
response = msg;
}
it2meService.onMessageExternal_({
method: 'hello'
}, null, callback);
QUnit.ok(response.supportedFeatures instanceof Array);
});
})();
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment