From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- .../PresentationDeviceInfoChromeScript.js | 150 +++ .../mochitest/PresentationSessionChromeScript.js | 470 +++++++ .../PresentationSessionChromeScript1UA.js | 366 ++++++ .../mochitest/PresentationSessionFrameScript.js | 258 ++++ dom/presentation/tests/mochitest/chrome.ini | 14 + .../mochitest/file_presentation_1ua_receiver.html | 220 ++++ .../mochitest/file_presentation_1ua_wentaway.html | 95 ++ .../file_presentation_mixed_security_contexts.html | 159 +++ .../mochitest/file_presentation_non_receiver.html | 41 + ...ile_presentation_non_receiver_inner_iframe.html | 26 + .../mochitest/file_presentation_receiver.html | 140 +++ ...presentation_receiver_auxiliary_navigation.html | 60 + ...tation_receiver_establish_connection_error.html | 79 ++ .../file_presentation_receiver_inner_iframe.html | 26 + .../mochitest/file_presentation_reconnect.html | 102 ++ .../file_presentation_sandboxed_presentation.html | 114 ++ .../mochitest/file_presentation_terminate.html | 104 ++ ...ation_terminate_establish_connection_error.html | 114 ++ .../file_presentation_unknown_content_type.test | 1 + ...presentation_unknown_content_type.test^headers^ | 1 + dom/presentation/tests/mochitest/mochitest.ini | 77 ++ .../test_presentation_1ua_connection_wentaway.js | 175 +++ ...resentation_1ua_connection_wentaway_inproc.html | 18 + ...t_presentation_1ua_connection_wentaway_oop.html | 18 + .../test_presentation_1ua_sender_and_receiver.js | 370 ++++++ ...resentation_1ua_sender_and_receiver_inproc.html | 18 + ...t_presentation_1ua_sender_and_receiver_oop.html | 18 + .../mochitest/test_presentation_availability.html | 236 ++++ ..._presentation_datachannel_sessiontransport.html | 245 ++++ .../mochitest/test_presentation_dc_receiver.html | 141 +++ .../test_presentation_dc_receiver_oop.html | 213 ++++ .../mochitest/test_presentation_dc_sender.html | 291 +++++ .../mochitest/test_presentation_device_info.html | 144 +++ .../test_presentation_device_info_permission.html | 35 + .../test_presentation_mixed_security_contexts.html | 81 ++ ...t_presentation_receiver_auxiliary_navigation.js | 77 ++ ...ation_receiver_auxiliary_navigation_inproc.html | 18 + ...entation_receiver_auxiliary_navigation_oop.html | 18 + .../mochitest/test_presentation_reconnect.html | 379 ++++++ .../test_presentation_sandboxed_presentation.html | 75 ++ ...t_presentation_sender_on_terminate_request.html | 187 +++ .../test_presentation_sender_startWithDevice.html | 173 +++ .../mochitest/test_presentation_tcp_receiver.html | 137 ++ ...on_tcp_receiver_establish_connection_error.html | 110 ++ ..._tcp_receiver_establish_connection_timeout.html | 81 ++ ...er_establish_connection_unknown_content_type.js | 88 ++ ...ish_connection_unknown_content_type_inproc.html | 16 + ...ablish_connection_unknown_content_type_oop.html | 16 + .../test_presentation_tcp_receiver_oop.html | 178 +++ .../mochitest/test_presentation_tcp_sender.html | 260 ++++ ...st_presentation_tcp_sender_default_request.html | 151 +++ .../test_presentation_tcp_sender_disconnect.html | 160 +++ ...tion_tcp_sender_establish_connection_error.html | 514 ++++++++ .../tests/mochitest/test_presentation_terminate.js | 243 ++++ ...ntation_terminate_establish_connection_error.js | 197 +++ ...erminate_establish_connection_error_inproc.html | 18 + ...n_terminate_establish_connection_error_oop.html | 18 + .../test_presentation_terminate_inproc.html | 18 + .../mochitest/test_presentation_terminate_oop.html | 18 + .../xpcshell/test_multicast_dns_device_provider.js | 1318 ++++++++++++++++++++ .../xpcshell/test_presentation_device_manager.js | 244 ++++ .../test_presentation_session_transport.js | 198 +++ .../xpcshell/test_presentation_state_machine.js | 236 ++++ .../tests/xpcshell/test_tcp_control_channel.js | 398 ++++++ dom/presentation/tests/xpcshell/xpcshell.ini | 9 + 65 files changed, 10173 insertions(+) create mode 100644 dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js create mode 100644 dom/presentation/tests/mochitest/PresentationSessionChromeScript.js create mode 100644 dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js create mode 100644 dom/presentation/tests/mochitest/PresentationSessionFrameScript.js create mode 100644 dom/presentation/tests/mochitest/chrome.ini create mode 100644 dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_non_receiver.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_receiver.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_reconnect.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_terminate.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html create mode 100644 dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test create mode 100644 dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^ create mode 100644 dom/presentation/tests/mochitest/mochitest.ini create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_availability.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_dc_receiver.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_dc_sender.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_device_info.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_device_info_permission.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_reconnect.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_sender.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html create mode 100644 dom/presentation/tests/mochitest/test_presentation_terminate_oop.html create mode 100644 dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js create mode 100644 dom/presentation/tests/xpcshell/test_presentation_device_manager.js create mode 100644 dom/presentation/tests/xpcshell/test_presentation_session_transport.js create mode 100644 dom/presentation/tests/xpcshell/test_presentation_state_machine.js create mode 100644 dom/presentation/tests/xpcshell/test_tcp_control_channel.js create mode 100644 dom/presentation/tests/xpcshell/xpcshell.ini (limited to 'dom/presentation/tests') diff --git a/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js new file mode 100644 index 000000000..2bc069f6b --- /dev/null +++ b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +'use strict'; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import('resource://gre/modules/PresentationDeviceInfoManager.jsm'); + +const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +const manager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + +var testProvider = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]), + forceDiscovery: function() { + sendAsyncMessage('force-discovery'); + }, + listener: null, +}; + +var testDevice = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + establishControlChannel: function() { + return null; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + return true; + }, + id: null, + name: null, + type: null, + listener: null, +}; + +var testDevice1 = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'dummyid', + name: 'dummyName', + type: 'dummyType', + establishControlChannel: function(url, presentationId) { + return null; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + return true; + }, +}; + +var testDevice2 = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'dummyid', + name: 'dummyName', + type: 'dummyType', + establishControlChannel: function(url, presentationId) { + return null; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + return true; + }, +}; + +var mockedDeviceWithoutSupportedURL = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'dummyid', + name: 'dummyName', + type: 'dummyType', + establishControlChannel: function(url, presentationId) { + return null; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + return false; + }, +}; + +var mockedDeviceSupportHttpsURL = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'dummyid', + name: 'dummyName', + type: 'dummyType', + establishControlChannel: function(url, presentationId) { + return null; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + if (requestedUrl.indexOf("https://") != -1) { + return true; + } + return false; + }, +}; + +addMessageListener('setup', function() { + manager.addDeviceProvider(testProvider); + + sendAsyncMessage('setup-complete'); +}); + +addMessageListener('trigger-device-add', function(device) { + testDevice.id = device.id; + testDevice.name = device.name; + testDevice.type = device.type; + manager.addDevice(testDevice); +}); + +addMessageListener('trigger-add-unsupport-url-device', function() { + manager.addDevice(mockedDeviceWithoutSupportedURL); +}); + +addMessageListener('trigger-add-multiple-devices', function() { + manager.addDevice(testDevice1); + manager.addDevice(testDevice2); +}); + +addMessageListener('trigger-add-https-devices', function() { + manager.addDevice(mockedDeviceSupportHttpsURL); +}); + + +addMessageListener('trigger-device-update', function(device) { + testDevice.id = device.id; + testDevice.name = device.name; + testDevice.type = device.type; + manager.updateDevice(testDevice); +}); + +addMessageListener('trigger-device-remove', function() { + manager.removeDevice(testDevice); +}); + +addMessageListener('trigger-remove-unsupported-device', function() { + manager.removeDevice(mockedDeviceWithoutSupportedURL); +}); + +addMessageListener('trigger-remove-multiple-devices', function() { + manager.removeDevice(testDevice1); + manager.removeDevice(testDevice2); +}); + +addMessageListener('trigger-remove-https-devices', function() { + manager.removeDevice(mockedDeviceSupportHttpsURL); +}); + +addMessageListener('teardown', function() { + manager.removeDeviceProvider(testProvider); +}); diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js new file mode 100644 index 000000000..3052bdcb1 --- /dev/null +++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js @@ -0,0 +1,470 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +'use strict'; + +const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/Timer.jsm'); + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + +function registerMockedFactory(contractId, mockedClassId, mockedFactory) { + var originalClassId, originalFactory; + + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (!registrar.isCIDRegistered(mockedClassId)) { + try { + originalClassId = registrar.contractIDToCID(contractId); + originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory); + } catch (ex) { + originalClassId = ""; + originalFactory = null; + } + if (originalFactory) { + registrar.unregisterFactory(originalClassId, originalFactory); + } + registrar.registerFactory(mockedClassId, "", contractId, mockedFactory); + } + + return { contractId: contractId, + mockedClassId: mockedClassId, + mockedFactory: mockedFactory, + originalClassId: originalClassId, + originalFactory: originalFactory }; +} + +function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) { + if (originalFactory) { + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(mockedClassId, mockedFactory); + registrar.registerFactory(originalClassId, "", contractId, originalFactory); + } +} + +var sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString(); + +const address = Cc["@mozilla.org/supports-cstring;1"] + .createInstance(Ci.nsISupportsCString); +address.data = "127.0.0.1"; +const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); +addresses.appendElement(address, false); + +const mockedChannelDescription = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]), + get type() { + if (Services.prefs.getBoolPref("dom.presentation.session_transport.data_channel.enable")) { + return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL; + } + return Ci.nsIPresentationChannelDescription.TYPE_TCP; + }, + tcpAddress: addresses, + tcpPort: 1234, +}; + +const mockedServerSocket = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIServerSocket, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + get port() { + return this._port; + }, + set listener(listener) { + this._listener = listener; + }, + init: function(port, loopbackOnly, backLog) { + if (port != -1) { + this._port = port; + } else { + this._port = 5678; + } + }, + asyncListen: function(listener) { + this._listener = listener; + }, + close: function() { + this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED); + }, + simulateOnSocketAccepted: function(serverSocket, socketTransport) { + this._listener.onSocketAccepted(serverSocket, socketTransport); + } +}; + +const mockedSocketTransport = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISocketTransport]), +}; + +const mockedControlChannel = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), + set listener(listener) { + this._listener = listener; + }, + get listener() { + return this._listener; + }, + sendOffer: function(offer) { + sendAsyncMessage('offer-sent', this._isValidSDP(offer)); + }, + sendAnswer: function(answer) { + sendAsyncMessage('answer-sent', this._isValidSDP(answer)); + + if (answer.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) { + this._listener.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady(); + } + }, + _isValidSDP: function(aSDP) { + var isValid = false; + if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) { + try { + var addresses = aSDP.tcpAddress; + if (addresses.length > 0) { + for (var i = 0; i < addresses.length; i++) { + // Ensure CString addresses are used. Otherwise, an error will be thrown. + addresses.queryElementAt(i, Ci.nsISupportsCString); + } + + isValid = true; + } + } catch (e) { + isValid = false; + } + } else if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL) { + isValid = (aSDP.dataChannelSDP == "test-sdp"); + } + return isValid; + }, + launch: function(presentationId, url) { + sessionId = presentationId; + }, + terminate: function(presentationId) { + sendAsyncMessage('sender-terminate', presentationId); + }, + reconnect: function(presentationId, url) { + sendAsyncMessage('start-reconnect', url); + }, + notifyReconnected: function() { + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyReconnected(); + }, + disconnect: function(reason) { + sendAsyncMessage('control-channel-closed', reason); + this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyDisconnected(reason); + }, + simulateReceiverReady: function() { + this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady(); + }, + simulateOnOffer: function() { + sendAsyncMessage('offer-received'); + this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onOffer(mockedChannelDescription); + }, + simulateOnAnswer: function() { + sendAsyncMessage('answer-received'); + this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onAnswer(mockedChannelDescription); + }, + simulateNotifyConnected: function() { + sendAsyncMessage('control-channel-opened'); + this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyConnected(); + }, +}; + +const mockedDevice = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'id', + name: 'name', + type: 'type', + establishControlChannel: function(url, presentationId) { + sendAsyncMessage('control-channel-established'); + return mockedControlChannel; + }, + disconnect: function() {}, + isRequestedUrlSupported: function(requestedUrl) { + return true; + }, +}; + +const mockedDevicePrompt = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + set request(request) { + this._request = request; + }, + get request() { + return this._request; + }, + promptDeviceSelection: function(request) { + this._request = request; + sendAsyncMessage('device-prompt'); + }, + simulateSelect: function() { + this._request.select(mockedDevice); + }, + simulateCancel: function(result) { + this._request.cancel(result); + } +}; + +const mockedSessionTransport = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport, + Ci.nsIPresentationSessionTransportBuilder, + Ci.nsIPresentationTCPSessionTransportBuilder, + Ci.nsIPresentationDataChannelSessionTransportBuilder, + Ci.nsIPresentationControlChannelListener, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + set callback(callback) { + this._callback = callback; + }, + get callback() { + return this._callback; + }, + get selfAddress() { + return this._selfAddress; + }, + buildTCPSenderTransport: function(transport, listener) { + this._listener = listener; + this._role = Ci.nsIPresentationService.ROLE_CONTROLLER; + this._listener.onSessionTransport(this); + this._listener = null; + sendAsyncMessage('data-transport-initialized'); + + setTimeout(()=>{ + this.simulateTransportReady(); + }, 0); + }, + buildTCPReceiverTransport: function(description, listener) { + this._listener = listener; + this._role = Ci.nsIPresentationService.ROLE_RECEIVER; + + var addresses = description.QueryInterface(Ci.nsIPresentationChannelDescription).tcpAddress; + this._selfAddress = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetAddr]), + address: (addresses.length > 0) ? + addresses.queryElementAt(0, Ci.nsISupportsCString).data : "", + port: description.QueryInterface(Ci.nsIPresentationChannelDescription).tcpPort, + }; + + setTimeout(()=>{ + this._listener.onSessionTransport(this); + this._listener = null; + }, 0); + }, + // in-process case + buildDataChannelTransport: function(role, window, listener) { + this._listener = listener; + this._role = role; + + var hasNavigator = window ? (typeof window.navigator != "undefined") : false; + sendAsyncMessage('check-navigator', hasNavigator); + + setTimeout(()=>{ + this._listener.onSessionTransport(this); + this._listener = null; + this.simulateTransportReady(); + }, 0); + }, + enableDataNotification: function() { + sendAsyncMessage('data-transport-notification-enabled'); + }, + send: function(data) { + sendAsyncMessage('message-sent', data); + }, + close: function(reason) { + sendAsyncMessage('data-transport-closed', reason); + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason); + }, + simulateTransportReady: function() { + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady(); + }, + simulateIncomingMessage: function(message) { + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyData(message, false); + }, + onOffer: function(aOffer) { + }, + onAnswer: function(aAnswer) { + } +}; + +const mockedNetworkInfo = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo]), + getAddresses: function(ips, prefixLengths) { + ips.value = ["127.0.0.1"]; + prefixLengths.value = [0]; + return 1; + }, +}; + +const mockedNetworkManager = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkManager, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + get activeNetworkInfo() { + return mockedNetworkInfo; + }, +}; + +var requestPromise = null; + +const mockedRequestUIGlue = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + sendRequest: function(aUrl, aSessionId) { + sendAsyncMessage('receiver-launching', aSessionId); + return requestPromise; + }, +}; + +// Register mocked factories. +const originalFactoryData = []; +originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation-device/prompt;1", + uuidGenerator.generateUUID(), + mockedDevicePrompt)); +originalFactoryData.push(registerMockedFactory("@mozilla.org/network/server-socket;1", + uuidGenerator.generateUUID(), + mockedServerSocket)); +originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation/presentationtcpsessiontransport;1", + uuidGenerator.generateUUID(), + mockedSessionTransport)); +originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation/datachanneltransportbuilder;1", + uuidGenerator.generateUUID(), + mockedSessionTransport)); +originalFactoryData.push(registerMockedFactory("@mozilla.org/network/manager;1", + uuidGenerator.generateUUID(), + mockedNetworkManager)); +originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation/requestuiglue;1", + uuidGenerator.generateUUID(), + mockedRequestUIGlue)); + +function tearDown() { + requestPromise = null; + mockedServerSocket.listener = null; + mockedControlChannel.listener = null; + mockedDevice.listener = null; + mockedDevicePrompt.request = null; + mockedSessionTransport.callback = null; + + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener).removeDevice(mockedDevice); + + // Register original factories. + for (var data of originalFactoryData) { + registerOriginalFactory(data.contractId, data.mockedClassId, + data.mockedFactory, data.originalClassId, + data.originalFactory); + } + + sendAsyncMessage('teardown-complete'); +} + +addMessageListener('trigger-device-add', function() { + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener).addDevice(mockedDevice); +}); + +addMessageListener('trigger-device-prompt-select', function() { + mockedDevicePrompt.simulateSelect(); +}); + +addMessageListener('trigger-device-prompt-cancel', function(result) { + mockedDevicePrompt.simulateCancel(result); +}); + +addMessageListener('trigger-incoming-session-request', function(url) { + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onSessionRequest(mockedDevice, url, sessionId, mockedControlChannel); +}); + +addMessageListener('trigger-incoming-terminate-request', function() { + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onTerminateRequest(mockedDevice, sessionId, mockedControlChannel, true); +}); + +addMessageListener('trigger-reconnected-acked', function(url) { + mockedControlChannel.notifyReconnected(); +}); + +addMessageListener('trigger-incoming-offer', function() { + mockedControlChannel.simulateOnOffer(); +}); + +addMessageListener('trigger-incoming-answer', function() { + mockedControlChannel.simulateOnAnswer(); +}); + +addMessageListener('trigger-incoming-transport', function() { + mockedServerSocket.simulateOnSocketAccepted(mockedServerSocket, mockedSocketTransport); +}); + +addMessageListener('trigger-control-channel-open', function(reason) { + mockedControlChannel.simulateNotifyConnected(); +}); + +addMessageListener('trigger-control-channel-close', function(reason) { + mockedControlChannel.disconnect(reason); +}); + +addMessageListener('trigger-data-transport-close', function(reason) { + mockedSessionTransport.close(reason); +}); + +addMessageListener('trigger-incoming-message', function(message) { + mockedSessionTransport.simulateIncomingMessage(message); +}); + +addMessageListener('teardown', function() { + tearDown(); +}); + +var controlChannelListener; +addMessageListener('save-control-channel-listener', function() { + controlChannelListener = mockedControlChannel.listener; +}); + +addMessageListener('restore-control-channel-listener', function(message) { + mockedControlChannel.listener = controlChannelListener; + controlChannelListener = null; +}); + +var obs = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); +obs.addObserver(function observer(aSubject, aTopic, aData) { + obs.removeObserver(observer, aTopic); + + requestPromise = aSubject; +}, 'setup-request-promise', false); diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js new file mode 100644 index 000000000..82d7362b2 --- /dev/null +++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js @@ -0,0 +1,366 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict'; + +const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + +function debug(str) { + // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n'); +} + +const originalFactoryData = []; +var sessionId; // Store the uuid generated by PresentationRequest. +var triggerControlChannelError = false; // For simulating error during control channel establishment. + +// control channel of sender +const mockControlChannelOfSender = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), + set listener(listener) { + // PresentationControllingInfo::SetControlChannel + if (listener) { + debug('set listener for mockControlChannelOfSender without null'); + } else { + debug('set listener for mockControlChannelOfSender with null'); + } + this._listener = listener; + }, + get listener() { + return this._listener; + }, + notifyConnected: function() { + // send offer after notifyConnected immediately + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyConnected(); + }, + notifyReconnected: function() { + // send offer after notifyOpened immediately + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyReconnected(); + }, + sendOffer: function(offer) { + Services.tm.mainThread.dispatch(() => { + mockControlChannelOfReceiver.onOffer(offer); + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + onAnswer: function(answer) { + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .onAnswer(answer); + }, + launch: function(presentationId, url) { + sessionId = presentationId; + sendAsyncMessage('sender-launch', url); + }, + disconnect: function(reason) { + if (!this._listener) { + return; + } + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyDisconnected(reason); + mockControlChannelOfReceiver.disconnect(); + }, + terminate: function(presentationId) { + sendAsyncMessage('sender-terminate'); + }, + reconnect: function(presentationId, url) { + sendAsyncMessage('start-reconnect', url); + }, + sendIceCandidate: function(candidate) { + mockControlChannelOfReceiver.notifyIceCandidate(candidate); + }, + notifyIceCandidate: function(candidate) { + if (!this._listener) { + return; + } + + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .onIceCandidate(candidate); + }, +}; + +// control channel of receiver +const mockControlChannelOfReceiver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), + set listener(listener) { + // PresentationPresentingInfo::SetControlChannel + if (listener) { + debug('set listener for mockControlChannelOfReceiver without null'); + } else { + debug('set listener for mockControlChannelOfReceiver with null'); + } + this._listener = listener; + + if (this._pendingOpened) { + this._pendingOpened = false; + this.notifyConnected(); + } + }, + get listener() { + return this._listener; + }, + notifyConnected: function() { + // do nothing + if (!this._listener) { + this._pendingOpened = true; + return; + } + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyConnected(); + }, + onOffer: function(offer) { + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .onOffer(offer); + }, + sendAnswer: function(answer) { + Services.tm.mainThread.dispatch(() => { + mockControlChannelOfSender.onAnswer(answer); + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + disconnect: function(reason) { + if (!this._listener) { + return; + } + + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .notifyDisconnected(reason); + sendAsyncMessage('control-channel-receiver-closed', reason); + }, + terminate: function(presentaionId) { + }, + sendIceCandidate: function(candidate) { + mockControlChannelOfReceiver.notifyIceCandidate(candidate); + }, + notifyIceCandidate: function(candidate) { + if (!this._listener) { + return; + } + + this._listener + .QueryInterface(Ci.nsIPresentationControlChannelListener) + .onIceCandidate(candidate); + }, +}; + +const mockDevice = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + id: 'id', + name: 'name', + type: 'type', + establishControlChannel: function(url, presentationId) { + if (triggerControlChannelError) { + throw Cr.NS_ERROR_FAILURE; + } + sendAsyncMessage('control-channel-established'); + return mockControlChannelOfSender; + }, + disconnect: function() { + sendAsyncMessage('device-disconnected'); + }, + isRequestedUrlSupported: function(requestedUrl) { + return true; + }, +}; + +const mockDevicePrompt = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt, + Ci.nsIFactory]), + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + set request(request) { + this._request = request; + }, + get request() { + return this._request; + }, + promptDeviceSelection: function(request) { + this._request = request; + sendAsyncMessage('device-prompt'); + }, + simulateSelect: function() { + this._request.select(mockDevice); + }, + simulateCancel: function() { + this._request.cancel(); + } +}; + +const mockRequestUIGlue = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue, + Ci.nsIFactory]), + set promise(aPromise) { + this._promise = aPromise + }, + get promise() { + return this._promise; + }, + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + sendRequest: function(aUrl, aSessionId) { + return this.promise; + }, +}; + +function initMockAndListener() { + + function registerMockFactory(contractId, mockClassId, mockFactory) { + var originalClassId, originalFactory; + + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (!registrar.isCIDRegistered(mockClassId)) { + try { + originalClassId = registrar.contractIDToCID(contractId); + originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory); + } catch (ex) { + originalClassId = ""; + originalFactory = null; + } + if (originalFactory) { + registrar.unregisterFactory(originalClassId, originalFactory); + } + registrar.registerFactory(mockClassId, "", contractId, mockFactory); + } + + return { contractId: contractId, + mockClassId: mockClassId, + mockFactory: mockFactory, + originalClassId: originalClassId, + originalFactory: originalFactory }; + } + // Register mock factories. + const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + originalFactoryData.push(registerMockFactory("@mozilla.org/presentation-device/prompt;1", + uuidGenerator.generateUUID(), + mockDevicePrompt)); + originalFactoryData.push(registerMockFactory("@mozilla.org/presentation/requestuiglue;1", + uuidGenerator.generateUUID(), + mockRequestUIGlue)); + + addMessageListener('trigger-device-add', function() { + debug('Got message: trigger-device-add'); + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .addDevice(mockDevice); + }); + + addMessageListener('trigger-device-prompt-select', function() { + debug('Got message: trigger-device-prompt-select'); + mockDevicePrompt.simulateSelect(); + }); + + addMessageListener('trigger-on-session-request', function(url) { + debug('Got message: trigger-on-session-request'); + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onSessionRequest(mockDevice, + url, + sessionId, + mockControlChannelOfReceiver); + }); + + addMessageListener('trigger-on-terminate-request', function() { + debug('Got message: trigger-on-terminate-request'); + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onTerminateRequest(mockDevice, + sessionId, + mockControlChannelOfReceiver, + false); + }); + + addMessageListener('trigger-control-channel-open', function(reason) { + debug('Got message: trigger-control-channel-open'); + mockControlChannelOfSender.notifyConnected(); + mockControlChannelOfReceiver.notifyConnected(); + }); + + addMessageListener('trigger-control-channel-error', function(reason) { + debug('Got message: trigger-control-channel-open'); + triggerControlChannelError = true; + }); + + addMessageListener('trigger-reconnected-acked', function(url) { + debug('Got message: trigger-reconnected-acked'); + mockControlChannelOfSender.notifyReconnected(); + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onReconnectRequest(mockDevice, + url, + sessionId, + mockControlChannelOfReceiver); + }); + + // Used to call sendAsyncMessage in chrome script from receiver. + addMessageListener('forward-command', function(command_data) { + let command = JSON.parse(command_data); + sendAsyncMessage(command.name, command.data); + }); + + addMessageListener('teardown', teardown); + + var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + obs.addObserver(function setupRequestPromiseHandler(aSubject, aTopic, aData) { + debug('Got observer: setup-request-promise'); + obs.removeObserver(setupRequestPromiseHandler, aTopic); + mockRequestUIGlue.promise = aSubject; + sendAsyncMessage('promise-setup-ready'); + }, 'setup-request-promise', false); +} + +function teardown() { + + function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) { + if (originalFactory) { + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(mockedClassId, mockedFactory); + registrar.registerFactory(originalClassId, "", contractId, originalFactory); + } + } + + mockRequestUIGlue.promise = null; + mockControlChannelOfSender.listener = null; + mockControlChannelOfReceiver.listener = null; + mockDevicePrompt.request = null; + + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .removeDevice(mockDevice); + // Register original factories. + for (var data of originalFactoryData) { + registerOriginalFactory(data.contractId, data.mockClassId, + data.mockFactory, data.originalClassId, + data.originalFactory); + } + sendAsyncMessage('teardown-complete'); +} + +initMockAndListener(); diff --git a/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js new file mode 100644 index 000000000..77240ab5f --- /dev/null +++ b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js @@ -0,0 +1,258 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function loadPrivilegedScriptTest() { + /** + * The script is loaded as + * (a) a privileged script in content process for dc_sender.html + * (b) a frame script in the remote iframe process for dc_receiver_oop.html + * |type port == "undefined"| indicates the script is load by + * |loadPrivilegedScript| which is the first case. + */ + function sendMessage(type, data) { + if (typeof port == "undefined") { + sendAsyncMessage(type, {'data': data}); + } else { + port.postMessage({'type': type, + 'data': data + }); + } + } + + if (typeof port != "undefined") { + /** + * When the script is loaded by |loadPrivilegedScript|, these APIs + * are exposed to this script. + */ + port.onmessage = (e) => { + var type = e.data['type']; + if (!handlers.hasOwnProperty(type)) { + return; + } + var args = [e]; + handlers[type].forEach(handler => handler.apply(null, args)); + }; + var handlers = {}; + addMessageListener = function(message, handler) { + if (handlers.hasOwnProperty(message)) { + handlers[message].push(handler); + } else { + handlers[message] = [handler]; + } + }; + removeMessageListener = function(message, handler) { + if (!handler || !handlers.hasOwnProperty(message)) { + return; + } + var index = handlers[message].indexOf(handler); + if (index != -1) { + handlers[message].splice(index, 1); + } + }; + } + + const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components; + + const mockedChannelDescription = { + QueryInterface : function (iid) { + const interfaces = [Ci.nsIPresentationChannelDescription]; + + if (!interfaces.some(v => iid.equals(v))) { + throw Cr.NS_ERROR_NO_INTERFACE; + } + return this; + }, + get type() { + if (Services.prefs.getBoolPref("dom.presentation.session_transport.data_channel.enable")) { + return Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL; + } + return Ci.nsIPresentationChannelDescription.TYPE_TCP; + }, + get dataChannelSDP() { + return "test-sdp"; + } + }; + + function setTimeout(callback, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback({ notify: callback }, + delay, + Ci.nsITimer.TYPE_ONE_SHOT); + return timer; + } + + const mockedSessionTransport = { + QueryInterface : function (iid) { + const interfaces = [Ci.nsIPresentationSessionTransport, + Ci.nsIPresentationDataChannelSessionTransportBuilder, + Ci.nsIFactory]; + + if (!interfaces.some(v => iid.equals(v))) { + throw Cr.NS_ERROR_NO_INTERFACE; + } + return this; + }, + createInstance: function(aOuter, aIID) { + if (aOuter) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(aIID); + }, + set callback(callback) { + this._callback = callback; + }, + get callback() { + return this._callback; + }, + /* OOP case */ + buildDataChannelTransport: function(role, window, listener) { + dump("PresentationSessionFrameScript: build data channel transport\n"); + this._listener = listener; + this._role = role; + + var hasNavigator = window ? (typeof window.navigator != "undefined") : false; + sendMessage('check-navigator', hasNavigator); + + if (this._role == Ci.nsIPresentationService.ROLE_CONTROLLER) { + this._listener.sendOffer(mockedChannelDescription); + } + }, + + enableDataNotification: function() { + sendMessage('data-transport-notification-enabled'); + }, + send: function(data) { + sendMessage('message-sent', data); + }, + close: function(reason) { + sendMessage('data-transport-closed', reason); + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason); + this._callback = null; + }, + simulateTransportReady: function() { + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady(); + }, + simulateIncomingMessage: function(message) { + this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyData(message, false); + }, + onOffer: function(aOffer) { + this._listener.sendAnswer(mockedChannelDescription); + this._onSessionTransport(); + }, + onAnswer: function(aAnswer) { + this._onSessionTransport(); + }, + _onSessionTransport: function() { + setTimeout(()=>{ + this._listener.onSessionTransport(this); + this.simulateTransportReady(); + this._listener = null; + }, 0); + } + }; + + + function tearDown() { + mockedSessionTransport.callback = null; + + /* Register original factories. */ + for (var data of originalFactoryData) { + registerOriginalFactory(data.contractId, data.mockedClassId, + data.mockedFactory, data.originalClassId, + data.originalFactory); + } + sendMessage("teardown-complete"); + } + + + function registerMockedFactory(contractId, mockedClassId, mockedFactory) { + var originalClassId, originalFactory; + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + + if (!registrar.isCIDRegistered(mockedClassId)) { + try { + originalClassId = registrar.contractIDToCID(contractId); + originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory); + } catch (ex) { + originalClassId = ""; + originalFactory = null; + } + if (originalFactory) { + registrar.unregisterFactory(originalClassId, originalFactory); + } + registrar.registerFactory(mockedClassId, "", contractId, mockedFactory); + } + + return { contractId: contractId, + mockedClassId: mockedClassId, + mockedFactory: mockedFactory, + originalClassId: originalClassId, + originalFactory: originalFactory }; + } + + function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) { + if (originalFactory) { + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(mockedClassId, mockedFactory); + registrar.registerFactory(originalClassId, "", contractId, originalFactory); + } + } + + /* Register mocked factories. */ + const originalFactoryData = []; + const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation/datachanneltransportbuilder;1", + uuidGenerator.generateUUID(), + mockedSessionTransport)); + + addMessageListener('trigger-incoming-message', function(event) { + mockedSessionTransport.simulateIncomingMessage(event.data.data); + }); + addMessageListener('teardown', ()=>tearDown()); +} + +// Exposed to the caller of |loadPrivilegedScript| +var contentScript = { + handlers: {}, + addMessageListener: function(message, handler) { + if (this.handlers.hasOwnProperty(message)) { + this.handlers[message].push(handler); + } else { + this.handlers[message] = [handler]; + } + }, + removeMessageListener: function(message, handler) { + if (!handler || !this.handlers.hasOwnProperty(message)) { + return; + } + var index = this.handlers[message].indexOf(handler); + if (index != -1) { + this.handlers[message].splice(index, 1); + } + }, + sendAsyncMessage: function(message, data) { + port.postMessage({'type': message, + 'data': data + }); + } +} + +if (!SpecialPowers.isMainProcess()) { + var port; + try { + port = SpecialPowers.loadPrivilegedScript(loadPrivilegedScriptTest.toSource()); + } catch (e) { + ok(false, "loadPrivilegedScript shoulde not throw" + e); + } + + port.onmessage = (e) => { + var type = e.data['type']; + if (!contentScript.handlers.hasOwnProperty(type)) { + return; + } + var args = [e.data['data']]; + contentScript.handlers[type].forEach(handler => handler.apply(null, args)); + }; +} diff --git a/dom/presentation/tests/mochitest/chrome.ini b/dom/presentation/tests/mochitest/chrome.ini new file mode 100644 index 000000000..83841f4f8 --- /dev/null +++ b/dom/presentation/tests/mochitest/chrome.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + PresentationDeviceInfoChromeScript.js + PresentationSessionChromeScript.js + +[test_presentation_datachannel_sessiontransport.html] +skip-if = os == 'android' +[test_presentation_device_info.html] +[test_presentation_sender_startWithDevice.html] +skip-if = toolkit == 'android' # Bug 1129785 +[test_presentation_tcp_sender.html] +skip-if = toolkit == 'android' # Bug 1129785 +[test_presentation_tcp_sender_default_request.html] +skip-if = toolkit == 'android' # Bug 1129785 diff --git a/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html new file mode 100644 index 000000000..cf02d2b2c --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html @@ -0,0 +1,220 @@ + + + + + + Test for B2G PresentationReceiver at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html b/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html new file mode 100644 index 000000000..370cb92e1 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html @@ -0,0 +1,95 @@ + + + + + + Test for B2G PresentationReceiver at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html new file mode 100644 index 000000000..f042d2994 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html @@ -0,0 +1,159 @@ + + + + + +Test allow-presentation sandboxing flag + + + + + diff --git a/dom/presentation/tests/mochitest/file_presentation_non_receiver.html b/dom/presentation/tests/mochitest/file_presentation_non_receiver.html new file mode 100644 index 000000000..1203523ac --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver.html @@ -0,0 +1,41 @@ + + + + + Test for B2G PresentationReceiver on a non-receiver page at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html b/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html new file mode 100644 index 000000000..c95eddf57 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html @@ -0,0 +1,26 @@ + + + + + Test for B2G PresentationReceiver on a non-receiver inner iframe of the receiver page at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver.html b/dom/presentation/tests/mochitest/file_presentation_receiver.html new file mode 100644 index 000000000..46a330b5f --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_receiver.html @@ -0,0 +1,140 @@ + + + + + Test for B2G PresentationReceiver at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html b/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html new file mode 100644 index 000000000..3a6060310 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html @@ -0,0 +1,60 @@ + + + + + Test for sandboxed auxiliary navigation flag in receiver page + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html b/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html new file mode 100644 index 000000000..6b1f2152f --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html @@ -0,0 +1,79 @@ + + + + + Test for connection establishing errors of B2G Presentation API at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html b/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html new file mode 100644 index 000000000..3bd5ac4b1 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html @@ -0,0 +1,26 @@ + + + + + Test for B2G PresentationReceiver in an inner iframe of the receiver page at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_reconnect.html b/dom/presentation/tests/mochitest/file_presentation_reconnect.html new file mode 100644 index 000000000..174ccd3f3 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_reconnect.html @@ -0,0 +1,102 @@ + + + + + +Test allow-presentation sandboxing flag + + + + + diff --git a/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html new file mode 100644 index 000000000..369621cee --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html @@ -0,0 +1,114 @@ + + + + + +Test allow-presentation sandboxing flag + + + + + diff --git a/dom/presentation/tests/mochitest/file_presentation_terminate.html b/dom/presentation/tests/mochitest/file_presentation_terminate.html new file mode 100644 index 000000000..a26a44b90 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html @@ -0,0 +1,104 @@ + + + + + + Test for B2G PresentationReceiver at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html new file mode 100644 index 000000000..d8df8a1a6 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html @@ -0,0 +1,114 @@ + + + + + + Test for B2G PresentationReceiver at receiver side + + +
+ + + diff --git a/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test @@ -0,0 +1 @@ + diff --git a/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^ new file mode 100644 index 000000000..fc044e3c4 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^ @@ -0,0 +1 @@ +Content-Type: application/unknown diff --git a/dom/presentation/tests/mochitest/mochitest.ini b/dom/presentation/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..f96e07f1e --- /dev/null +++ b/dom/presentation/tests/mochitest/mochitest.ini @@ -0,0 +1,77 @@ +[DEFAULT] +support-files = + PresentationDeviceInfoChromeScript.js + PresentationSessionChromeScript.js + PresentationSessionFrameScript.js + PresentationSessionChromeScript1UA.js + file_presentation_1ua_receiver.html + test_presentation_1ua_sender_and_receiver.js + file_presentation_non_receiver_inner_iframe.html + file_presentation_non_receiver.html + file_presentation_receiver.html + file_presentation_receiver_establish_connection_error.html + file_presentation_receiver_inner_iframe.html + file_presentation_1ua_wentaway.html + test_presentation_1ua_connection_wentaway.js + file_presentation_receiver_auxiliary_navigation.html + test_presentation_receiver_auxiliary_navigation.js + file_presentation_sandboxed_presentation.html + file_presentation_terminate.html + test_presentation_terminate.js + file_presentation_terminate_establish_connection_error.html + test_presentation_terminate_establish_connection_error.js + file_presentation_reconnect.html + file_presentation_unknown_content_type.test + file_presentation_unknown_content_type.test^headers^ + test_presentation_tcp_receiver_establish_connection_unknown_content_type.js + file_presentation_mixed_security_contexts.html + +[test_presentation_dc_sender.html] +[test_presentation_dc_receiver.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_dc_receiver_oop.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_1ua_sender_and_receiver_inproc.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_1ua_sender_and_receiver_oop.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_1ua_connection_wentaway_inproc.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_1ua_connection_wentaway_oop.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_device_info_permission.html] +[test_presentation_tcp_sender_disconnect.html] +skip-if = toolkit == 'android' # Bug 1129785 +[test_presentation_tcp_sender_establish_connection_error.html] +skip-if = toolkit == 'android' # Bug 1129785 +[test_presentation_tcp_receiver_establish_connection_error.html] +skip-if = (e10s || toolkit == 'android' || os == 'mac' || os == 'win') # Bug 1129785, Bug 1204709 +[test_presentation_tcp_receiver_establish_connection_timeout.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_tcp_receiver.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_tcp_receiver_oop.html] +skip-if = (e10s || toolkit == 'android') # Bug 1129785 +[test_presentation_receiver_auxiliary_navigation_inproc.html] +skip-if = e10s +[test_presentation_receiver_auxiliary_navigation_oop.html] +skip-if = e10s +[test_presentation_terminate_inproc.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_terminate_oop.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_terminate_establish_connection_error_inproc.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_terminate_establish_connection_error_oop.html] +skip-if = (e10s || toolkit == 'android') +[test_presentation_sender_on_terminate_request.html] +skip-if = toolkit == 'android' +[test_presentation_sandboxed_presentation.html] +skip-if = true # bug 1315867 +[test_presentation_reconnect.html] +[test_presentation_mixed_security_contexts.html] +[test_presentation_availability.html] diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js new file mode 100644 index 000000000..dbeb4ffcc --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js @@ -0,0 +1,175 @@ +'use strict'; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); + +function debug(str) { + // info(str); +} + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js')); +var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_wentaway.html'); +var request; +var connection; +var receiverIframe; + +function setup() { + gScript.addMessageListener('device-prompt', function devicePromptHandler() { + debug('Got message: device-prompt'); + gScript.removeMessageListener('device-prompt', devicePromptHandler); + gScript.sendAsyncMessage('trigger-device-prompt-select'); + }); + + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + gScript.sendAsyncMessage("trigger-control-channel-open"); + }); + + gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) { + debug('Got message: sender-launch'); + gScript.removeMessageListener('sender-launch', senderLaunchHandler); + is(url, receiverUrl, 'Receiver: should receive the same url'); + receiverIframe = document.createElement('iframe'); + receiverIframe.setAttribute("mozbrowser", "true"); + receiverIframe.setAttribute("mozpresentation", receiverUrl); + var oop = location.pathname.indexOf('_inproc') == -1; + receiverIframe.setAttribute("remote", oop); + + receiverIframe.setAttribute('src', receiverUrl); + receiverIframe.addEventListener("mozbrowserloadend", function mozbrowserloadendHander() { + receiverIframe.removeEventListener("mozbrowserloadend", mozbrowserloadendHander); + info("Receiver loaded."); + }); + + // This event is triggered when the iframe calls "alert". + receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) { + var message = evt.detail.message; + if (/^OK /.exec(message)) { + ok(true, message.replace(/^OK /, "")); + } else if (/^KO /.exec(message)) { + ok(false, message.replace(/^KO /, "")); + } else if (/^INFO /.exec(message)) { + info(message.replace(/^INFO /, "")); + } else if (/^COMMAND /.exec(message)) { + var command = JSON.parse(message.replace(/^COMMAND /, "")); + gScript.sendAsyncMessage(command.name, command.data); + } else if (/^DONE$/.exec(message)) { + receiverIframe.removeEventListener("mozbrowsershowmodalprompt", + receiverListener); + teardown(); + } + }, false); + + var promise = new Promise(function(aResolve, aReject) { + document.body.appendChild(receiverIframe); + aResolve(receiverIframe); + }); + + var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"] + .getService(SpecialPowers.Ci.nsIObserverService); + obs.notifyObservers(promise, 'setup-request-promise', null); + }); + + gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() { + debug('Got message: promise-setup-ready'); + gScript.removeMessageListener('promise-setup-ready', + promiseSetupReadyHandler); + gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl); + }); + + return Promise.resolve(); +} + +function testCreateRequest() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testCreateRequest ---'); + request = new PresentationRequest(receiverUrl); + request.getAvailability().then((aAvailability) => { + is(aAvailability.value, false, "Sender: should have no available device after setup"); + aAvailability.onchange = function() { + aAvailability.onchange = null; + ok(aAvailability.value, "Sender: Device should be available."); + aResolve(); + } + + gScript.sendAsyncMessage('trigger-device-add'); + }).catch((aError) => { + ok(false, "Sender: Error occurred when getting availability: " + aError); + teardown(); + aReject(); + }); + }); +} + +function testStartConnection() { + return new Promise(function(aResolve, aReject) { + request.start().then((aConnection) => { + connection = aConnection; + ok(connection, "Sender: Connection should be available."); + ok(connection.id, "Sender: Connection ID should be set."); + is(connection.state, "connecting", "Sender: The initial state should be connecting."); + connection.onconnect = function() { + connection.onconnect = null; + is(connection.state, "connected", "Connection should be connected."); + aResolve(); + }; + }).catch((aError) => { + ok(false, "Sender: Error occurred when establishing a connection: " + aError); + teardown(); + aReject(); + }); + }); +} + +function testConnectionWentaway() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testConnectionWentaway ---'); + connection.onclose = function() { + connection.onclose = null; + is(connection.state, "closed", "Sender: Connection should be closed."); + receiverIframe.addEventListener('mozbrowserclose', function closeHandler() { + ok(false, 'wentaway should not trigger receiver close'); + aResolve(); + }); + setTimeout(aResolve, 3000); + }; + gScript.addMessageListener('ready-to-remove-receiverFrame', function onReadyToRemove() { + gScript.removeMessageListener('ready-to-remove-receiverFrame', onReadyToRemove); + receiverIframe.src = "http://example.com"; + }); + }); +} + +function teardown() { + gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { + debug('Got message: teardown-complete'); + gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + + gScript.sendAsyncMessage('teardown'); +} + +function runTests() { + setup().then(testCreateRequest) + .then(testStartConnection) + .then(testConnectionWentaway) + .then(teardown); +} + +SpecialPowers.pushPermissions([ + {type: 'presentation-device-manage', allow: false, context: document}, + {type: "browser", allow: true, context: document}, +], () => { + SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true], + ["dom.presentation.controller.enabled", true], + ["dom.presentation.receiver.enabled", true], + ["dom.presentation.test.enabled", true], + ["dom.mozBrowserFramesEnabled", true], + ["dom.ipc.tabs.disabled", false], + ["network.disable.ipc.security", true], + ["dom.presentation.test.stage", 0]]}, + runTests); +}); diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html new file mode 100644 index 000000000..68491d81b --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html @@ -0,0 +1,18 @@ + + + + + + + Test for B2G Presentation API when sender and receiver at the same side + + + + + + Test for PresentationConnectionCloseEvent with wentaway reason + + + diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html new file mode 100644 index 000000000..68491d81b --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html @@ -0,0 +1,18 @@ + + + + + + + Test for B2G Presentation API when sender and receiver at the same side + + + + + + Test for PresentationConnectionCloseEvent with wentaway reason + + + diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js new file mode 100644 index 000000000..8a7787b40 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js @@ -0,0 +1,370 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, +* You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict'; + +function debug(str) { + // info(str); +} + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js')); +var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_receiver.html'); +var request; +var connection; +var receiverIframe; +var presentationId; +const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0]; +const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length); +const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER); +TYPED_DATA_ARRAY.set(DATA_ARRAY); + +function postMessageToIframe(aType) { + receiverIframe.src = receiverUrl + "#" + + encodeURIComponent(JSON.stringify({ type: aType })); +} + +function setup() { + + gScript.addMessageListener('device-prompt', function devicePromptHandler() { + debug('Got message: device-prompt'); + gScript.removeMessageListener('device-prompt', devicePromptHandler); + gScript.sendAsyncMessage('trigger-device-prompt-select'); + }); + + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + gScript.sendAsyncMessage("trigger-control-channel-open"); + }); + + gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) { + debug('Got message: sender-launch'); + gScript.removeMessageListener('sender-launch', senderLaunchHandler); + is(url, receiverUrl, 'Receiver: should receive the same url'); + receiverIframe = document.createElement('iframe'); + receiverIframe.setAttribute('src', receiverUrl); + receiverIframe.setAttribute("mozbrowser", "true"); + receiverIframe.setAttribute("mozpresentation", receiverUrl); + var oop = location.pathname.indexOf('_inproc') == -1; + receiverIframe.setAttribute("remote", oop); + + // This event is triggered when the iframe calls "alert". + receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(evt) { + var message = evt.detail.message; + debug('Got iframe message: ' + message); + if (/^OK /.exec(message)) { + ok(true, message.replace(/^OK /, "")); + } else if (/^KO /.exec(message)) { + ok(false, message.replace(/^KO /, "")); + } else if (/^INFO /.exec(message)) { + info(message.replace(/^INFO /, "")); + } else if (/^COMMAND /.exec(message)) { + var command = JSON.parse(message.replace(/^COMMAND /, "")); + gScript.sendAsyncMessage(command.name, command.data); + } else if (/^DONE$/.exec(message)) { + receiverIframe.removeEventListener("mozbrowsershowmodalprompt", + receiverListener); + } + }, false); + + var promise = new Promise(function(aResolve, aReject) { + document.body.appendChild(receiverIframe); + aResolve(receiverIframe); + }); + + var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"] + .getService(SpecialPowers.Ci.nsIObserverService); + obs.notifyObservers(promise, 'setup-request-promise', null); + }); + + gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() { + debug('Got message: promise-setup-ready'); + gScript.removeMessageListener('promise-setup-ready', promiseSetupReadyHandler); + gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl); + }); + + return Promise.resolve(); +} + +function testCreateRequest() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testCreateRequest ---'); + request = new PresentationRequest("file_presentation_1ua_receiver.html"); + request.getAvailability().then((aAvailability) => { + is(aAvailability.value, false, "Sender: should have no available device after setup"); + aAvailability.onchange = function() { + aAvailability.onchange = null; + ok(aAvailability.value, "Sender: Device should be available."); + aResolve(); + } + + gScript.sendAsyncMessage('trigger-device-add'); + }).catch((aError) => { + ok(false, "Sender: Error occurred when getting availability: " + aError); + teardown(); + aReject(); + }); + }); +} + +function testStartConnection() { + return new Promise(function(aResolve, aReject) { + request.start().then((aConnection) => { + connection = aConnection; + ok(connection, "Sender: Connection should be available."); + ok(connection.id, "Sender: Connection ID should be set."); + is(connection.state, "connecting", "The initial state should be connecting."); + is(connection.url, receiverUrl, "request URL should be expanded to absolute URL"); + connection.onconnect = function() { + connection.onconnect = null; + is(connection.state, "connected", "Connection should be connected."); + presentationId = connection.id; + aResolve(); + }; + }).catch((aError) => { + ok(false, "Sender: Error occurred when establishing a connection: " + aError); + teardown(); + aReject(); + }); + + let request2 = new PresentationRequest("/"); + request2.start().then(() => { + ok(false, "Sender: session start should fail while there is an unsettled promise."); + }).catch((aError) => { + is(aError.name, "OperationError", "Expect to get OperationError."); + }); + }); +} + +function testSendMessage() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testSendMessage ---'); + gScript.addMessageListener('trigger-message-from-sender', function triggerMessageFromSenderHandler() { + debug('Got message: trigger-message-from-sender'); + gScript.removeMessageListener('trigger-message-from-sender', triggerMessageFromSenderHandler); + info('Send message to receiver'); + connection.send('msg-sender-to-receiver'); + }); + + gScript.addMessageListener('message-from-sender-received', function messageFromSenderReceivedHandler() { + debug('Got message: message-from-sender-received'); + gScript.removeMessageListener('message-from-sender-received', messageFromSenderReceivedHandler); + aResolve(); + }); + }); +} + +function testIncomingMessage() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testIncomingMessage ---'); + connection.addEventListener('message', function messageHandler(evt) { + connection.removeEventListener('message', messageHandler); + let msg = evt.data; + is(msg, "msg-receiver-to-sender", "Sender: Sender should receive message from Receiver"); + postMessageToIframe('message-from-receiver-received'); + aResolve(); + }); + postMessageToIframe('trigger-message-from-receiver'); + }); +} + +function testSendBlobMessage() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testSendBlobMessage ---'); + connection.addEventListener('message', function messageHandler(evt) { + connection.removeEventListener('message', messageHandler); + let msg = evt.data; + is(msg, "testIncomingBlobMessage", "Sender: Sender should receive message from Receiver"); + let blob = new Blob(["Hello World"], {type : 'text/plain'}); + connection.send(blob); + aResolve(); + }); + }); +} + +function testSendArrayBuffer() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testSendArrayBuffer ---'); + connection.addEventListener('message', function messageHandler(evt) { + connection.removeEventListener('message', messageHandler); + let msg = evt.data; + is(msg, "testIncomingArrayBuffer", "Sender: Sender should receive message from Receiver"); + connection.send(DATA_ARRAY_BUFFER); + aResolve(); + }); + }); +} + +function testSendArrayBufferView() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testSendArrayBufferView ---'); + connection.addEventListener('message', function messageHandler(evt) { + connection.removeEventListener('message', messageHandler); + let msg = evt.data; + is(msg, "testIncomingArrayBufferView", "Sender: Sender should receive message from Receiver"); + connection.send(TYPED_DATA_ARRAY); + aResolve(); + }); + }); +} + +function testCloseConnection() { + info('Sender: --- testCloseConnection ---'); + // Test terminate immediate after close. + function controlChannelEstablishedHandler() + { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + ok(false, "terminate after close should do nothing"); + } + gScript.addMessageListener('ready-to-close', function onReadyToClose() { + gScript.removeMessageListener('ready-to-close', onReadyToClose); + connection.close(); + + gScript.addMessageListener('control-channel-established', controlChannelEstablishedHandler); + connection.terminate(); + }); + + return Promise.all([ + new Promise(function(aResolve, aReject) { + connection.onclose = function() { + connection.onclose = null; + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + aResolve(); + }; + }), + new Promise(function(aResolve, aReject) { + let timeout = setTimeout(function() { + gScript.removeMessageListener('device-disconnected', + deviceDisconnectedHandler); + ok(true, "terminate after close should not trigger device.disconnect"); + aResolve(); + }, 3000); + + function deviceDisconnectedHandler() { + gScript.removeMessageListener('device-disconnected', + deviceDisconnectedHandler); + ok(false, "terminate after close should not trigger device.disconnect"); + clearTimeout(timeout); + aResolve(); + } + + gScript.addMessageListener('device-disconnected', deviceDisconnectedHandler); + }), + new Promise(function(aResolve, aReject) { + gScript.addMessageListener('receiver-closed', function onReceiverClosed() { + gScript.removeMessageListener('receiver-closed', onReceiverClosed); + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + aResolve(); + }); + }), + ]); +} + +function testTerminateAfterClose() { + info('Sender: --- testTerminateAfterClose ---'); + return Promise.race([ + new Promise(function(aResolve, aReject) { + connection.onterminate = function() { + connection.onterminate = null; + ok(false, 'terminate after close should do nothing'); + aResolve(); + }; + connection.terminate(); + }), + new Promise(function(aResolve, aReject) { + setTimeout(function() { + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + aResolve(); + }, 3000); + }), + ]); +} + +function testReconnect() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testReconnect ---'); + gScript.addMessageListener('control-channel-established', function controlChannelEstablished() { + gScript.removeMessageListener('control-channel-established', controlChannelEstablished); + gScript.sendAsyncMessage("trigger-control-channel-open"); + }); + + gScript.addMessageListener('start-reconnect', function startReconnectHandler(url) { + debug('Got message: start-reconnect'); + gScript.removeMessageListener('start-reconnect', startReconnectHandler); + is(url, receiverUrl, "URLs should be the same.") + gScript.sendAsyncMessage('trigger-reconnected-acked', url); + }); + + gScript.addMessageListener('ready-to-reconnect', function onReadyToReconnect() { + gScript.removeMessageListener('ready-to-reconnect', onReadyToReconnect); + request.reconnect(presentationId).then((aConnection) => { + connection = aConnection; + ok(connection, "Sender: Connection should be available."); + is(connection.id, presentationId, "The presentationId should be the same."); + is(connection.state, "connecting", "The initial state should be connecting."); + connection.onconnect = function() { + connection.onconnect = null; + is(connection.state, "connected", "Connection should be connected."); + aResolve(); + }; + }).catch((aError) => { + ok(false, "Sender: Error occurred when establishing a connection: " + aError); + teardown(); + aReject(); + }); + }); + + postMessageToIframe('prepare-for-reconnect'); + }); +} + +function teardown() { + gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { + debug('Got message: teardown-complete'); + gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + + gScript.sendAsyncMessage('teardown'); +} + +function runTests() { + setup().then(testCreateRequest) + .then(testStartConnection) + .then(testSendMessage) + .then(testIncomingMessage) + .then(testSendBlobMessage) + .then(testCloseConnection) + .then(testReconnect) + .then(testSendArrayBuffer) + .then(testSendArrayBufferView) + .then(testCloseConnection) + .then(testTerminateAfterClose) + .then(teardown); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); +SpecialPowers.pushPermissions([ + {type: 'presentation-device-manage', allow: false, context: document}, + {type: "browser", allow: true, context: document}, +], () => { + SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true], + /* Mocked TCP session transport builder in the test */ + ["dom.presentation.session_transport.data_channel.enable", true], + ["dom.presentation.controller.enabled", true], + ["dom.presentation.receiver.enabled", true], + ["dom.presentation.test.enabled", true], + ["dom.presentation.test.stage", 0], + ["dom.mozBrowserFramesEnabled", true], + ["network.disable.ipc.security", true], + ["media.navigator.permission.disabled", true]]}, + runTests); +}); diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html new file mode 100644 index 000000000..520b1a98c --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html @@ -0,0 +1,18 @@ + + + + + + + Test for B2G Presentation API when sender and receiver at the same side + + + + + + Test for B2G Presentation API when sender and receiver at the same side + + + diff --git a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html new file mode 100644 index 000000000..e744e6802 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html @@ -0,0 +1,18 @@ + + + + + + + Test for B2G Presentation API when sender and receiver at the same side (OOP ver.) + + + + + + Test for B2G Presentation API when sender and receiver at the same side (OOP ver.) + + + diff --git a/dom/presentation/tests/mochitest/test_presentation_availability.html b/dom/presentation/tests/mochitest/test_presentation_availability.html new file mode 100644 index 000000000..89f1ad1b7 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_availability.html @@ -0,0 +1,236 @@ + + + + + + Test for PresentationAvailability + + + + +Test PresentationAvailability + + + diff --git a/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html new file mode 100644 index 000000000..89a51afb7 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html @@ -0,0 +1,245 @@ + + + + + + Test for data channel as session transport in Presentation API + + + + +Test for data channel as session transport in Presentation API + +
+
+
+ + diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html new file mode 100644 index 000000000..a42489bdb --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html @@ -0,0 +1,141 @@ + + + + + + Test for B2G PresentationConnection API at receiver side + + + + +Test for B2G PresentationConnection API at receiver side +

+ +

+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
new file mode 100644
index 000000000..b289b0be6
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
@@ -0,0 +1,213 @@
+
+
+
+
+  
+  Test for B2G PresentationConnection API at receiver side (OOP)
+  
+  
+  
+
+
+Test B2G PresentationConnection API at receiver side (OOP)
+

+ +

+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_dc_sender.html b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
new file mode 100644
index 000000000..97e252e84
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
@@ -0,0 +1,291 @@
+
+
+
+
+  
+  Test for B2G Presentation API at sender side
+  
+  
+  
+
+
+Test for B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_device_info.html b/dom/presentation/tests/mochitest/test_presentation_device_info.html
new file mode 100644
index 000000000..77253e41d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info.html
@@ -0,0 +1,144 @@
+
+
+
+
+  
+  Test for B2G Presentation Device Info API
+  
+  
+
+
+Test for B2G Presentation Device Info API
+
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html b/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html
new file mode 100644
index 000000000..c7f7ac96d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_device_info_permission.html
@@ -0,0 +1,35 @@
+
+
+
+
+  
+  Test for B2G Presentation Device Info API Permission
+  
+  
+
+
+Test for B2G Presentation Device Info API Permission
+
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
new file mode 100644
index 000000000..31918a2c4
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
@@ -0,0 +1,81 @@
+
+
+
+
+  
+  Test default request for B2G Presentation API at sender side
+  
+  
+
+
+Test allow-presentation sandboxing flag
+
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
new file mode 100644
index 000000000..0647bff3a
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
@@ -0,0 +1,77 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver_auxiliary_navigation.html");
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+          .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage("trigger-device-add");
+
+    var iframe = document.createElement("iframe");
+    iframe.setAttribute("mozbrowser", "true");
+    iframe.setAttribute("mozpresentation", receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    iframe.setAttribute("remote", oop);
+    iframe.setAttribute("src", receiverUrl);
+
+    // This event is triggered when the iframe calls "postMessage".
+    iframe.addEventListener("mozbrowsershowmodalprompt", function listener(aEvent) {
+      var message = aEvent.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, "Message from iframe: " + message);
+      } else if (/^KO /.exec(message)) {
+        ok(false, "Message from iframe: " + message);
+      } else if (/^INFO /.exec(message)) {
+        info("Message from iframe: " + message);
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ""));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, "Messaging from iframe complete.");
+        iframe.removeEventListener("mozbrowsershowmodalprompt", listener);
+
+        teardown();
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(iframe);
+
+      aResolve(iframe);
+    });
+    obs.notifyObservers(promise, "setup-request-promise", null);
+
+    aResolve();
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+    gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+  setup().then();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: "presentation-device-manage", allow: false, context: document},
+  {type: "browser", allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+                                      ["dom.presentation.controller.enabled", true],
+                                      ["dom.presentation.receiver.enabled", true],
+                                      ["dom.mozBrowserFramesEnabled", true],
+                                      ["network.disable.ipc.security", true],
+                                      ["dom.presentation.session_transport.data_channel.enable", false]]},
+                            runTests);
+});
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html
new file mode 100644
index 000000000..f873fa3da
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for B2G Presentation API when sender and receiver at the same side
+    
+    
+  
+  
+    
+      Test for receiver page with sandboxed auxiliary navigation browsing context flag.
+    
+  
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html
new file mode 100644
index 000000000..f873fa3da
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for B2G Presentation API when sender and receiver at the same side
+    
+    
+  
+  
+    
+      Test for receiver page with sandboxed auxiliary navigation browsing context flag.
+    
+  
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_reconnect.html b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
new file mode 100644
index 000000000..079b7f5c5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
@@ -0,0 +1,379 @@
+
+
+
+
+  
+  Test for B2G Presentation API at sender side
+  
+  
+  
+
+
+Test for Presentation API at sender side
+
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html b/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html
new file mode 100644
index 000000000..dc17209c5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html
@@ -0,0 +1,75 @@
+
+
+
+
+  
+  Test default request for B2G Presentation API at sender side
+  
+  
+
+
+Test allow-presentation sandboxing flag
+
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
new file mode 100644
index 000000000..d0c8af0ad
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
@@ -0,0 +1,187 @@
+
+
+
+
+  
+  Test onTerminateRequest at sender side
+  
+  
+
+
+Test onTerminateRequest at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html b/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html
new file mode 100644
index 000000000..27b17bb32
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html
@@ -0,0 +1,173 @@
+
+
+
+
+  
+  Test startWithDevice for B2G Presentation API at sender side
+  
+  
+
+
+Test startWithDevice for B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
new file mode 100644
index 000000000..f26184f0b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
@@ -0,0 +1,137 @@
+
+
+
+
+  
+  Test for B2G PresentationConnection API at receiver side
+  
+  
+
+
+Test for B2G PresentationConnection API at receiver side
+

+ +

+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
new file mode 100644
index 000000000..0935aaaf9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
@@ -0,0 +1,110 @@
+
+
+
+
+  
+  Test for connection establishing errors of B2G Presentation API at receiver side
+  
+  
+
+
+Test for connection establishing errors of B2G Presentation API at receiver side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
new file mode 100644
index 000000000..1dc002644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
@@ -0,0 +1,81 @@
+
+
+
+
+  
+  Test for connection establishing timeout of B2G Presentation API at receiver side
+  
+  
+
+
+Test for connection establishing timeout of B2G Presentation API at receiver side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
new file mode 100644
index 000000000..d73f84cf8
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
@@ -0,0 +1,88 @@
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_unknown_content_type.test');
+
+var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+          .getService(SpecialPowers.Ci.nsIObserverService);
+
+var receiverIframe;
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage('trigger-device-add');
+
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    receiverIframe.setAttribute('src', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute("remote", oop);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+
+      aResolve(receiverIframe);
+    });
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+
+    aResolve();
+  });
+}
+
+function testIncomingSessionRequestReceiverLaunchUnknownContentType() {
+  let promise = Promise.all([
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('receiver-launching', function launchReceiverHandler(aSessionId) {
+        gScript.removeMessageListener('receiver-launching', launchReceiverHandler);
+        info('Trying to launch receiver page.');
+
+        receiverIframe.addEventListener('mozbrowserclose', function() {
+          ok(true, 'observe receiver window closed');
+          aResolve();
+        });
+      });
+    }),
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) {
+        gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler);
+        is(aReason, 0x80530020 /* NS_ERROR_DOM_OPERATION_ERR */, 'The control channel is closed due to load failure.');
+        aResolve();
+      });
+    })
+  ]);
+
+  gScript.sendAsyncMessage('trigger-incoming-session-request', receiverUrl);
+  return promise;
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().
+  then(testIncomingSessionRequestReceiverLaunchUnknownContentType).
+  then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'browser', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ["dom.presentation.controller.enabled", true],
+                                      ["dom.presentation.receiver.enabled", true],
+                                      ['dom.presentation.session_transport.data_channel.enable', false],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ["network.disable.ipc.security", true],
+                                      ['dom.ipc.tabs.disabled', false]]},
+                            runTests);
+});
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html
new file mode 100644
index 000000000..8ade1d72d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html
@@ -0,0 +1,16 @@
+
+
+
+
+  
+  Test for unknown content type of B2G Presentation API at receiver side
+  
+  
+
+
+Test for unknown content type of B2G Presentation API at receiver side
+    
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html
new file mode 100644
index 000000000..b2d2d3c6e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html
@@ -0,0 +1,16 @@
+
+
+
+
+  
+  Test for unknown content type of B2G Presentation API at receiver side (OOP)
+  
+  
+
+
+Test for unknown content type of B2G Presentation API at receiver side (OOP)
+    
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
new file mode 100644
index 000000000..bfbc7947a
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
@@ -0,0 +1,178 @@
+
+
+
+
+  
+  Test for B2G PresentationConnection API at receiver side (OOP)
+  
+  
+
+
+Test B2G PresentationConnection API at receiver side (OOP)
+

+ +

+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
new file mode 100644
index 000000000..8df34c884
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
@@ -0,0 +1,260 @@
+
+
+
+
+  
+  Test for B2G Presentation API at sender side
+  
+  
+
+
+Test for B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html
new file mode 100644
index 000000000..60247ec98
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html
@@ -0,0 +1,151 @@
+
+
+
+
+  
+  Test default request for B2G Presentation API at sender side
+  
+  
+
+
+Test default request for B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html
new file mode 100644
index 000000000..a95da104f
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html
@@ -0,0 +1,160 @@
+
+
+
+
+  
+  Test for disconnection of B2G Presentation API at sender side
+  
+  
+
+
+Test for disconnection of B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html
new file mode 100644
index 000000000..557ae71a6
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html
@@ -0,0 +1,514 @@
+
+
+
+
+  
+  Test for connection establishing errors of B2G Presentation API at sender side
+  
+  
+
+
+Test for connection establishing errors of B2G Presentation API at sender side
+
+
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate.js b/dom/presentation/tests/mochitest/test_presentation_terminate.js
new file mode 100644
index 000000000..8ebfd9d64
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,243 @@
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
+
+function debug(str) {
+  // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate.html');
+var request;
+var connection;
+var receiverIframe;
+
+function setup() {
+  gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+    debug('Got message: device-prompt');
+    gScript.removeMessageListener('device-prompt', devicePromptHandler);
+    gScript.sendAsyncMessage('trigger-device-prompt-select');
+  });
+
+  gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute('remote', oop);
+
+    receiverIframe.setAttribute('src', receiverUrl);
+    receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+      info('Receiver loaded.');
+    });
+
+    // This event is triggered when the iframe calls 'alert'.
+    receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ''));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ''));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ''));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, 'Messaging from iframe complete.');
+        receiverIframe.removeEventListener('mozbrowsershowmodalprompt',
+                                            receiverListener);
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+      aResolve(receiverIframe);
+    });
+
+    var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+                           .getService(SpecialPowers.Ci.nsIObserverService);
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+  });
+
+  gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() {
+    debug('Got message: promise-setup-ready');
+    gScript.removeMessageListener('promise-setup-ready',
+                                  promiseSetupReadyHandler);
+    gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+  });
+
+  return Promise.resolve();
+}
+
+function testCreateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testCreateRequest ---');
+    request = new PresentationRequest(receiverUrl);
+    request.getAvailability().then((aAvailability) => {
+      is(aAvailability.value, false, "Sender: should have no available device after setup");
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, 'Sender: Device should be available.');
+        aResolve();
+      }
+
+      gScript.sendAsyncMessage('trigger-device-add');
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when getting availability: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    request.start().then((aConnection) => {
+      connection = aConnection;
+      ok(connection, 'Sender: Connection should be available.');
+      ok(connection.id, 'Sender: Connection ID should be set.');
+      is(connection.state, 'connecting', 'Sender: The initial state should be connecting.');
+      connection.onconnect = function() {
+        connection.onconnect = null;
+        is(connection.state, 'connected', 'Connection should be connected.');
+        aResolve();
+      };
+
+      info('Sender: test terminate at connecting state');
+      connection.onterminate = function() {
+        connection.onterminate = null;
+        ok(false, 'Should not be able to terminate at connecting state');
+        aReject();
+      }
+      connection.terminate();
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when establishing a connection: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testConnectionTerminate---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+    };
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      gScript.sendAsyncMessage('trigger-control-channel-open');
+    });
+    gScript.addMessageListener('sender-terminate', function senderTerminateHandler() {
+      gScript.removeMessageListener('sender-terminate',
+                                    senderTerminateHandler);
+
+      Promise.all([
+        new Promise((resolve) => {
+          gScript.addMessageListener('device-disconnected', function deviceDisconnectedHandler() {
+            gScript.removeMessageListener('device-disconnected', deviceDisconnectedHandler);
+            ok(true, 'observe device disconnect');
+            resolve();
+          });
+        }),
+        new Promise((resolve) => {
+          receiverIframe.addEventListener('mozbrowserclose', function() {
+            ok(true, 'observe receiver page closing');
+            resolve();
+          });
+        }),
+      ]).then(aResolve);
+
+      gScript.sendAsyncMessage('trigger-on-terminate-request');
+    });
+    gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() {
+      gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate);
+      connection.terminate();
+
+      // test unexpected close right after terminate
+      connection.onclose = function() {
+        ok(false, 'close after terminate should do nothing');
+      };
+      connection.close();
+    });
+  });
+}
+
+function testSendAfterTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    try {
+      connection.send('something');
+      ok(false, 'PresentationConnection.send should be failed');
+    } catch (e) {
+      is(e.name, 'InvalidStateError', 'Must throw InvalidStateError');
+    }
+    aResolve();
+  });
+}
+
+function testCloseAfterTerminate() {
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onclose = function() {
+          connection.onclose = null;
+          ok(false, 'close at terminated state should do nothing');
+          aResolve();
+        };
+        connection.close();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    debug('Got message: teardown-complete');
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().then(testCreateRequest)
+         .then(testStartConnection)
+         .then(testConnectionTerminate)
+         .then(testSendAfterTerminate)
+         .then(testCloseAfterTerminate)
+         .then(teardown);
+}
+
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'browser', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ["dom.presentation.controller.enabled", true],
+                                      ["dom.presentation.receiver.enabled", true],
+                                      ['dom.presentation.test.enabled', true],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ["network.disable.ipc.security", true],
+                                      ['dom.ipc.tabs.disabled', false],
+                                      ['dom.presentation.test.stage', 0]]},
+                            runTests);
+});
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
new file mode 100644
index 000000000..a1d477aab
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
@@ -0,0 +1,197 @@
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
+
+function debug(str) {
+  // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate_establish_connection_error.html');
+var request;
+var connection;
+var receiverIframe;
+
+function postMessageToIframe(aType) {
+  receiverIframe.src = receiverUrl + "#" +
+                       encodeURIComponent(JSON.stringify({ type: aType }));
+}
+
+function setup() {
+  gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+    debug('Got message: device-prompt');
+    gScript.removeMessageListener('device-prompt', devicePromptHandler);
+    gScript.sendAsyncMessage('trigger-device-prompt-select');
+  });
+
+  gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute('remote', oop);
+
+    receiverIframe.setAttribute('src', receiverUrl);
+    receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+      info('Receiver loaded.');
+    });
+
+    // This event is triggered when the iframe calls 'alert'.
+    receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ''));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ''));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ''));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, 'Messaging from iframe complete.');
+        receiverIframe.removeEventListener('mozbrowsershowmodalprompt',
+                                            receiverListener);
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+      aResolve(receiverIframe);
+    });
+
+    var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+                           .getService(SpecialPowers.Ci.nsIObserverService);
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+  });
+
+  gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() {
+    debug('Got message: promise-setup-ready');
+    gScript.removeMessageListener('promise-setup-ready',
+                                  promiseSetupReadyHandler);
+    gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+  });
+
+  return Promise.resolve();
+}
+
+function testCreateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testCreateRequest ---');
+    request = new PresentationRequest(receiverUrl);
+    request.getAvailability().then((aAvailability) => {
+      is(aAvailability.value, false, "Sender: should have no available device after setup");
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, 'Sender: Device should be available.');
+        aResolve();
+      }
+
+      gScript.sendAsyncMessage('trigger-device-add');
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when getting availability: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    request.start().then((aConnection) => {
+      connection = aConnection;
+      ok(connection, 'Sender: Connection should be available.');
+      ok(connection.id, 'Sender: Connection ID should be set.');
+      is(connection.state, 'connecting', 'Sender: The initial state should be connecting.');
+      connection.onconnect = function() {
+        connection.onconnect = null;
+        is(connection.state, 'connected', 'Connection should be connected.');
+        aResolve();
+      };
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when establishing a connection: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testConnectionTerminate() {
+  info('Sender: --- testConnectionTerminate---');
+  let promise = Promise.all([
+    new Promise(function(aResolve, aReject) {
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+        aResolve();
+      };
+    }),
+    new Promise(function(aResolve, aReject) {
+      function deviceDisconnectedHandler() {
+        gScript.removeMessageListener('device-disconnected', deviceDisconnectedHandler);
+        ok(true, 'should not receive device disconnect');
+        aResolve();
+      }
+
+      gScript.addMessageListener('device-disconnected', deviceDisconnectedHandler);
+    }),
+    new Promise(function(aResolve, aReject) {
+      receiverIframe.addEventListener('mozbrowserclose', function() {
+        ok(true, 'observe receiver page closing');
+        aResolve();
+      });
+    })
+  ]);
+
+  gScript.addMessageListener('prepare-for-terminate', function prepareForTerminateHandler() {
+    debug('Got message: prepare-for-terminate');
+    gScript.removeMessageListener('prepare-for-terminate', prepareForTerminateHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-error');
+    postMessageToIframe('ready-to-terminate');
+  });
+
+  return promise;
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    debug('Got message: teardown-complete');
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().then(testCreateRequest)
+         .then(testStartConnection)
+         .then(testConnectionTerminate)
+         .then(teardown);
+}
+
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'browser', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ["dom.presentation.controller.enabled", true],
+                                      ["dom.presentation.receiver.enabled", true],
+                                      ['dom.presentation.test.enabled', true],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ["network.disable.ipc.security", true],
+                                      ['dom.ipc.tabs.disabled', false],
+                                      ['dom.presentation.test.stage', 0]]},
+                            runTests);
+});
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
new file mode 100644
index 000000000..ccf0767f1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for control channel establish error during PresentationConnection.terminate()
+    
+    
+  
+  
+    
+      Test for constrol channel establish error during PresentationConnection.terminate()
+    
+  
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
new file mode 100644
index 000000000..ccf0767f1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for control channel establish error during PresentationConnection.terminate()
+    
+    
+  
+  
+    
+      Test for constrol channel establish error during PresentationConnection.terminate()
+    
+  
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
new file mode 100644
index 000000000..33bbcda57
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for PresentationConnection.terminate()
+    
+    
+  
+  
+    
+      Test for PresentationConnection.terminate()
+    
+  
+
diff --git a/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
new file mode 100644
index 000000000..33bbcda57
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
@@ -0,0 +1,18 @@
+
+
+
+  
+  
+    
+    Test for PresentationConnection.terminate()
+    
+    
+  
+  
+    
+      Test for PresentationConnection.terminate()
+    
+  
+
diff --git a/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
new file mode 100644
index 000000000..137a5609a
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
@@ -0,0 +1,1318 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global Services, do_register_cleanup, do_test_pending */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const INFO_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-info;1";
+const PROVIDER_CONTRACT_ID = "@mozilla.org/presentation-device/multicastdns-provider;1";
+const SD_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-sd;1";
+const UUID_CONTRACT_ID = "@mozilla.org/uuid-generator;1";
+const SERVER_CONTRACT_ID = "@mozilla.org/presentation/control-service;1";
+
+const PREF_DISCOVERY = "dom.presentation.discovery.enabled";
+const PREF_DISCOVERABLE = "dom.presentation.discoverable";
+const PREF_DEVICENAME= "dom.presentation.device.name";
+
+const LATEST_VERSION = 1;
+const SERVICE_TYPE = "_presentation-ctrl._tcp";
+const versionAttr = Cc["@mozilla.org/hash-property-bag;1"]
+                      .createInstance(Ci.nsIWritablePropertyBag2);
+versionAttr.setPropertyAsUint32("version", LATEST_VERSION);
+
+var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+function sleep(aMs) {
+  let deferred = Promise.defer();
+
+  let timer = Cc["@mozilla.org/timer;1"]
+                .createInstance(Ci.nsITimer);
+
+  timer.initWithCallback({
+    notify: function () {
+      deferred.resolve();
+    },
+  }, aMs, timer.TYPE_ONE_SHOT);
+
+  return deferred.promise;
+}
+
+function MockFactory(aClass) {
+  this._cls = aClass;
+}
+MockFactory.prototype = {
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    switch(typeof(this._cls)) {
+      case "function":
+        return new this._cls().QueryInterface(aIID);
+      case "object":
+        return this._cls.QueryInterface(aIID);
+      default:
+        return null;
+    }
+  },
+  lockFactory: function(aLock) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
+};
+
+function ContractHook(aContractID, aClass) {
+  this._contractID = aContractID;
+  this.classID = Cc[UUID_CONTRACT_ID].getService(Ci.nsIUUIDGenerator).generateUUID();
+  this._newFactory = new MockFactory(aClass);
+
+  if (!this.hookedMap.has(this._contractID)) {
+    this.hookedMap.set(this._contractID, []);
+  }
+
+  this.init();
+}
+
+ContractHook.prototype = {
+  hookedMap: new Map(), // remember only the most original factory.
+
+  init: function() {
+    this.reset();
+
+    let oldContract = this.unregister();
+    this.hookedMap.get(this._contractID).push(oldContract);
+    registrar.registerFactory(this.classID,
+                              "",
+                              this._contractID,
+                              this._newFactory);
+
+    do_register_cleanup(() => { this.cleanup.apply(this); });
+  },
+
+  reset: function() {},
+
+  cleanup: function() {
+    this.reset();
+
+    this.unregister();
+    let prevContract = this.hookedMap.get(this._contractID).pop();
+
+    if (prevContract.factory) {
+      registrar.registerFactory(prevContract.classID,
+                                "",
+                                this._contractID,
+                                prevContract.factory);
+    }
+  },
+
+  unregister: function() {
+    var classID, factory;
+
+    try {
+      classID = registrar.contractIDToCID(this._contractID);
+      factory = Cm.getClassObject(Cc[this._contractID], Ci.nsIFactory);
+    } catch (ex) {
+      classID = "";
+      factory = null;
+    }
+
+    if (factory) {
+      registrar.unregisterFactory(classID, factory);
+    }
+
+    return { classID: classID, factory: factory };
+  }
+};
+
+function MockDNSServiceInfo() {}
+MockDNSServiceInfo.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceInfo]),
+
+  set host(aHost) {
+    this._host = aHost;
+  },
+
+  get host() {
+    return this._host;
+  },
+
+  set address(aAddress) {
+    this._address = aAddress;
+  },
+
+  get address() {
+    return this._address;
+  },
+
+  set port(aPort) {
+    this._port = aPort;
+  },
+
+  get port() {
+    return this._port;
+  },
+
+  set serviceName(aServiceName) {
+    this._serviceName = aServiceName;
+  },
+
+  get serviceName() {
+    return this._serviceName;
+  },
+
+  set serviceType(aServiceType) {
+    this._serviceType = aServiceType;
+  },
+
+  get serviceType() {
+    return this._serviceType;
+  },
+
+  set domainName(aDomainName) {
+    this._domainName = aDomainName;
+  },
+
+  get domainName() {
+    return this._domainName;
+  },
+
+  set attributes(aAttributes) {
+    this._attributes = aAttributes;
+  },
+
+  get attributes() {
+    return this._attributes;
+  }
+};
+
+function TestPresentationDeviceListener() {
+  this.devices = {};
+}
+TestPresentationDeviceListener.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                         Ci.nsISupportsWeakReference]),
+
+  addDevice: function(device) { this.devices[device.id] = device; },
+  removeDevice: function(device) { delete this.devices[device.id]; },
+  updateDevice: function(device) { this.devices[device.id] = device; },
+  onSessionRequest: function(device, url, presentationId, controlChannel) {},
+
+  count: function() {
+    var size = 0, key;
+    for (key in this.devices) {
+        if (this.devices.hasOwnProperty(key)) {
+          ++size;
+        }
+    }
+    return size;
+  }
+};
+
+function createDevice(host, port, serviceName, serviceType, domainName, attributes) {
+  let device = new MockDNSServiceInfo();
+  device.host = host || "";
+  device.port = port || 0;
+  device.address = host || "";
+  device.serviceName = serviceName || "";
+  device.serviceType = serviceType || "";
+  device.domainName = domainName || "";
+  device.attributes = attributes || versionAttr;
+  return device;
+}
+
+function registerService() {
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let deferred = Promise.defer();
+
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {},
+    registerService: function(serviceInfo, listener) {
+      deferred.resolve();
+      this.serviceRegistered++;
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {
+          this.serviceUnregistered++;
+        }.bind(this)
+      };
+    },
+    resolveService: function(serviceInfo, listener) {},
+    serviceRegistered: 0,
+    serviceUnregistered: 0
+  };
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+
+  Assert.equal(mockObj.serviceRegistered, 0);
+  Assert.equal(mockObj.serviceUnregistered, 0);
+
+  // Register
+  provider.listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+  };
+
+  deferred.promise.then(function() {
+    Assert.equal(mockObj.serviceRegistered, 1);
+    Assert.equal(mockObj.serviceUnregistered, 0);
+
+    // Unregister
+    provider.listener = null;
+    Assert.equal(mockObj.serviceRegistered, 1);
+    Assert.equal(mockObj.serviceUnregistered, 1);
+
+    run_next_test();
+  });
+}
+
+function noRegisterService() {
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+  let deferred = Promise.defer();
+
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {},
+    registerService: function(serviceInfo, listener) {
+      deferred.resolve();
+      Assert.ok(false, "should not register service if not discoverable");
+    },
+    resolveService: function(serviceInfo, listener) {},
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+
+  // Try register
+  provider.listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+  };
+
+  let race = Promise.race([
+    deferred.promise,
+    sleep(1000),
+  ]);
+
+  race.then(() => {
+    provider.listener = null;
+
+    run_next_test();
+  });
+}
+
+function registerServiceDynamically() {
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+  let deferred = Promise.defer();
+
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {},
+    registerService: function(serviceInfo, listener) {
+      deferred.resolve();
+      this.serviceRegistered++;
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {
+          this.serviceUnregistered++;
+        }.bind(this)
+      };
+    },
+    resolveService: function(serviceInfo, listener) {},
+    serviceRegistered: 0,
+    serviceUnregistered: 0
+  };
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+
+  Assert.equal(mockObj.serviceRegistered, 0);
+  Assert.equal(mockObj.serviceRegistered, 0);
+
+  // Try Register
+  provider.listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+  };
+
+  Assert.equal(mockObj.serviceRegistered, 0);
+  Assert.equal(mockObj.serviceUnregistered, 0);
+
+  // Enable registration
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  deferred.promise.then(function() {
+    Assert.equal(mockObj.serviceRegistered, 1);
+    Assert.equal(mockObj.serviceUnregistered, 0);
+
+    // Disable registration
+    Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+    Assert.equal(mockObj.serviceRegistered, 1);
+    Assert.equal(mockObj.serviceUnregistered, 1);
+
+    // Try unregister
+    provider.listener = null;
+    Assert.equal(mockObj.serviceRegistered, 1);
+    Assert.equal(mockObj.serviceUnregistered, 1);
+
+    run_next_test();
+  });
+}
+
+function addDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = new TestPresentationDeviceListener();
+  Assert.equal(listener.count(), 0);
+
+  // Start discovery
+  provider.listener = listener;
+  Assert.equal(listener.count(), 1);
+
+  // Force discovery again
+  provider.forceDiscovery();
+  Assert.equal(listener.count(), 1);
+
+  provider.listener = null;
+  Assert.equal(listener.count(), 1);
+
+  run_next_test();
+}
+
+function filterDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {
+      let tests = [
+        { requestedUrl: "app://fling-player.gaiamobile.org/index.html", supported: true },
+        { requestedUrl: "app://notification-receiver.gaiamobile.org/index.html", supported: true },
+        { requestedUrl: "http://example.com", supported: true },
+        { requestedUrl: "https://example.com", supported: true },
+        { requestedUrl: "ftp://example.com", supported: false },
+        { requestedUrl: "app://unknown-app-id", supported: false },
+        { requestedUrl: "unknowSchem://example.com", supported: false },
+      ];
+
+      for (let test of tests) {
+        Assert.equal(device.isRequestedUrlSupported(test.requestedUrl), test.supported);
+      }
+
+      provider.listener = null;
+      run_next_test();
+    },
+    updateDevice: function() {},
+    removeDevice: function() {},
+    onSessionRequest: function() {},
+  };
+
+  provider.listener = listener;
+}
+
+function handleSessionRequest() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+  const testUrl = "http://example.com";
+  const testPresentationId = "test-presentation-id";
+  const testDeviceName = "test-device-name";
+
+  Services.prefs.setCharPref(PREF_DEVICENAME, testDeviceName);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    connect: function(deviceInfo) {
+      this.request = {
+        deviceInfo: deviceInfo,
+      };
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+      };
+    },
+    id: "",
+    version: LATEST_VERSION,
+    isCompatibleServer: function(version) {
+      return this.version === version;
+    }
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {
+      this.device = device;
+    },
+  };
+
+  provider.listener = listener;
+
+  let controlChannel = listener.device.establishControlChannel();
+
+  Assert.equal(mockServerObj.request.deviceInfo.id, mockDevice.host);
+  Assert.equal(mockServerObj.request.deviceInfo.address, mockDevice.host);
+  Assert.equal(mockServerObj.request.deviceInfo.port, mockDevice.port);
+  Assert.equal(mockServerObj.id, testDeviceName);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function handleOnSessionRequest() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    startServer: function() {},
+    sessionRequest: function() {},
+    close: function() {},
+    id: '',
+    version: LATEST_VERSION,
+    port: 0,
+    listener: null,
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+    onSessionRequest: function(device, url, presentationId, controlChannel) {
+      Assert.ok(true, "receive onSessionRequest event");
+      this.request = {
+        deviceId: device.id,
+        url: url,
+        presentationId: presentationId,
+      };
+    }
+  };
+
+  provider.listener = listener;
+
+  const deviceInfo = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+    id: mockDevice.host,
+    address: mockDevice.host,
+    port: 54321,
+  };
+
+  const testUrl = "http://example.com";
+  const testPresentationId = "test-presentation-id";
+  const testControlChannel = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  };
+  provider.QueryInterface(Ci.nsIPresentationControlServerListener)
+          .onSessionRequest(deviceInfo, testUrl, testPresentationId, testControlChannel);
+
+  Assert.equal(listener.request.deviceId, deviceInfo.id);
+  Assert.equal(listener.request.url, testUrl);
+  Assert.equal(listener.request.presentationId, testPresentationId);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function handleOnSessionRequestFromUnknownDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {},
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {}
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    startServer: function() {},
+    sessionRequest: function() {},
+    close: function() {},
+    id: '',
+    version: LATEST_VERSION,
+    port: 0,
+    listener: null,
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {
+      Assert.ok(false, "shouldn't create any new device");
+    },
+    removeDevice: function(device) {
+      Assert.ok(false, "shouldn't remote any device");
+    },
+    updateDevice: function(device) {
+      Assert.ok(false, "shouldn't update any device");
+    },
+    onSessionRequest: function(device, url, presentationId, controlChannel) {
+      Assert.ok(true, "receive onSessionRequest event");
+      this.request = {
+        deviceId: device.id,
+        url: url,
+        presentationId: presentationId,
+      };
+    }
+  };
+
+  provider.listener = listener;
+
+  const deviceInfo = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+    id: "unknown-device.local",
+    address: "unknown-device.local",
+    port: 12345,
+  };
+
+  const testUrl = "http://example.com";
+  const testPresentationId = "test-presentation-id";
+  const testControlChannel = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  };
+  provider.QueryInterface(Ci.nsIPresentationControlServerListener)
+          .onSessionRequest(deviceInfo, testUrl, testPresentationId, testControlChannel);
+
+  Assert.equal(listener.request.deviceId, deviceInfo.id);
+  Assert.equal(listener.request.url, testUrl);
+  Assert.equal(listener.request.presentationId, testPresentationId);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function noAddDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+
+  let mockDevice = createDevice("device.local", 12345, "service.name", SERVICE_TYPE);
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      Assert.ok(false, "shouldn't perform any device discovery");
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+    }
+  };
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+  };
+  provider.listener = listener;
+  provider.forceDiscovery();
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function ignoreIncompatibleDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+
+  let deferred = Promise.defer();
+
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {
+      deferred.resolve();
+      listener.onServiceRegistered(createDevice("",
+                                                54321,
+                                                mockDevice.serviceName,
+                                                mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    startServer: function() {
+      Services.tm.currentThread.dispatch(() => {
+        this.listener.onServerReady(this.port, this.certFingerprint);
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+    },
+    sessionRequest: function() {},
+    close: function() {},
+    id: '',
+    version: LATEST_VERSION,
+    isCompatibleServer: function(version) {
+      return false;
+    },
+    port: 54321,
+    certFingerprint: 'mock-cert-fingerprint',
+    listener: null,
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = new TestPresentationDeviceListener();
+
+  // Register service
+  provider.listener = listener;
+
+  deferred.promise.then(function() {
+    Assert.equal(mockServerObj.id, mockDevice.host);
+
+    // Start discovery
+    Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+    Assert.equal(listener.count(), 0);
+
+    provider.listener = null;
+
+    run_next_test();
+  });
+}
+
+function ignoreSelfDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+
+  let deferred = Promise.defer();
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {
+      deferred.resolve();
+      listener.onServiceRegistered(createDevice("",
+                                                0,
+                                                mockDevice.serviceName,
+                                                mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    startServer: function() {
+      Services.tm.currentThread.dispatch(() => {
+        this.listener.onServerReady(this.port, this.certFingerprint);
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+    },
+    sessionRequest: function() {},
+    close: function() {},
+    id: '',
+    version: LATEST_VERSION,
+    isCompatibleServer: function(version) {
+      return this.version === version;
+    },
+    port: 54321,
+    certFingerprint: 'mock-cert-fingerprint',
+    listener: null,
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = new TestPresentationDeviceListener();
+
+  // Register service
+  provider.listener = listener;
+  deferred.promise.then(() => {
+    Assert.equal(mockServerObj.id, mockDevice.host);
+
+    // Start discovery
+    Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+    Assert.equal(listener.count(), 0);
+
+    provider.listener = null;
+
+    run_next_test();
+  });
+}
+
+function addDeviceDynamically() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    }
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = new TestPresentationDeviceListener();
+  provider.listener = listener;
+  Assert.equal(listener.count(), 0);
+
+  // Enable discovery
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+  Assert.equal(listener.count(), 1);
+
+  // Try discovery again
+  provider.forceDiscovery();
+  Assert.equal(listener.count(), 1);
+
+  // Try discovery once more
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+  provider.forceDiscovery();
+  Assert.equal(listener.count(), 1);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function updateDevice() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
+  let mockDevice2 = createDevice("A.local", 23456, "N2", SERVICE_TYPE);
+
+  let mockObj = {
+    discovered: false,
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+
+      if (!this.discovered) {
+        listener.onServiceFound(mockDevice1);
+      } else {
+        listener.onServiceFound(mockDevice2);
+      }
+      this.discovered = true;
+
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {
+          listener.onDiscoveryStopped(serviceType);
+        }
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
+      if (serviceInfo.serviceName == "N1") {
+        listener.onServiceResolved(mockDevice1);
+      } else if (serviceInfo.serviceName == "N2") {
+        listener.onServiceResolved(mockDevice2);
+      } else {
+        Assert.ok(false);
+      }
+    }
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+
+    addDevice: function(device) {
+      Assert.ok(!this.isDeviceAdded);
+      Assert.equal(device.id, mockDevice1.host);
+      Assert.equal(device.name, mockDevice1.serviceName);
+      this.isDeviceAdded = true;
+    },
+    removeDevice: function(device) { Assert.ok(false); },
+    updateDevice: function(device) {
+      Assert.ok(!this.isDeviceUpdated);
+      Assert.equal(device.id, mockDevice2.host);
+      Assert.equal(device.name, mockDevice2.serviceName);
+      this.isDeviceUpdated = true;
+    },
+
+    isDeviceAdded: false,
+    isDeviceUpdated: false
+  };
+  Assert.equal(listener.isDeviceAdded, false);
+  Assert.equal(listener.isDeviceUpdated, false);
+
+  // Start discovery
+  provider.listener = listener; // discover: N1
+
+  Assert.equal(listener.isDeviceAdded, true);
+  Assert.equal(listener.isDeviceUpdated, false);
+
+  // temporarily disable to stop discovery and re-enable
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  provider.forceDiscovery(); // discover: N2
+
+  Assert.equal(listener.isDeviceAdded, true);
+  Assert.equal(listener.isDeviceUpdated, true);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function diffDiscovery() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
+  let mockDevice2 = createDevice("B.local", 23456, "N2", SERVICE_TYPE);
+  let mockDevice3 = createDevice("C.local", 45678, "N3", SERVICE_TYPE);
+
+  let mockObj = {
+    discovered: false,
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+
+      if (!this.discovered) {
+        listener.onServiceFound(mockDevice1);
+        listener.onServiceFound(mockDevice2);
+      } else {
+        listener.onServiceFound(mockDevice1);
+        listener.onServiceFound(mockDevice3);
+      }
+      this.discovered = true;
+
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {
+          listener.onDiscoveryStopped(serviceType);
+        }
+      };
+    },
+    registerService: function(serviceInfo, listener) {},
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
+      if (serviceInfo.serviceName == "N1") {
+        listener.onServiceResolved(mockDevice1);
+      } else if (serviceInfo.serviceName == "N2") {
+        listener.onServiceResolved(mockDevice2);
+      } else if (serviceInfo.serviceName == "N3") {
+        listener.onServiceResolved(mockDevice3);
+      } else {
+        Assert.ok(false);
+      }
+    }
+  };
+
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = new TestPresentationDeviceListener();
+  Assert.equal(listener.count(), 0);
+
+  // Start discovery
+  provider.listener = listener; // discover: N1, N2
+  Assert.equal(listener.count(), 2);
+  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
+  Assert.equal(listener.devices['B.local'].name, mockDevice2.serviceName);
+  Assert.ok(!listener.devices['C.local']);
+
+  // temporarily disable to stop discovery and re-enable
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  provider.forceDiscovery(); // discover: N1, N3, going to remove: N2
+  Assert.equal(listener.count(), 3);
+  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
+  Assert.equal(listener.devices['B.local'].name, mockDevice2.serviceName);
+  Assert.equal(listener.devices['C.local'].name, mockDevice3.serviceName);
+
+  // temporarily disable to stop discovery and re-enable
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  provider.forceDiscovery(); // discover: N1, N3, remove: N2
+  Assert.equal(listener.count(), 2);
+  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
+  Assert.ok(!listener.devices['B.local']);
+  Assert.equal(listener.devices['C.local'].name, mockDevice3.serviceName);
+
+  provider.listener = null;
+
+  run_next_test();
+}
+
+function serverClosed() {
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+
+  let mockDevice = createDevice("device.local",
+                                12345,
+                                "service.name",
+                                SERVICE_TYPE);
+
+  let mockObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {
+      listener.onDiscoveryStarted(serviceType);
+      listener.onServiceFound(createDevice("",
+                                           0,
+                                           mockDevice.serviceName,
+                                           mockDevice.serviceType));
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {}
+      };
+    },
+    registerService: function(serviceInfo, listener) {
+      this.serviceRegistered++;
+      return {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+        cancel: function() {
+          this.serviceUnregistered++;
+        }.bind(this)
+      };
+    },
+    resolveService: function(serviceInfo, listener) {
+      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
+      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
+      listener.onServiceResolved(createDevice(mockDevice.host,
+                                              mockDevice.port,
+                                              mockDevice.serviceName,
+                                              mockDevice.serviceType));
+    },
+    serviceRegistered: 0,
+    serviceUnregistered: 0
+  };
+  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+
+  Assert.equal(mockObj.serviceRegistered, 0);
+  Assert.equal(mockObj.serviceUnregistered, 0);
+
+  // Register
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) { this.devices.push(device); },
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+    devices: []
+  };
+  Assert.equal(listener.devices.length, 0);
+
+  provider.listener = listener;
+  Assert.equal(mockObj.serviceRegistered, 1);
+  Assert.equal(mockObj.serviceUnregistered, 0);
+  Assert.equal(listener.devices.length, 1);
+
+  let serverListener = provider.QueryInterface(Ci.nsIPresentationControlServerListener);
+  let randomPort = 9527;
+  serverListener.onServerReady(randomPort, '');
+
+  Assert.equal(mockObj.serviceRegistered, 2);
+  Assert.equal(mockObj.serviceUnregistered, 1);
+  Assert.equal(listener.devices.length, 1);
+
+  // Unregister
+  provider.listener = null;
+  Assert.equal(mockObj.serviceRegistered, 2);
+  Assert.equal(mockObj.serviceUnregistered, 2);
+  Assert.equal(listener.devices.length, 1);
+
+  run_next_test();
+}
+
+function serverRetry() {
+  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
+  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
+
+  let isRetrying = false;
+
+  let mockSDObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
+    startDiscovery: function(serviceType, listener) {},
+    registerService: function(serviceInfo, listener) {
+      Assert.ok(isRetrying, "register service after retrying startServer");
+      provider.listener = null;
+      run_next_test();
+    },
+    resolveService: function(serviceInfo, listener) {}
+  };
+
+  let mockServerObj = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlService]),
+    startServer: function(encrypted, port) {
+      if (!isRetrying) {
+        isRetrying = true;
+        Services.tm.currentThread.dispatch(() => {
+          this.listener.onServerStopped(Cr.NS_ERROR_FAILURE);
+        }, Ci.nsIThread.DISPATCH_NORMAL);
+      } else {
+        this.port = 54321;
+        Services.tm.currentThread.dispatch(() => {
+          this.listener.onServerReady(this.port, this.certFingerprint);
+        }, Ci.nsIThread.DISPATCH_NORMAL);
+      }
+    },
+    sessionRequest: function() {},
+    close: function() {},
+    id: '',
+    version: LATEST_VERSION,
+    port: 0,
+    certFingerprint: 'mock-cert-fingerprint',
+    listener: null,
+  };
+
+  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
+  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
+  let listener = {
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceListener,
+                                           Ci.nsISupportsWeakReference]),
+    addDevice: function(device) {},
+    removeDevice: function(device) {},
+    updateDevice: function(device) {},
+    onSessionRequest: function(device, url, presentationId, controlChannel) {}
+  };
+
+  provider.listener = listener;
+}
+
+function run_test() {
+  // Need profile dir to store the key / cert
+  do_get_profile();
+  // Ensure PSM is initialized
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+  let infoHook = new ContractHook(INFO_CONTRACT_ID, MockDNSServiceInfo);
+
+  do_register_cleanup(() => {
+    Services.prefs.clearUserPref(PREF_DISCOVERY);
+    Services.prefs.clearUserPref(PREF_DISCOVERABLE);
+  });
+
+  add_test(registerService);
+  add_test(noRegisterService);
+  add_test(registerServiceDynamically);
+  add_test(addDevice);
+  add_test(filterDevice);
+  add_test(handleSessionRequest);
+  add_test(handleOnSessionRequest);
+  add_test(handleOnSessionRequestFromUnknownDevice);
+  add_test(noAddDevice);
+  add_test(ignoreIncompatibleDevice);
+  add_test(ignoreSelfDevice);
+  add_test(addDeviceDynamically);
+  add_test(updateDevice);
+  add_test(diffDiscovery);
+  add_test(serverClosed);
+  add_test(serverRetry);
+
+  run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
new file mode 100644
index 000000000..68f07df65
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -0,0 +1,244 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+const manager = Cc['@mozilla.org/presentation-device/manager;1']
+                  .getService(Ci.nsIPresentationDeviceManager);
+
+function TestPresentationDevice() {}
+
+
+function TestPresentationControlChannel() {}
+
+TestPresentationControlChannel.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  sendOffer: function(offer) {},
+  sendAnswer: function(answer) {},
+  disconnect: function() {},
+  launch: function() {},
+  terminate: function() {},
+  reconnect: function() {},
+  set listener(listener) {},
+  get listener() {},
+};
+
+var testProvider = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
+
+  forceDiscovery: function() {
+  },
+  set listener(listener) {
+  },
+  get listener() {
+  },
+};
+
+const forbiddenRequestedUrl = 'http://example.com';
+var testDevice = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+  id: 'id',
+  name: 'name',
+  type: 'type',
+  establishControlChannel: function(url, presentationId) {
+    return null;
+  },
+  disconnect: function() {},
+  isRequestedUrlSupported: function(requestedUrl) {
+    return forbiddenRequestedUrl !== requestedUrl;
+  },
+};
+
+function addProvider() {
+  Object.defineProperty(testProvider, 'listener', {
+    configurable: true,
+    set: function(listener) {
+      Assert.strictEqual(listener, manager, 'listener setter is invoked by PresentationDeviceManager');
+      delete testProvider.listener;
+      run_next_test();
+    },
+  });
+  manager.addDeviceProvider(testProvider);
+}
+
+function forceDiscovery() {
+  testProvider.forceDiscovery = function() {
+    testProvider.forceDiscovery = function() {};
+    Assert.ok(true, 'forceDiscovery is invoked by PresentationDeviceManager');
+    run_next_test();
+  };
+  manager.forceDiscovery();
+}
+
+function addDevice() {
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
+    Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
+    Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
+    Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
+    Assert.equal(data, 'add', 'expected update type');
+
+    Assert.ok(manager.deviceAvailable, 'device is available');
+
+    let devices = manager.getAvailableDevices();
+    Assert.equal(devices.length, 1, 'expect 1 available device');
+
+    let device = devices.queryElementAt(0, Ci.nsIPresentationDevice);
+    Assert.equal(device.id, testDevice.id, 'expected device id');
+    Assert.equal(device.name, testDevice.name, 'expected device name');
+    Assert.equal(device.type, testDevice.type, 'expected device type');
+
+    run_next_test();
+  }, 'presentation-device-change', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener).addDevice(testDevice);
+}
+
+function updateDevice() {
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
+    Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
+    Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
+    Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
+    Assert.equal(data, 'update', 'expected update type');
+
+    Assert.ok(manager.deviceAvailable, 'device is available');
+
+    let devices = manager.getAvailableDevices();
+    Assert.equal(devices.length, 1, 'expect 1 available device');
+
+    let device = devices.queryElementAt(0, Ci.nsIPresentationDevice);
+    Assert.equal(device.id, testDevice.id, 'expected device id');
+    Assert.equal(device.name, testDevice.name, 'expected name after device update');
+    Assert.equal(device.type, testDevice.type, 'expected device type');
+
+    run_next_test();
+  }, 'presentation-device-change', false);
+  testDevice.name = 'updated-name';
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener).updateDevice(testDevice);
+}
+
+function filterDevice() {
+  let presentationUrls = Cc['@mozilla.org/array;1'].createInstance(Ci.nsIMutableArray);
+  let url = Cc['@mozilla.org/supports-string;1'].createInstance(Ci.nsISupportsString);
+  url.data = forbiddenRequestedUrl;
+  presentationUrls.appendElement(url, false);
+  let devices = manager.getAvailableDevices(presentationUrls);
+  Assert.equal(devices.length, 0, 'expect 0 available device for example.com');
+  run_next_test();
+}
+
+function sessionRequest() {
+  let testUrl = 'http://www.example.org/';
+  let testPresentationId = 'test-presentation-id';
+  let testControlChannel = new TestPresentationControlChannel();
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let request = subject.QueryInterface(Ci.nsIPresentationSessionRequest);
+
+    Assert.equal(request.device.id, testDevice.id, 'expected device');
+    Assert.equal(request.url, testUrl, 'expected requesting URL');
+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+
+    run_next_test();
+  }, 'presentation-session-request', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+         .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel);
+}
+
+function terminateRequest() {
+  let testUrl = 'http://www.example.org/';
+  let testPresentationId = 'test-presentation-id';
+  let testControlChannel = new TestPresentationControlChannel();
+  let testIsFromReceiver = true;
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest);
+
+    Assert.equal(request.device.id, testDevice.id, 'expected device');
+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+    Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver');
+
+    run_next_test();
+  }, 'presentation-terminate-request', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+         .onTerminateRequest(testDevice, testPresentationId,
+                             testControlChannel, testIsFromReceiver);
+}
+
+function reconnectRequest() {
+  let testUrl = 'http://www.example.org/';
+  let testPresentationId = 'test-presentation-id';
+  let testControlChannel = new TestPresentationControlChannel();
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let request = subject.QueryInterface(Ci.nsIPresentationSessionRequest);
+
+    Assert.equal(request.device.id, testDevice.id, 'expected device');
+    Assert.equal(request.url, testUrl, 'expected requesting URL');
+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+
+    run_next_test();
+  }, 'presentation-reconnect-request', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+         .onReconnectRequest(testDevice, testUrl, testPresentationId, testControlChannel);
+}
+
+function removeDevice() {
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
+    Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
+    Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
+    Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
+    Assert.equal(data, 'remove', 'expected update type');
+
+    Assert.ok(!manager.deviceAvailable, 'device is not available');
+
+    let devices = manager.getAvailableDevices();
+    Assert.equal(devices.length, 0, 'expect 0 available device');
+
+    run_next_test();
+  }, 'presentation-device-change', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener).removeDevice(testDevice);
+}
+
+function removeProvider() {
+  Object.defineProperty(testProvider, 'listener', {
+    configurable: true,
+    set: function(listener) {
+      Assert.strictEqual(listener, null, 'unsetListener is invoked by PresentationDeviceManager');
+      delete testProvider.listener;
+      run_next_test();
+    },
+  });
+  manager.removeDeviceProvider(testProvider);
+}
+
+add_test(addProvider);
+add_test(forceDiscovery);
+add_test(addDevice);
+add_test(updateDevice);
+add_test(filterDevice);
+add_test(sessionRequest);
+add_test(terminateRequest);
+add_test(reconnectRequest);
+add_test(removeDevice);
+add_test(removeProvider);
+
+function run_test() {
+  run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_presentation_session_transport.js b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
new file mode 100644
index 000000000..8e207bc22
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
@@ -0,0 +1,198 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC } = Components;
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+                        "nsIServerSocket",
+                        "init");
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var testServer = null;
+var clientTransport = null;
+var serverTransport = null;
+
+var clientBuilder = null;
+var serverBuilder = null;
+
+const clientMessage = "Client Message";
+const serverMessage = "Server Message";
+
+const address = Cc["@mozilla.org/supports-cstring;1"]
+                  .createInstance(Ci.nsISupportsCString);
+address.data = "127.0.0.1";
+const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+addresses.appendElement(address, false);
+
+const serverChannelDescription = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+  type: 1,
+  tcpAddress: addresses,
+};
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+const clientCallback = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+  notifyTransportReady: function () {
+    Assert.ok(true, "Client transport ready.");
+
+    isClientReady = true;
+    if (isClientReady && isServerReady) {
+      run_next_test();
+    }
+  },
+  notifyTransportClosed: function (aReason) {
+    Assert.ok(true, "Client transport is closed.");
+
+    isClientClosed = true;
+    if (isClientClosed && isServerClosed) {
+      run_next_test();
+    }
+  },
+  notifyData: function(aData) {
+    Assert.equal(aData, serverMessage, "Client transport receives data.");
+    run_next_test();
+  },
+};
+
+const serverCallback = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportCallback]),
+  notifyTransportReady: function () {
+    Assert.ok(true, "Server transport ready.");
+
+    isServerReady = true;
+    if (isClientReady && isServerReady) {
+      run_next_test();
+    }
+  },
+  notifyTransportClosed: function (aReason) {
+    Assert.ok(true, "Server transport is closed.");
+
+    isServerClosed = true;
+    if (isClientClosed && isServerClosed) {
+      run_next_test();
+    }
+  },
+  notifyData: function(aData) {
+    Assert.equal(aData, clientMessage, "Server transport receives data.");
+    run_next_test();
+  },
+};
+
+const clientListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+  onSessionTransport(aTransport) {
+    Assert.ok(true, "Client Transport is built.");
+    clientTransport = aTransport;
+    clientTransport.callback = clientCallback;
+
+    if (serverTransport) {
+      run_next_test();
+    }
+  }
+}
+
+const serverListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransportBuilderListener]),
+  onSessionTransport(aTransport) {
+    Assert.ok(true, "Server Transport is built.");
+    serverTransport = aTransport;
+    serverTransport.callback = serverCallback;
+    serverTransport.enableDataNotification();
+
+    if (clientTransport) {
+      run_next_test();
+    }
+  }
+}
+
+function TestServer() {
+  this.serverSocket = ServerSocket(-1, true, -1);
+  this.serverSocket.asyncListen(this)
+}
+
+TestServer.prototype = {
+  onSocketAccepted: function(aSocket, aTransport) {
+    print("Test server gets a client connection.");
+    serverBuilder = Cc["@mozilla.org/presentation/presentationtcpsessiontransport;1"]
+                      .createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+    serverBuilder.buildTCPSenderTransport(aTransport, serverListener);
+  },
+  onStopListening: function(aSocket) {
+    print("Test server stops listening.");
+  },
+  close: function() {
+    if (this.serverSocket) {
+      this.serverSocket.close();
+      this.serverSocket = null;
+    }
+  }
+};
+
+// Set up the transport connection and ensure |notifyTransportReady| triggered
+// at both sides.
+function setup() {
+  clientBuilder = Cc["@mozilla.org/presentation/presentationtcpsessiontransport;1"]
+                    .createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+  clientBuilder.buildTCPReceiverTransport(serverChannelDescription, clientListener);
+}
+
+// Test |selfAddress| attribute of |nsIPresentationSessionTransport|.
+function selfAddress() {
+  var serverSelfAddress = serverTransport.selfAddress;
+  Assert.equal(serverSelfAddress.address, address.data, "The self address of server transport should be set.");
+  Assert.equal(serverSelfAddress.port, testServer.serverSocket.port, "The port of server transport should be set.");
+
+  var clientSelfAddress = clientTransport.selfAddress;
+  Assert.ok(clientSelfAddress.address, "The self address of client transport should be set.");
+  Assert.ok(clientSelfAddress.port, "The port of client transport should be set.");
+
+  run_next_test();
+}
+
+// Test the client sends a message and then a corresponding notification gets
+// triggered at the server side.
+function clientSendMessage() {
+  clientTransport.send(clientMessage);
+}
+
+// Test the server sends a message an then a corresponding notification gets
+// triggered at the client side.
+function serverSendMessage() {
+  serverTransport.send(serverMessage);
+  // The client enables data notification even after the incoming message has
+  // been sent, and should still be able to consume it.
+  clientTransport.enableDataNotification();
+}
+
+function transportClose() {
+  clientTransport.close(Cr.NS_OK);
+}
+
+function shutdown() {
+  testServer.close();
+  run_next_test();
+}
+
+add_test(setup);
+add_test(selfAddress);
+add_test(clientSendMessage);
+add_test(serverSendMessage);
+add_test(transportClose);
+add_test(shutdown);
+
+function run_test() {
+  testServer = new TestServer();
+  // Get the port of the test server.
+  serverChannelDescription.tcpPort = testServer.serverSocket.port;
+
+  run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
new file mode 100644
index 000000000..fcaf34da6
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -0,0 +1,236 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* jshint esnext:true, globalstrict:true, moz:true, undef:true, unused:true */
+/* globals Components,Assert,run_next_test,add_test,do_execute_soon */
+
+'use strict';
+
+const { utils: Cu, results: Cr } = Components;
+
+/* globals ControllerStateMachine */
+Cu.import('resource://gre/modules/presentation/ControllerStateMachine.jsm');
+/* globals ReceiverStateMachine */
+Cu.import('resource://gre/modules/presentation/ReceiverStateMachine.jsm');
+/* globals State */
+Cu.import('resource://gre/modules/presentation/StateMachineHelper.jsm');
+
+const testControllerId = 'test-controller-id';
+const testPresentationId = 'test-presentation-id';
+const testUrl = 'http://example.org';
+
+let mockControllerChannel = {};
+let mockReceiverChannel = {};
+
+let controllerState = new ControllerStateMachine(mockControllerChannel, testControllerId);
+let receiverState = new ReceiverStateMachine(mockReceiverChannel);
+
+mockControllerChannel.sendCommand = function(command) {
+  do_execute_soon(function() {
+    receiverState.onCommand(command);
+  });
+};
+
+mockReceiverChannel.sendCommand = function(command) {
+  do_execute_soon(function() {
+    controllerState.onCommand(command);
+  });
+};
+
+function connect() {
+  Assert.equal(controllerState.state, State.INIT, 'controller in init state');
+  Assert.equal(receiverState.state, State.INIT, 'receiver in init state');
+  // step 1: underlying connection is ready
+  controllerState.onChannelReady();
+  Assert.equal(controllerState.state, State.CONNECTING, 'controller in connecting state');
+  receiverState.onChannelReady();
+  Assert.equal(receiverState.state, State.CONNECTING, 'receiver in connecting state');
+
+  // step 2: receiver reply to connect command
+  mockReceiverChannel.notifyDeviceConnected = function(deviceId) {
+    Assert.equal(deviceId, testControllerId, 'receiver connect to mock controller');
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+    // step 3: controller receive connect-ack command
+    mockControllerChannel.notifyDeviceConnected = function() {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      run_next_test();
+    };
+  };
+}
+
+function launch() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  controllerState.launch(testPresentationId, testUrl);
+  mockReceiverChannel.notifyLaunch = function(presentationId, url) {
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+    Assert.equal(url, testUrl, 'expected url received');
+
+    mockControllerChannel.notifyLaunch = function(presentationId) {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+      run_next_test();
+    };
+  };
+}
+
+function terminateByController() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  controllerState.terminate(testPresentationId);
+  mockReceiverChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockControllerChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+      run_next_test();
+    };
+
+    receiverState.terminateAck(presentationId);
+  };
+}
+
+function terminateByReceiver() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  receiverState.terminate(testPresentationId);
+  mockControllerChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockReceiverChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+      run_next_test();
+    };
+
+    controllerState.terminateAck(presentationId);
+  };
+}
+
+function exchangeSDP() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  const testOffer = 'test-offer';
+  const testAnswer = 'test-answer';
+  const testIceCandidate = 'test-ice-candidate';
+  controllerState.sendOffer(testOffer);
+  mockReceiverChannel.notifyOffer = function(offer) {
+    Assert.equal(offer, testOffer, 'expected offer received');
+
+    receiverState.sendAnswer(testAnswer);
+    mockControllerChannel.notifyAnswer = function(answer) {
+      Assert.equal(answer, testAnswer, 'expected answer received');
+
+      controllerState.updateIceCandidate(testIceCandidate);
+      mockReceiverChannel.notifyIceCandidate = function(candidate) {
+        Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in receiver');
+
+        receiverState.updateIceCandidate(testIceCandidate);
+        mockControllerChannel.notifyIceCandidate = function(candidate) {
+          Assert.equal(candidate, testIceCandidate, 'expected ice candidate received in controller');
+
+          run_next_test();
+        };
+      };
+    };
+  };
+}
+
+function disconnect() {
+  // step 1: controller send disconnect command
+  controllerState.onChannelClosed(Cr.NS_OK, false);
+  Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+  mockReceiverChannel.notifyDisconnected = function(reason) {
+    Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    mockControllerChannel.notifyDisconnected = function(reason) {
+      Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+      Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+      run_next_test();
+    };
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+function receiverDisconnect() {
+  // initial state: controller and receiver are connected
+  controllerState.state = State.CONNECTED;
+  receiverState.state = State.CONNECTED;
+
+  // step 1: controller send disconnect command
+  receiverState.onChannelClosed(Cr.NS_OK, false);
+  Assert.equal(receiverState.state, State.CLOSING, 'receiver in closing state');
+
+  mockControllerChannel.notifyDisconnected = function(reason) {
+    Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+    Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+    mockReceiverChannel.notifyDisconnected = function(reason) {
+      Assert.equal(reason, Cr.NS_OK, 'receive close reason');
+      Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+      run_next_test();
+    };
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+function abnormalDisconnect() {
+  // initial state: controller and receiver are connected
+  controllerState.state = State.CONNECTED;
+  receiverState.state = State.CONNECTED;
+
+  const testErrorReason = Cr.NS_ERROR_FAILURE;
+  // step 1: controller send disconnect command
+  controllerState.onChannelClosed(testErrorReason, false);
+  Assert.equal(controllerState.state, State.CLOSING, 'controller in closing state');
+
+  mockReceiverChannel.notifyDisconnected = function(reason) {
+    Assert.equal(reason, testErrorReason, 'receive abnormal close reason');
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    receiverState.onChannelClosed(Cr.NS_OK, true);
+    Assert.equal(receiverState.state, State.CLOSED, 'receiver in closed state');
+
+    mockControllerChannel.notifyDisconnected = function(reason) {
+      Assert.equal(reason, testErrorReason, 'receive abnormal close reason');
+      Assert.equal(controllerState.state, State.CLOSED, 'controller in closed state');
+
+      run_next_test();
+    };
+    controllerState.onChannelClosed(Cr.NS_OK, true);
+  };
+}
+
+add_test(connect);
+add_test(launch);
+add_test(terminateByController);
+add_test(terminateByReceiver);
+add_test(exchangeSDP);
+add_test(disconnect);
+add_test(receiverDisconnect);
+add_test(abnormalDisconnect);
+
+function run_test() { // jshint ignore:line
+  run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
new file mode 100644
index 000000000..5f3df584d
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -0,0 +1,398 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var pcs;
+
+// Call |run_next_test| if all functions in |names| are called
+function makeJointSuccess(names) {
+  let funcs = {}, successCount = 0;
+  names.forEach(function(name) {
+    funcs[name] = function() {
+      do_print('got expected: ' + name);
+      if (++successCount === names.length)
+        run_next_test();
+    };
+  });
+  return funcs;
+}
+
+function TestDescription(aType, aTcpAddress, aTcpPort) {
+  this.type = aType;
+  this.tcpAddress = Cc["@mozilla.org/array;1"]
+                      .createInstance(Ci.nsIMutableArray);
+  for (let address of aTcpAddress) {
+    let wrapper = Cc["@mozilla.org/supports-cstring;1"]
+                    .createInstance(Ci.nsISupportsCString);
+    wrapper.data = address;
+    this.tcpAddress.appendElement(wrapper, false);
+  }
+  this.tcpPort = aTcpPort;
+}
+
+TestDescription.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+}
+
+const CONTROLLER_CONTROL_CHANNEL_PORT = 36777;
+const PRESENTER_CONTROL_CHANNEL_PORT = 36888;
+
+var CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_OK;
+var candidate;
+
+// presenter's presentation channel description
+const OFFER_ADDRESS = '192.168.123.123';
+const OFFER_PORT = 123;
+
+// controller's presentation channel description
+const ANSWER_ADDRESS = '192.168.321.321';
+const ANSWER_PORT = 321;
+
+function loopOfferAnser() {
+  pcs = Cc["@mozilla.org/presentation/control-service;1"]
+        .createInstance(Ci.nsIPresentationControlService);
+  pcs.id = 'controllerID';
+  pcs.listener = {
+    onServerReady: function() {
+      testPresentationServer();
+    }
+  };
+
+  // First run with TLS enabled.
+  pcs.startServer(true, PRESENTER_CONTROL_CHANNEL_PORT);
+}
+
+
+function testPresentationServer() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelClose',
+                                   'presenterControlChannelClose',
+                                   'controllerControlChannelReconnect',
+                                   'presenterControlChannelReconnect']);
+  let presenterControlChannel;
+
+  pcs.listener = {
+
+    onSessionRequest: function(deviceInfo, url, presentationId, controlChannel) {
+      presenterControlChannel = controlChannel;
+      Assert.equal(deviceInfo.id, pcs.id, 'expected device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(url, 'http://example.com', 'expected url');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+
+      presenterControlChannel.listener = {
+        status: 'created',
+        onOffer: function(aOffer) {
+          Assert.equal(this.status, 'opened', '1. presenterControlChannel: get offer, send answer');
+          this.status = 'onOffer';
+
+          let offer = aOffer.QueryInterface(Ci.nsIPresentationChannelDescription);
+          Assert.strictEqual(offer.tcpAddress.queryElementAt(0,Ci.nsISupportsCString).data,
+                             OFFER_ADDRESS,
+                             'expected offer address array');
+          Assert.equal(offer.tcpPort, OFFER_PORT, 'expected offer port');
+          try {
+            let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+            let answer = new TestDescription(tcpType, [ANSWER_ADDRESS], ANSWER_PORT);
+            presenterControlChannel.sendAnswer(answer);
+          } catch (e) {
+            Assert.ok(false, 'sending answer fails' + e);
+          }
+        },
+        onAnswer: function(aAnswer) {
+          Assert.ok(false, 'get answer');
+        },
+        onIceCandidate: function(aCandidate) {
+          Assert.ok(true, '3. presenterControlChannel: get ice candidate, close channel');
+          let recvCandidate = JSON.parse(aCandidate);
+          for (let key in recvCandidate) {
+            if (typeof(recvCandidate[key]) !== "function") {
+              Assert.equal(recvCandidate[key], candidate[key], "key " + key + " should match.");
+            }
+          }
+          presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+        },
+        notifyConnected: function() {
+          Assert.equal(this.status, 'created', '0. presenterControlChannel: opened');
+          this.status = 'opened';
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(this.status, 'onOffer', '4. presenterControlChannel: closed');
+          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'presenterControlChannel notify closed');
+          this.status = 'closed';
+          yayFuncs.controllerControlChannelClose();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    onReconnectRequest: function(deviceInfo, url, presentationId, controlChannel) {
+      Assert.equal(url, 'http://example.com', 'expected url');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      yayFuncs.presenterControlChannelReconnect();
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    certFingerprint: pcs.certFingerprint,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let controllerControlChannel = pcs.connect(presenterDeviceInfo);
+
+  controllerControlChannel.listener = {
+    status: 'created',
+    onOffer: function(offer) {
+      Assert.ok(false, 'get offer');
+    },
+    onAnswer: function(aAnswer) {
+      Assert.equal(this.status, 'opened', '2. controllerControlChannel: get answer, send ICE candidate');
+
+      let answer = aAnswer.QueryInterface(Ci.nsIPresentationChannelDescription);
+      Assert.strictEqual(answer.tcpAddress.queryElementAt(0,Ci.nsISupportsCString).data,
+                         ANSWER_ADDRESS,
+                         'expected answer address array');
+      Assert.equal(answer.tcpPort, ANSWER_PORT, 'expected answer port');
+      candidate = {
+        candidate: "1 1 UDP 1 127.0.0.1 34567 type host",
+        sdpMid: "helloworld",
+        sdpMLineIndex: 1
+      };
+      controllerControlChannel.sendIceCandidate(JSON.stringify(candidate));
+    },
+    onIceCandidate: function(aCandidate) {
+      Assert.ok(false, 'get ICE candidate');
+    },
+    notifyConnected: function() {
+      Assert.equal(this.status, 'created', '0. controllerControlChannel: opened, send offer');
+      controllerControlChannel.launch('testPresentationId', 'http://example.com');
+      this.status = 'opened';
+      try {
+        let tcpType = Ci.nsIPresentationChannelDescription.TYPE_TCP;
+        let offer = new TestDescription(tcpType, [OFFER_ADDRESS], OFFER_PORT)
+        controllerControlChannel.sendOffer(offer);
+      } catch (e) {
+        Assert.ok(false, 'sending offer fails:' + e);
+      }
+    },
+    notifyDisconnected: function(aReason) {
+      this.status = 'closed';
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. controllerControlChannel notify closed');
+      yayFuncs.presenterControlChannelClose();
+
+      let reconnectControllerControlChannel = pcs.connect(presenterDeviceInfo);
+      reconnectControllerControlChannel.listener = {
+        notifyConnected: function() {
+          reconnectControllerControlChannel.reconnect('testPresentationId', 'http://example.com');
+        },
+        notifyReconnected: function() {
+          yayFuncs.controllerControlChannelReconnect();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequest() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected',
+                                   'terminatedByController',
+                                   'terminatedByReceiver']);
+  let controllerControlChannel;
+  let terminatePhase = 'controller';
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      controlChannel.terminate(presentationId); // Reply terminate ack.
+
+      if (terminatePhase === 'controller') {
+        controllerControlChannel = controlChannel;
+        Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+        Assert.equal(isFromReceiver, false, 'expected request from controller');
+        yayFuncs.terminatedByController();
+
+        controllerControlChannel.listener = {
+          notifyConnected: function() {
+            Assert.ok(true, 'control channel notify connected');
+            yayFuncs.controllerControlChannelConnected();
+
+            terminatePhase = 'receiver';
+            controllerControlChannel.terminate('testPresentationId');
+          },
+          notifyDisconnected: function(aReason) {
+            Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+            yayFuncs.controllerControlChannelDisconnected();
+          },
+          QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+        };
+      } else {
+        Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id');
+        Assert.equal(isFromReceiver, true, 'expected request from receiver');
+        yayFuncs.terminatedByReceiver();
+        presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+      }
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    certFingerprint: pcs.certFingerprint,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequestAbnormal() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected']);
+  let controllerControlChannel;
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      Assert.equal(isFromReceiver, false, 'expected request from controller');
+      controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack.
+
+      controllerControlChannel = controlChannel;
+
+      controllerControlChannel.listener = {
+          notifyConnected: function() {
+          Assert.ok(true, 'control channel notify connected');
+          yayFuncs.controllerControlChannelConnected();
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error');
+          yayFuncs.controllerControlChannelDisconnected();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    certFingerprint: pcs.certFingerprint,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function setOffline() {
+  pcs.listener = {
+    onServerReady: function(aPort, aCertFingerprint) {
+      Assert.notEqual(aPort, 0, 'TCPPresentationServer port changed and the port should be valid');
+      pcs.close();
+      run_next_test();
+    },
+  };
+
+  // Let the server socket restart automatically.
+  Services.io.offline = true;
+  Services.io.offline = false;
+}
+
+function oneMoreLoop() {
+  try {
+    pcs.listener = {
+      onServerReady: function() {
+        testPresentationServer();
+      }
+    };
+
+    // Second run with TLS disabled.
+    pcs.startServer(false, PRESENTER_CONTROL_CHANNEL_PORT);
+  } catch (e) {
+    Assert.ok(false, 'TCP presentation init fail:' + e);
+    run_next_test();
+  }
+}
+
+
+function shutdown()
+{
+  pcs.listener = {
+    onServerReady: function(aPort, aCertFingerprint) {
+      Assert.ok(false, 'TCPPresentationServer port changed');
+    },
+  };
+  pcs.close();
+  Assert.equal(pcs.port, 0, "TCPPresentationServer closed");
+  run_next_test();
+}
+
+// Test manually close control channel with NS_ERROR_FAILURE
+function changeCloseReason() {
+  CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_ERROR_FAILURE;
+  run_next_test();
+}
+
+add_test(loopOfferAnser);
+add_test(terminateRequest);
+add_test(terminateRequestAbnormal);
+add_test(setOffline);
+add_test(changeCloseReason);
+add_test(oneMoreLoop);
+add_test(shutdown);
+
+function run_test() {
+  // Need profile dir to store the key / cert
+  do_get_profile();
+  // Ensure PSM is initialized
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+  Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
+
+  do_register_cleanup(() => {
+    Services.prefs.clearUserPref("dom.presentation.tcp_server.debug");
+  });
+
+  run_next_test();
+}
diff --git a/dom/presentation/tests/xpcshell/xpcshell.ini b/dom/presentation/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..8a9c305a0
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+tail =
+
+[test_multicast_dns_device_provider.js]
+[test_presentation_device_manager.js]
+[test_presentation_session_transport.js]
+[test_tcp_control_channel.js]
+[test_presentation_state_machine.js]
-- 
cgit v1.2.3