summaryrefslogtreecommitdiffstats
path: root/dom/presentation/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js150
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript.js553
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js412
-rw-r--r--dom/presentation/tests/mochitest/PresentationSessionFrameScript.js291
-rw-r--r--dom/presentation/tests/mochitest/chrome.ini13
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html216
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html95
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html10
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html158
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_non_receiver.html41
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html26
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver.html139
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html60
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html79
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html26
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_reconnect.html100
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html113
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_terminate.html104
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html114
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test1
-rw-r--r--dom/presentation/tests/mochitest/file_presentation_unknown_content_type.test^headers^1
-rw-r--r--dom/presentation/tests/mochitest/mochitest.ini81
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js241
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js522
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_availability.html35
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_availability_iframe.html227
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html243
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver.html138
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html209
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_dc_sender.html289
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html143
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html80
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js91
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_reconnect.html378
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html75
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html187
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html173
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html130
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html103
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html76
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js116
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html16
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html16
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html171
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender.html258
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html151
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html160
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html514
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate.js325
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js266
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html18
-rw-r--r--dom/presentation/tests/mochitest/test_presentation_terminate_oop.html18
-rw-r--r--dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js1465
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_device_manager.js288
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_session_transport.js225
-rw-r--r--dom/presentation/tests/xpcshell/test_presentation_state_machine.js376
-rw-r--r--dom/presentation/tests/xpcshell/test_tcp_control_channel.js523
-rw-r--r--dom/presentation/tests/xpcshell/xpcshell.ini9
66 files changed, 11182 insertions, 0 deletions
diff --git a/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js b/dom/presentation/tests/mochitest/PresentationDeviceInfoChromeScript.js
new file mode 100644
index 0000000000..83f8965b03
--- /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";
+
+/* eslint-env mozilla/frame-script */
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const manager = Cc["@mozilla.org/presentation-device/manager;1"].getService(
+ Ci.nsIPresentationDeviceManager
+);
+
+var testProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDeviceProvider"]),
+ forceDiscovery() {
+ sendAsyncMessage("force-discovery");
+ },
+ listener: null,
+};
+
+var testDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ establishControlChannel() {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+ id: null,
+ name: null,
+ type: null,
+ listener: null,
+};
+
+var testDevice1 = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+var testDevice2 = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+var mockedDeviceWithoutSupportedURL = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return false;
+ },
+};
+
+var mockedDeviceSupportHttpsURL = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "dummyid",
+ name: "dummyName",
+ type: "dummyType",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ if (requestedUrl.includes("https://")) {
+ 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 0000000000..5e5041a870
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -0,0 +1,553 @@
+/* -*- 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";
+
+/* eslint-env mozilla/frame-script */
+
+const Cm = Components.manager;
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout } = ChromeUtils.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;
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return {
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory,
+ };
+}
+
+function registerOriginalFactory(
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory
+) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ // Passing null for the factory remaps the original CID to the
+ // contract ID.
+ registrar.registerFactory(originalClassId, "", contractId, null);
+ }
+}
+
+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);
+
+const mockedChannelDescription = {
+ QueryInterface: ChromeUtils.generateQI(["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: ChromeUtils.generateQI(["nsIServerSocket", "nsIFactory"]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ get port() {
+ return this._port;
+ },
+ set listener(listener) {
+ this._listener = listener;
+ },
+ init(port, loopbackOnly, backLog) {
+ if (port != -1) {
+ this._port = port;
+ } else {
+ this._port = 5678;
+ }
+ },
+ asyncListen(listener) {
+ this._listener = listener;
+ },
+ close() {
+ this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED);
+ },
+ simulateOnSocketAccepted(serverSocket, socketTransport) {
+ this._listener.onSocketAccepted(serverSocket, socketTransport);
+ },
+};
+
+const mockedSocketTransport = Cc[
+ "@mozilla.org/presentation/mockedsockettransport;1"
+].createInstance(Ci.nsISocketTransport);
+
+const mockedControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ set listener(listener) {
+ this._listener = listener;
+ },
+ get listener() {
+ return this._listener;
+ },
+ sendOffer(offer) {
+ sendAsyncMessage("offer-sent", this._isValidSDP(offer));
+ },
+ sendAnswer(answer) {
+ sendAsyncMessage("answer-sent", this._isValidSDP(answer));
+
+ if (answer.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ }
+ },
+ _isValidSDP(aSDP) {
+ var isValid = false;
+ if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_TCP) {
+ try {
+ var sdpAddresses = aSDP.tcpAddress;
+ if (sdpAddresses.length > 0) {
+ for (var i = 0; i < sdpAddresses.length; i++) {
+ // Ensure CString addresses are used. Otherwise, an error will be thrown.
+ sdpAddresses.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(presentationId, url) {
+ sessionId = presentationId;
+ },
+ terminate(presentationId) {
+ sendAsyncMessage("sender-terminate", presentationId);
+ },
+ reconnect(presentationId, url) {
+ sendAsyncMessage("start-reconnect", url);
+ },
+ notifyReconnected() {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ disconnect(reason) {
+ sendAsyncMessage("control-channel-closed", reason);
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ },
+ simulateReceiverReady() {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReceiverReady();
+ },
+ simulateOnOffer() {
+ sendAsyncMessage("offer-received");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onOffer(mockedChannelDescription);
+ },
+ simulateOnAnswer() {
+ sendAsyncMessage("answer-received");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onAnswer(mockedChannelDescription);
+ },
+ simulateNotifyConnected() {
+ sendAsyncMessage("control-channel-opened");
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+};
+
+const mockedDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ sendAsyncMessage("control-channel-established");
+ return mockedControlChannel;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+const mockedDevicePrompt = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDevicePrompt",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection(request) {
+ this._request = request;
+ sendAsyncMessage("device-prompt");
+ },
+ simulateSelect() {
+ this._request.select(mockedDevice);
+ },
+ simulateCancel(result) {
+ this._request.cancel(result);
+ },
+};
+
+const mockedSessionTransport = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransport",
+ "nsIPresentationSessionTransportBuilder",
+ "nsIPresentationTCPSessionTransportBuilder",
+ "nsIPresentationDataChannelSessionTransportBuilder",
+ "nsIPresentationControlChannelListener",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.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(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(description, listener) {
+ this._listener = listener;
+ this._role = Ci.nsIPresentationService.ROLE_RECEIVER;
+
+ var tcpAddresses = description.QueryInterface(
+ Ci.nsIPresentationChannelDescription
+ ).tcpAddress;
+ this._selfAddress = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetAddr"]),
+ address:
+ tcpAddresses.length > 0
+ ? tcpAddresses.queryElementAt(0, Ci.nsISupportsCString).data
+ : "",
+ port: description.QueryInterface(Ci.nsIPresentationChannelDescription)
+ .tcpPort,
+ };
+
+ setTimeout(() => {
+ this._listener.onSessionTransport(this);
+ this._listener = null;
+ }, 0);
+ },
+ // in-process case
+ buildDataChannelTransport(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() {
+ sendAsyncMessage("data-transport-notification-enabled");
+ },
+ send(data) {
+ sendAsyncMessage("message-sent", data);
+ },
+ close(reason) {
+ // Don't send a message after tearDown, to avoid a leak.
+ if (this._callback) {
+ sendAsyncMessage("data-transport-closed", reason);
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportClosed(reason);
+ }
+ },
+ simulateTransportReady() {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ },
+ simulateIncomingMessage(message) {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyData(message, false);
+ },
+ onOffer(aOffer) {},
+ onAnswer(aAnswer) {},
+};
+
+const mockedNetworkInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkInfo"]),
+ getAddresses(ips, prefixLengths) {
+ ips.value = ["127.0.0.1"];
+ prefixLengths.value = [0];
+ return 1;
+ },
+};
+
+const mockedNetworkManager = {
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkManager", "nsIFactory"]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ get activeNetworkInfo() {
+ return mockedNetworkInfo;
+ },
+};
+
+var requestPromise = null;
+
+const mockedRequestUIGlue = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationRequestUIGlue",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest(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;
+});
+
+Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+
+ requestPromise = aSubject;
+}, "setup-request-promise");
diff --git a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
new file mode 100644
index 0000000000..fe5bb6d6be
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -0,0 +1,412 @@
+/* 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";
+
+/* eslint-env mozilla/frame-script */
+
+const Cm = Components.manager;
+
+const { Services } = ChromeUtils.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: ChromeUtils.generateQI(["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() {
+ // send offer after notifyConnected immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ notifyReconnected() {
+ // send offer after notifyOpened immediately
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyReconnected();
+ },
+ sendOffer(offer) {
+ Services.tm.dispatchToMainThread(() => {
+ mockControlChannelOfReceiver.onOffer(offer);
+ });
+ },
+ onAnswer(answer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onAnswer(answer);
+ },
+ launch(presentationId, url) {
+ sessionId = presentationId;
+ sendAsyncMessage("sender-launch", url);
+ },
+ disconnect(reason) {
+ if (!this._listener) {
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ mockControlChannelOfReceiver.disconnect();
+ },
+ terminate(presentationId) {
+ sendAsyncMessage("sender-terminate");
+ },
+ reconnect(presentationId, url) {
+ sendAsyncMessage("start-reconnect", url);
+ },
+ sendIceCandidate(candidate) {
+ mockControlChannelOfReceiver.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+// control channel of receiver
+const mockControlChannelOfReceiver = {
+ QueryInterface: ChromeUtils.generateQI(["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() {
+ // do nothing
+ if (!this._listener) {
+ this._pendingOpened = true;
+ return;
+ }
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyConnected();
+ },
+ onOffer(offer) {
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onOffer(offer);
+ },
+ sendAnswer(answer) {
+ Services.tm.dispatchToMainThread(() => {
+ mockControlChannelOfSender.onAnswer(answer);
+ });
+ },
+ disconnect(reason) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .notifyDisconnected(reason);
+ sendAsyncMessage("control-channel-receiver-closed", reason);
+ },
+ terminate(presentaionId) {},
+ sendIceCandidate(candidate) {
+ mockControlChannelOfSender.notifyIceCandidate(candidate);
+ },
+ notifyIceCandidate(candidate) {
+ if (!this._listener) {
+ return;
+ }
+
+ this._listener
+ .QueryInterface(Ci.nsIPresentationControlChannelListener)
+ .onIceCandidate(candidate);
+ },
+};
+
+const mockDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ if (triggerControlChannelError) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ sendAsyncMessage("control-channel-established");
+ return mockControlChannelOfSender;
+ },
+ disconnect() {
+ sendAsyncMessage("device-disconnected");
+ },
+ isRequestedUrlSupported(requestedUrl) {
+ return true;
+ },
+};
+
+const mockDevicePrompt = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDevicePrompt",
+ "nsIFactory",
+ ]),
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set request(request) {
+ this._request = request;
+ },
+ get request() {
+ return this._request;
+ },
+ promptDeviceSelection(request) {
+ this._request = request;
+ sendAsyncMessage("device-prompt");
+ },
+ simulateSelect() {
+ this._request.select(mockDevice);
+ },
+ simulateCancel() {
+ this._request.cancel();
+ },
+};
+
+const mockRequestUIGlue = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationRequestUIGlue",
+ "nsIFactory",
+ ]),
+ set promise(aPromise) {
+ this._promise = aPromise;
+ },
+ get promise() {
+ return this._promise;
+ },
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ sendRequest(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,
+ mockClassId,
+ mockFactory,
+ originalClassId,
+ originalFactory,
+ };
+ }
+ // Register mock factories.
+ 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);
+
+ Services.obs.addObserver(function setupRequestPromiseHandler(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ debug("Got observer: setup-request-promise");
+ Services.obs.removeObserver(setupRequestPromiseHandler, aTopic);
+ mockRequestUIGlue.promise = aSubject;
+ sendAsyncMessage("promise-setup-ready");
+ },
+ "setup-request-promise");
+}
+
+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 0000000000..5385e5d9b9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionFrameScript.js
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env mozilla/frame-script */
+
+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 });
+ } else {
+ port.postMessage({ type, 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 = {};
+ /* eslint-disable-next-line no-global-assign */
+ addMessageListener = function(message, handler) {
+ if (handlers.hasOwnProperty(message)) {
+ handlers[message].push(handler);
+ } else {
+ handlers[message] = [handler];
+ }
+ };
+ /* eslint-disable-next-line no-global-assign */
+ 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 Cm = Components.manager;
+
+ const mockedChannelDescription = {
+ /* eslint-disable-next-line mozilla/use-chromeutils-generateqi */
+ QueryInterface(iid) {
+ const interfaces = [Ci.nsIPresentationChannelDescription];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ return this;
+ },
+ get type() {
+ /* global Services */
+ 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 = {
+ /* eslint-disable-next-line mozilla/use-chromeutils-generateqi */
+ QueryInterface(iid) {
+ const interfaces = [
+ Ci.nsIPresentationSessionTransport,
+ Ci.nsIPresentationDataChannelSessionTransportBuilder,
+ Ci.nsIFactory,
+ ];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ return this;
+ },
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ return this.QueryInterface(aIID);
+ },
+ set callback(callback) {
+ this._callback = callback;
+ },
+ get callback() {
+ return this._callback;
+ },
+ /* OOP case */
+ buildDataChannelTransport(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() {
+ sendMessage("data-transport-notification-enabled");
+ },
+ send(data) {
+ sendMessage("message-sent", data);
+ },
+ close(reason) {
+ sendMessage("data-transport-closed", reason);
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportClosed(reason);
+ this._callback = null;
+ },
+ simulateTransportReady() {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyTransportReady();
+ },
+ simulateIncomingMessage(message) {
+ this._callback
+ .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+ .notifyData(message, false);
+ },
+ onOffer(aOffer) {
+ this._listener.sendAnswer(mockedChannelDescription);
+ this._onSessionTransport();
+ },
+ onAnswer(aAnswer) {
+ this._onSessionTransport();
+ },
+ _onSessionTransport() {
+ 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;
+ }
+ registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+ }
+
+ return {
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory,
+ };
+ }
+
+ function registerOriginalFactory(
+ contractId,
+ mockedClassId,
+ mockedFactory,
+ originalClassId,
+ originalFactory
+ ) {
+ if (originalFactory) {
+ var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(mockedClassId, mockedFactory);
+ registrar.registerFactory(originalClassId, "", contractId, null);
+ }
+ }
+
+ /* 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(message, handler) {
+ if (this.handlers.hasOwnProperty(message)) {
+ this.handlers[message].push(handler);
+ } else {
+ this.handlers[message] = [handler];
+ }
+ },
+ removeMessageListener(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(message, data) {
+ port.postMessage({ type: message, data });
+ },
+};
+
+if (!SpecialPowers.isMainProcess()) {
+ var port;
+ try {
+ port = SpecialPowers.loadPrivilegedScript(
+ loadPrivilegedScriptTest.toString()
+ );
+ } catch (e) {
+ ok(false, "loadPrivilegedScript should 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 0000000000..22ecc244ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/chrome.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ PresentationDeviceInfoChromeScript.js
+ PresentationSessionChromeScript.js
+
+[test_presentation_datachannel_sessiontransport.html]
+skip-if = os == 'android'
+[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 0000000000..3d98d12a2b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -0,0 +1,216 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+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 is_same_buffer(recv_data, expect_data) {
+ let recv_dataview = new Uint8Array(recv_data);
+ let expected_dataview = new Uint8Array(expect_data);
+
+ if (recv_dataview.length !== expected_dataview.length) {
+ return false;
+ }
+
+ for (let i = 0; i < recv_dataview.length; i++) {
+ if (recv_dataview[i] != expected_dataview[i]) {
+ info("discover byte differenct at " + i);
+ return false;
+ }
+ }
+ return true;
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+ is(navigator.presentation.defaultRequest, null, "Receiver: navigator.presentation.defaultRequest should be null.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingMessage ---");
+ connection.addEventListener("message", function(evt) {
+ let msg = evt.data;
+ is(msg, "msg-sender-to-receiver", "Receiver: Receiver should receive message from sender.");
+ command("forward-command", JSON.stringify({ name: "message-from-sender-received" }));
+ aResolve();
+ }, {once: true});
+ command("forward-command", JSON.stringify({ name: "trigger-message-from-sender" }));
+ });
+}
+
+function testSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "trigger-message-from-receiver") {
+ info("Receiver: --- testSendMessage ---");
+ connection.send("msg-receiver-to-sender");
+ }
+ if (message.type === "message-from-receiver-received") {
+ window.removeEventListener("hashchange", hashchangeHandler);
+ aResolve();
+ }
+ });
+ });
+}
+
+function testIncomingBlobMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingBlobMessage ---");
+ connection.send("testIncomingBlobMessage");
+ connection.addEventListener("message", function(evt) {
+ let recvData = String.fromCharCode.apply(null, new Uint8Array(evt.data));
+ is(recvData, "Hello World", "expected same string data");
+ aResolve();
+ }, {once: true});
+ });
+}
+
+function testConnectionClosed() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionClosed ---");
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Receiver: Connection should be closed.");
+ command("forward-command", JSON.stringify({ name: "receiver-closed" }));
+ aResolve();
+ };
+ command("forward-command", JSON.stringify({ name: "ready-to-close" }));
+ });
+}
+
+function testReconnectConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testReconnectConnection ---");
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "prepare-for-reconnect") {
+ command("forward-command", JSON.stringify({ name: "ready-to-reconnect" }));
+ }
+ });
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(true, "The connection is reconnected.");
+ aResolve();
+ };
+ });
+}
+
+function testIncomingArrayBuffer() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingArrayBuffer ---");
+ connection.binaryType = "blob";
+ connection.send("testIncomingArrayBuffer");
+ connection.addEventListener("message", function(evt) {
+ var fileReader = new FileReader();
+ fileReader.onload = function() {
+ ok(is_same_buffer(DATA_ARRAY_BUFFER, this.result), "expected same buffer data");
+ aResolve();
+ };
+ fileReader.readAsArrayBuffer(evt.data);
+ }, {once: true});
+ });
+}
+
+function testIncomingArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testIncomingArrayBufferView ---");
+ connection.binaryType = "arraybuffer";
+ connection.send("testIncomingArrayBufferView");
+ connection.addEventListener("message", function(evt) {
+ ok(is_same_buffer(evt.data, TYPED_DATA_ARRAY), "expected same buffer data");
+ aResolve();
+ }, {once: true});
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testIncomingMessage)
+ .then(testSendMessage)
+ .then(testIncomingBlobMessage)
+ .then(testConnectionClosed)
+ .then(testReconnectConnection)
+ .then(testIncomingArrayBuffer)
+ .then(testIncomingArrayBufferView)
+ .then(testConnectionClosed);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
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 0000000000..5198e76be8
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_wentaway.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionWentaway() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionWentaway ---\n");
+ command("forward-command", JSON.stringify({ name: "ready-to-remove-receiverFrame" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionWentaway);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
diff --git a/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html b/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html
new file mode 100644
index 0000000000..5fd1967331
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_fingerprinting_resistance_receiver.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+function testReceiver() {
+ alert(!!navigator.presentation.receiver);
+}
+
+testReceiver();
+window.addEventListener("hashchange", testReceiver);
+</script>
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 0000000000..d31b3578b2
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html
@@ -0,0 +1,158 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+function testGetAvailability() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ var request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.start().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.reconnect("dummyId").then(
+ function(aConnection) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForAboutBlank() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("about:blank");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForAboutSrcdoc() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("about:srcdoc");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testGetAvailabilityForDataURL() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("data:text/html,1");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(true, "Success due to a priori authenticated URL.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function runTest() {
+ testGetAvailability()
+ .then(testStartRequest)
+ .then(testReconnectRequest)
+ .then(testGetAvailabilityForAboutBlank)
+ .then(testGetAvailabilityForAboutSrcdoc)
+ .then(testGetAvailabilityForDataURL)
+ .then(finish);
+}
+
+window.addEventListener("message", function(evt) {
+ if (evt.data === "start") {
+ runTest();
+ }
+}, {once: true});
+
+window.setTimeout(function() {
+ command("ready-to-start");
+}, 3000);
+
+</script>
+</head>
+<body>
+</body>
+</html>
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 0000000000..75ffe6b4ad
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver on a non-receiver page at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver, null, "navigator.presentation.receiver shouldn't be available in non-receiving pages.");
+ aResolve();
+ });
+}
+
+testConnectionAvailable().
+then(finish);
+
+</script>
+</body>
+</html>
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 0000000000..c1b5c10610
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver on a non-receiver inner iframe of the receiver page at receiver side</title>
+</head>
+<body onload="testConnectionAvailable()">
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver, null, "navigator.presentation.receiver shouldn't be available in inner iframes with different origins from receiving pages.");
+ aResolve();
+ });
+}
+
+</script>
+</body>
+</html>
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 0000000000..b864fb7c56
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(msg) {
+ alert("COMMAND " + JSON.stringify(msg));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available in receiving pages.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in receiving pages.");
+
+ navigator.presentation.receiver.connectionList.then(
+ function(aList) {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ finish();
+ aReject();
+ }
+ );
+ command({ name: "trigger-incoming-offer" });
+ });
+}
+
+function testDefaultRequestIsUndefined() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.defaultRequest, undefined, "navigator.presentation.defaultRequest should not be available in receiving UA");
+ aResolve();
+ });
+}
+
+function testConnectionAvailableSameOriginInnerIframe() {
+ return new Promise(function(aResolve, aReject) {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "./file_presentation_receiver_inner_iframe.html");
+ document.body.appendChild(iframe);
+
+ aResolve();
+ });
+}
+
+function testConnectionUnavailableDiffOriginInnerIframe() {
+ return new Promise(function(aResolve, aReject) {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "http://example.com/tests/dom/presentation/tests/mochitest/file_presentation_non_receiver_inner_iframe.html");
+ document.body.appendChild(iframe);
+
+ aResolve();
+ });
+}
+
+function testConnectionListSameObject() {
+ return new Promise(function(aResolve, aReject) {
+ is(navigator.presentation.receiver.connectionList, navigator.presentation.receiver.connectionList, "The promise should be the same object.");
+ navigator.presentation.receiver.connectionList.then(
+ function(aList) {
+ is(connection, aList.connections[0], "The connection from list and the one from |connectionavailable| event should be the same.");
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ finish();
+ aReject();
+ }
+ );
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ command({ name: "trigger-incoming-message",
+ data: incomingMessage });
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+testConnectionAvailable().
+then(testDefaultRequestIsUndefined).
+then(testConnectionAvailableSameOriginInnerIframe).
+then(testConnectionUnavailableDiffOriginInnerIframe).
+then(testConnectionListSameObject).
+then(testIncomingMessage).
+then(testCloseConnection).
+then(finish);
+
+</script>
+</body>
+</html>
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 0000000000..6bd3da1f47
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_auxiliary_navigation.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for sandboxed auxiliary navigation flag in receiver page</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ alert((a === b ? "OK " : "KO ") + msg);
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(msg) {
+ alert("COMMAND " + JSON.stringify(msg));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available in OOP receiving pages.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in receiving pages.");
+
+ aResolve();
+ });
+}
+
+function testOpenWindow() {
+ return new Promise(function(aResolve, aReject) {
+ try {
+ window.open("http://example.com");
+ ok(false, "receiver page should not be able to open a new window.");
+ } catch (e) {
+ ok(true, "receiver page should not be able to open a new window.");
+ aResolve();
+ }
+ });
+}
+
+testConnectionAvailable().
+then(testOpenWindow).
+then(finish);
+
+</script>
+</body>
+</html>
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 0000000000..f6dd10797e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_establish_connection_error.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available.");
+ aResolve();
+ });
+}
+
+function testUnexpectedControlChannelClose() {
+ // Trigger the control channel to be closed with error code.
+ command({ name: "trigger-control-channel-close", data: 0x80004004 /* NS_ERROR_ABORT */ });
+
+ return new Promise(function(aResolve, aReject) {
+ return Promise.race([
+ navigator.presentation.receiver.connectionList.then(
+ (aList) => {
+ ok(false, "Should not get a connection list.");
+ aReject();
+ },
+ (aError) => {
+ ok(false, "Error occurred when getting the connection list: " + aError);
+ aReject();
+ }
+ ),
+ new Promise(
+ () => {
+ setTimeout(() => {
+ ok(true, "Not getting a conenction list.");
+ aResolve();
+ }, 3000);
+ }
+ ),
+ ]);
+ });
+}
+
+testConnectionAvailable().
+then(testUnexpectedControlChannelClose).
+then(finish);
+
+</script>
+</body>
+</html>
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 0000000000..dac1abac14
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_inner_iframe.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationReceiver in an inner iframe of the receiver page at receiver side</title>
+</head>
+<body onload="testConnectionAvailable()">
+<div id="content"></div>
+<script type="application/javascript">
+
+"use strict";
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation.receiver, "navigator.presentation.receiver should be available in same-origin inner iframes of receiving pages.");
+ aResolve();
+ });
+}
+
+</script>
+</body>
+</html>
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 0000000000..31d6e0aa1c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_reconnect.html
@@ -0,0 +1,100 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+var request;
+var connection;
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ request = new PresentationRequest("http://example1.com");
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ command({ name: "notify-connection-closed", id: connection.id });
+ };
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ aReject();
+ }
+ );
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ if (connection.state === "closed") {
+ aResolve();
+ return;
+ }
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "The connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+window.addEventListener("message", function onMessage(evt) {
+ if (evt.data === "startConnection") {
+ testStartRequest().then(
+ function() {
+ command({ name: "connection-connected", id: connection.id });
+ }
+ );
+ } else if (evt.data === "closeConnection") {
+ testCloseConnection().then(
+ function() {
+ command({ name: "connection-closed", id: connection.id });
+ }
+ );
+ }
+});
+
+</script>
+</head>
+<body>
+</body>
+</html>
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 0000000000..162e4209e7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_sandboxed_presentation.html
@@ -0,0 +1,113 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test allow-presentation sandboxing flag</title>
+<script type="application/javascript">
+
+"use strict";
+
+function is(a, b, msg) {
+ window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
+}
+
+function ok(a, msg) {
+ window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+ window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(msg) {
+ window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
+}
+
+function finish() {
+ window.parent.postMessage("DONE", "*");
+}
+
+function testGetAvailability() {
+ return new Promise(function(aResolve, aReject) {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ var request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testStartRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.start().then(
+ function(aAvailability) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testDefaultRequest() {
+ return new Promise(function(aResolve, aReject) {
+ navigator.presentation.defaultRequest = new PresentationRequest("http://example.com");
+ is(navigator.presentation.defaultRequest, null, "DefaultRequest shoud be null.");
+ aResolve();
+ });
+}
+
+function testReconnectRequest() {
+ return new Promise(function(aResolve, aReject) {
+ var request = new PresentationRequest("http://example.com");
+
+ request.reconnect("dummyId").then(
+ function(aConnection) {
+ ok(false, "Unexpected success, should get a security error.");
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "SecurityError", "Should get a security error.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function runTest() {
+ testGetAvailability()
+ .then(testStartRequest)
+ .then(testDefaultRequest)
+ .then(testReconnectRequest)
+ .then(finish);
+}
+
+window.addEventListener("message", function(evt) {
+ if (evt.data === "start") {
+ runTest();
+ }
+}, {once: true});
+
+window.setTimeout(function() {
+ command("ready-to-start");
+}, 3000);
+
+</script>
+</head>
+<body>
+</body>
+</html>
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 0000000000..db5972f8d1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id='content'></div>
+<script type='application/javascript'>
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one conncetion.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionTerminate ---");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ // Using window.alert at this stage will cause window.close() fail.
+ // Only trigger it if verdict fail.
+ if (connection.state !== "terminated") {
+ is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+ }
+ aResolve();
+ };
+ command("forward-command", JSON.stringify({ name: "ready-to-terminate" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionTerminate);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
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 0000000000..3dfb692e88
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate_establish_connection_error.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for B2G PresentationReceiver at receiver side</title>
+ </head>
+ <body>
+ <div id='content'></div>
+<script type='application/javascript'>
+
+"use strict";
+
+function is(a, b, msg) {
+ if (a === b) {
+ alert("OK " + msg);
+ } else {
+ alert("KO " + msg + " | reason: " + a + " != " + b);
+ }
+}
+
+function ok(a, msg) {
+ alert((a ? "OK " : "KO ") + msg);
+}
+
+function info(msg) {
+ alert("INFO " + msg);
+}
+
+function command(name, data) {
+ alert("COMMAND " + JSON.stringify({name, data}));
+}
+
+function finish() {
+ alert("DONE");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionAvailable ---");
+ ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+ ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+
+ navigator.presentation.receiver.connectionList
+ .then((aList) => {
+ is(aList.connections.length, 1, "Should get one connection.");
+ connection = aList.connections[0];
+ ok(connection.id, "Connection ID should be set: " + connection.id);
+ is(connection.state, "connected", "Connection state at receiver side should be connected.");
+ aResolve();
+ })
+ .catch((aError) => {
+ ok(false, "Receiver: Error occurred when getting the connection: " + aError);
+ finish();
+ aReject();
+ });
+ });
+}
+
+function testConnectionReady() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionReady ---");
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ ok(false, "Should not get |onconnect| event.");
+ aReject();
+ };
+ if (connection.state === "connected") {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Receiver: Connection state should become connected.");
+ aResolve();
+ }
+ });
+}
+
+function testConnectionTerminate() {
+ return new Promise(function(aResolve, aReject) {
+ info("Receiver: --- testConnectionTerminate ---");
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ // Using window.alert at this stage will cause window.close() fail.
+ // Only trigger it if verdict fail.
+ if (connection.state !== "terminated") {
+ is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+ }
+ aResolve();
+ };
+
+ window.addEventListener("hashchange", function hashchangeHandler(evt) {
+ var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+ if (message.type === "ready-to-terminate") {
+ info("Receiver: --- ready-to-terminate ---");
+ connection.terminate();
+ }
+ });
+
+
+ command("forward-command", JSON.stringify({ name: "prepare-for-terminate" }));
+ });
+}
+
+function runTests() {
+ testConnectionAvailable()
+ .then(testConnectionReady)
+ .then(testConnectionTerminate);
+}
+
+runTests();
+
+</script>
+ </body>
+</html>
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 0000000000..8b13789179
--- /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 0000000000..fc044e3c49
--- /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 0000000000..3ff134cc4c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -0,0 +1,81 @@
+[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]
+skip-if = e10s # Bug 1656033
+[test_presentation_dc_receiver.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_dc_receiver_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_inproc.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_connection_wentaway_inproc.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_1ua_connection_wentaway_oop.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_sender_disconnect.html]
+skip-if = os == 'android'
+[test_presentation_tcp_sender_establish_connection_error.html]
+skip-if = os == 'android'
+[test_presentation_tcp_receiver_establish_connection_error.html]
+skip-if = (e10s || os == 'mac' || os == 'win') # Bug 1129785, Bug 1204709
+[test_presentation_tcp_receiver_establish_connection_timeout.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html]
+skip-if = e10s
+[test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html]
+skip-if = e10s
+[test_presentation_tcp_receiver.html]
+skip-if = e10s # Bug 1129785
+[test_presentation_tcp_receiver_oop.html]
+skip-if = e10s # 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
+[test_presentation_terminate_oop.html]
+skip-if = e10s
+[test_presentation_terminate_establish_connection_error_inproc.html]
+skip-if = e10s
+[test_presentation_terminate_establish_connection_error_oop.html]
+skip-if = e10s
+[test_presentation_sender_on_terminate_request.html]
+skip-if = os == '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]
+support-files = test_presentation_availability_iframe.html
+[test_presentation_fingerprinting_resistance.html]
+skip-if = e10s
+support-files = file_presentation_fingerprinting_resistance_receiver.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 0000000000..cff2280b72
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -0,0 +1,241 @@
+"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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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();
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ 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.ipc.tabs.disabled", false],
+ ["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 0000000000..a75507e6d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1258600">
+ Test for PresentationConnectionCloseEvent with wentaway reason</a>
+ <script type="application/javascript" src="test_presentation_1ua_connection_wentaway.js">
+ </script>
+ </body>
+</html>
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 0000000000..a75507e6d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1258600">
+ Test for PresentationConnectionCloseEvent with wentaway reason</a>
+ <script type="application/javascript" src="test_presentation_1ua_connection_wentaway.js">
+ </script>
+ </body>
+</html>
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 0000000000..73764e597b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.js
@@ -0,0 +1,522 @@
+/* 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.includes("_inproc");
+ 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
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ 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(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "msg-receiver-to-sender",
+ "Sender: Sender should receive message from Receiver"
+ );
+ postMessageToIframe("message-from-receiver-received");
+ aResolve();
+ },
+ { once: true }
+ );
+ postMessageToIframe("trigger-message-from-receiver");
+ });
+}
+
+function testSendBlobMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendBlobMessage ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ 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();
+ },
+ { once: true }
+ );
+ });
+}
+
+function testSendArrayBuffer() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendArrayBuffer ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "testIncomingArrayBuffer",
+ "Sender: Sender should receive message from Receiver"
+ );
+ connection.send(DATA_ARRAY_BUFFER);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function testSendArrayBufferView() {
+ return new Promise(function(aResolve, aReject) {
+ info("Sender: --- testSendArrayBufferView ---");
+ connection.addEventListener(
+ "message",
+ function(evt) {
+ let msg = evt.data;
+ is(
+ msg,
+ "testIncomingArrayBufferView",
+ "Sender: Sender should receive message from Receiver"
+ );
+ connection.send(TYPED_DATA_ARRAY);
+ aResolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+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],
+ ["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 0000000000..b57573fdd6
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1234492">
+ Test for B2G Presentation API when sender and receiver at the same side</a>
+ <script type="application/javascript" src="test_presentation_1ua_sender_and_receiver.js">
+ </script>
+ </body>
+</html>
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 0000000000..28004487d5
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side (OOP ver.)</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1234492">
+ Test for B2G Presentation API when sender and receiver at the same side (OOP ver.)</a>
+ <script type="application/javascript" src="test_presentation_1ua_sender_and_receiver.js">
+ </script>
+ </body>
+</html>
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 0000000000..7d7a348353
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_availability.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for PresentationAvailability</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1228508">Test PresentationAvailability</a>
+<script type="application/javascript">
+// This test loads in an iframe, to ensure that the navigator instance is
+// loaded with the correct value of the preference.
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("Test for guarantee not firing async event");
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({
+ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ () => {
+ let iframe = document.createElement("iframe");
+ iframe.src = "test_presentation_availability_iframe.html";
+ document.body.appendChild(iframe);
+ }
+ );
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html b/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html
new file mode 100644
index 0000000000..135b3b92bc
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_availability_iframe.html
@@ -0,0 +1,227 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for PresentationAvailability</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1228508">Test PresentationAvailability</a>
+<script type="application/javascript">
+let ok = window.parent.ok;
+let is = window.parent.is;
+let isnot = window.parent.isnot;
+let SimpleTest = window.parent.SimpleTest;
+let SpecialPowers = window.parent.SpecialPowers;
+
+"use strict";
+
+var testDevice = {
+ id: "id",
+ name: "name",
+ type: "type",
+};
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationDeviceInfoChromeScript.js"));
+var request;
+var availability;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("setup-complete", function() {
+ aResolve();
+ });
+ gScript.sendAsyncMessage("setup");
+ });
+}
+
+function testInitialUnavailable() {
+ request = new PresentationRequest("https://example.com");
+
+ return request.getAvailability().then(async function(aAvailability) {
+ is(aAvailability.value, false, "Should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ };
+ availability = aAvailability;
+ await gScript.sendQuery("trigger-device-add", testDevice);
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testInitialAvailable() {
+ let anotherRequest = new PresentationRequest("https://example.net");
+ return anotherRequest.getAvailability().then(function(aAvailability) {
+ is(aAvailability.value, true, "Should have available device initially");
+ isnot(aAvailability, availability, "Should get different availability object for different request URL");
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testSameObject() {
+ let sameUrlRequest = new PresentationRequest("https://example.com");
+ return sameUrlRequest.getAvailability().then(function(aAvailability) {
+ is(aAvailability, availability, "Should get same availability object for same request URL");
+ }).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testOnChangeEvent() {
+ return new Promise(function(aResolve, aReject) {
+ availability.onchange = function() {
+ availability.onchange = null;
+ is(availability.value, false, "Should have no available device after device removed");
+ aResolve();
+ };
+ gScript.sendAsyncMessage("trigger-device-remove");
+ });
+}
+
+function testConsecutiveGetAvailability() {
+ let presRequest = new PresentationRequest("https://example.org");
+ let firstAvailabilityResolved = false;
+ return Promise.all([
+ presRequest.getAvailability().then(function() {
+ firstAvailabilityResolved = true;
+ }),
+ presRequest.getAvailability().then(function() {
+ ok(firstAvailabilityResolved, "getAvailability() should be resolved in sequence");
+ }),
+ ]).catch(function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ });
+}
+
+function testUnsupportedDeviceAvailability() {
+ return Promise.race([
+ new Promise(function(aResolve, aReject) {
+ let presRequest = new PresentationRequest("https://test.com");
+ presRequest.getAvailability().then(function(aAvailability) {
+ availability = aAvailability;
+ aAvailability.onchange = function() {
+ availability.onchange = null;
+ ok(false, "Should not get onchange event.");
+ teardown();
+ };
+ });
+ gScript.sendAsyncMessage("trigger-add-unsupport-url-device");
+ }),
+ new Promise(function(aResolve, aReject) {
+ setTimeout(function() {
+ ok(true, "Should not get onchange event.");
+ availability.onchange = null;
+ gScript.sendAsyncMessage("trigger-remove-unsupported-device");
+ aResolve();
+ }, 3000);
+ }),
+ ]);
+}
+
+function testMultipleAvailabilityURLs() {
+ let request1 = new PresentationRequest(["https://example.com",
+ "https://example1.com"]);
+ let request2 = new PresentationRequest(["https://example1.com",
+ "https://example2.com"]);
+ return Promise.all([
+ request1.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ request2.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-add-multiple-devices");
+ aResolve();
+ }),
+ ]).then(new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-remove-multiple-devices");
+ aResolve();
+ }));
+}
+
+function testPartialSupportedDeviceAvailability() {
+ let request1 = new PresentationRequest(["https://supportedUrl.com"]);
+ let request2 = new PresentationRequest(["http://notSupportedUrl.com"]);
+
+ return Promise.all([
+ request1.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(true, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ Promise.race([
+ request2.getAvailability().then(function(aAvailability) {
+ return new Promise(function(aResolve) {
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(false, "Should get onchange event.");
+ aResolve();
+ };
+ });
+ }),
+ new Promise(function(aResolve) {
+ setTimeout(function() {
+ ok(true, "Should not get onchange event.");
+ availability.onchange = null;
+ aResolve();
+ }, 3000);
+ }),
+ ]),
+ new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-add-https-devices");
+ aResolve();
+ }),
+ ]).then(new Promise(function(aResolve) {
+ gScript.sendAsyncMessage("trigger-remove-https-devices");
+ aResolve();
+ }));
+}
+
+function teardown() {
+ request = null;
+ availability = null;
+ gScript.sendAsyncMessage("teardown");
+ gScript.destroy();
+ SimpleTest.finish();
+}
+
+ok(navigator.presentation, "navigator.presentation should be available.");
+testSetup().then(testInitialUnavailable)
+ .then(testInitialAvailable)
+ .then(testSameObject)
+ .then(testOnChangeEvent)
+ .then(testConsecutiveGetAvailability)
+ .then(testMultipleAvailabilityURLs)
+ .then(testUnsupportedDeviceAvailability)
+ .then(testPartialSupportedDeviceAvailability)
+ .then(teardown);
+
+</script>
+</body>
+</html>
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 0000000000..6c0fa8f4c7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_datachannel_sessiontransport.html
@@ -0,0 +1,243 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for data channel as session transport in Presentation API</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for data channel as session transport in Presentation API</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const loadingTimeoutPref = "presentation.receiver.loading.timeout";
+
+var clientBuilder;
+var serverBuilder;
+var clientTransport;
+var serverTransport;
+
+const clientMessage = "Client Message";
+const serverMessage = "Server Message";
+
+const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+var gResolve;
+var gReject;
+
+const clientCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportCallback"]),
+ notifyTransportReady() {
+ info("Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ info("Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData(aData) {
+ is(aData, serverMessage, "Client transport receives data.");
+ gResolve();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportCallback"]),
+ notifyTransportReady() {
+ info("Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ gResolve();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ info("Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ gResolve();
+ }
+ },
+ notifyData(aData) {
+ is(aData, clientMessage, "Server transport receives data.");
+ gResolve();
+ },
+};
+
+const clientListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportBuilderListener"]),
+ onSessionTransport(aTransport) {
+ info("Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+ },
+ onError(aError) {
+ ok(false, "client's builder reports error " + aError);
+ },
+ sendOffer(aOffer) {
+ setTimeout(() => this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer(aAnswer) {
+ setTimeout(() => this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate(aCandidate) {
+ setTimeout(() => this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect(aReason) {
+ setTimeout(() => this._localBuilder.notifyDisconnected(aReason), 0);
+ setTimeout(() => this._remoteBuilder.notifyDisconnected(aReason), 0);
+ },
+ set remoteBuilder(aRemoteBuilder) {
+ this._remoteBuilder = aRemoteBuilder;
+ },
+ set localBuilder(aLocalBuilder) {
+ this._localBuilder = aLocalBuilder;
+ },
+};
+
+const serverListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationSessionTransportBuilderListener"]),
+ onSessionTransport(aTransport) {
+ info("Server Transport is built.");
+ serverTransport = aTransport;
+ serverTransport.callback = serverCallback;
+ serverTransport.enableDataNotification();
+ },
+ onError(aError) {
+ ok(false, "server's builder reports error " + aError);
+ },
+ sendOffer(aOffer) {
+ setTimeout(() => this._remoteBuilder.onOffer(aOffer), 0);
+ },
+ sendAnswer(aAnswer) {
+ setTimeout(() => this._remoteBuilder.onAnswer(aAnswer), 0);
+ },
+ sendIceCandidate(aCandidate) {
+ setTimeout(() => this._remoteBuilder.onIceCandidate(aCandidate), 0);
+ },
+ disconnect(aReason) {
+ setTimeout(() => this._localBuilder.notifyDisconnected(aReason), 0);
+ setTimeout(() => this._remoteBuilder.notifyDisconnected(aReason), 0);
+ },
+ set remoteBuilder(aRemoteBuilder) {
+ this._remoteBuilder = aRemoteBuilder;
+ },
+ set localBuilder(aLocalBuilder) {
+ this._localBuilder = aLocalBuilder;
+ },
+};
+
+function testBuilder() {
+ return new Promise(function(aResolve, aReject) {
+ gResolve = aResolve;
+ gReject = aReject;
+
+ clientBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+ .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+ serverBuilder = Cc["@mozilla.org/presentation/datachanneltransportbuilder;1"]
+ .createInstance(Ci.nsIPresentationDataChannelSessionTransportBuilder);
+
+ clientListener.localBuilder = clientBuilder;
+ clientListener.remoteBuilder = serverBuilder;
+ serverListener.localBuilder = serverBuilder;
+ serverListener.remoteBuilder = clientBuilder;
+
+ clientBuilder
+ .buildDataChannelTransport(Ci.nsIPresentationService.ROLE_CONTROLLER,
+ window,
+ clientListener);
+
+ serverBuilder
+ .buildDataChannelTransport(Ci.nsIPresentationService.ROLE_RECEIVER,
+ window,
+ serverListener);
+ });
+}
+
+function testClientSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("client sends message");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ clientTransport.send(clientMessage);
+ });
+}
+
+function testServerSendMessage() {
+ return new Promise(function(aResolve, aReject) {
+ info("server sends message");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ serverTransport.send(serverMessage);
+ setTimeout(() => clientTransport.enableDataNotification(), 0);
+ });
+}
+
+function testCloseSessionTransport() {
+ return new Promise(function(aResolve, aReject) {
+ info("close session transport");
+ gResolve = aResolve;
+ gReject = aReject;
+
+ serverTransport.close(Cr.NS_OK);
+ });
+}
+
+function finish() {
+ info("test finished, teardown");
+ Services.prefs.clearUserPref(loadingTimeoutPref);
+
+ SimpleTest.finish();
+}
+
+function error(aError) {
+ ok(false, "report Error " + aError.name + ":" + aError.message);
+ gReject();
+}
+
+function runTests() {
+ Services.prefs.setIntPref(loadingTimeoutPref, 30000);
+
+ testBuilder()
+ .then(testClientSendMessage)
+ .then(testServerSendMessage)
+ .then(testCloseSessionTransport)
+ .then(finish)
+ .catch(error);
+}
+
+window.addEventListener("load", function() {
+ runTests();
+});
+
+</script>
+</pre>
+</body>
+</html>
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 0000000000..6ea64b2843
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for B2G PresentationConnection API at receiver side</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", receiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ iframe.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)) {
+ iframe.removeEventListener("mozbrowsershowmodalprompt",
+ receiverListener);
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aInnerResolve, aInnerReject) {
+ document.body.appendChild(iframe);
+
+ aInnerResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ gScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+
+ aResolve();
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ ok(navigator.presentation, "navigator.presentation should be available in in-process pages.");
+ is(navigator.presentation.receiver, null, "Non-receiving in-process pages shouldn't get a presentation receiver instance.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup().
+ then(testIncomingSessionRequest);
+}
+
+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", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..f828fc44f3
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_receiver_oop.html
@@ -0,0 +1,209 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side (OOP)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test B2G PresentationConnection API at receiver side (OOP)</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+var nonReceiverUrl = SimpleTest.getTestFileURL("file_presentation_non_receiver.html");
+
+var isReceiverFinished = false;
+var isNonReceiverFinished = false;
+
+var obs = SpecialPowers.Services.obs;
+var receiverIframe;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ // Create a receiver OOP iframe.
+ receiverIframe = document.createElement("iframe");
+ receiverIframe.setAttribute("remote", "true");
+ receiverIframe.setAttribute("mozbrowser", "true");
+ receiverIframe.setAttribute("mozpresentation", receiverUrl);
+ receiverIframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(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 /, ""));
+ if (command.name == "trigger-incoming-message") {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.sendAsyncMessage("trigger-incoming-message", {"data": command.data});
+ } else {
+ gScript.sendAsyncMessage(command.name, command.data);
+ }
+ } else if (/^DONE$/.exec(message)) {
+ ok(true, "Messaging from iframe complete.");
+ receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener);
+
+ isReceiverFinished = true;
+
+ if (isNonReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ var promise = new Promise(function(aInnerResolve, aInnerReject) {
+ document.body.appendChild(receiverIframe);
+ receiverIframe.addEventListener("mozbrowserloadstart", function() {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.loadFrameScript("data:,(" + loadPrivilegedScriptTest.toString() + ")();", false);
+ }, {once: true});
+
+ aInnerResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ // Create a non-receiver OOP iframe.
+ var nonReceiverIframe = document.createElement("iframe");
+ nonReceiverIframe.setAttribute("remote", "true");
+ nonReceiverIframe.setAttribute("mozbrowser", "true");
+ nonReceiverIframe.setAttribute("src", nonReceiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ nonReceiverIframe.addEventListener("mozbrowsershowmodalprompt", function nonReceiverListener(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.");
+ nonReceiverIframe.removeEventListener("mozbrowsershowmodalprompt", nonReceiverListener);
+
+ isNonReceiverFinished = true;
+
+ if (isReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ document.body.appendChild(nonReceiverIframe);
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ mm.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(SpecialPowers.wrap(aSuccess).data.data, "buildDataChannel get correct window object");
+ });
+
+ mm.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ mm.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ mm.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ mm.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(SpecialPowers.wrap(aReason).data.data, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+
+ aResolve();
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+var mmTeardownComplete = false;
+var gScriptTeardownComplete = false;
+function teardown() {
+ var mm = SpecialPowers.getBrowserFrameMessageManager(receiverIframe);
+ mm.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ mm.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ mmTeardownComplete = true;
+ if (gScriptTeardownComplete) {
+ SimpleTest.finish();
+ }
+ });
+
+ mm.sendAsyncMessage("teardown");
+
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ gScriptTeardownComplete = true;
+ if (mmTeardownComplete) {
+ SimpleTest.finish();
+ }
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup().
+ then(testIncomingSessionRequest);
+}
+
+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", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true],
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["presentation.receiver.loading.timeout", 5000000]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..40f8b337df
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_dc_sender.html
@@ -0,0 +1,289 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1148307">Test for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var frameScript = SpecialPowers.isMainProcess() ? gScript : contentScript;
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com/");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ frameScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ frameScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ });
+
+ frameScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ frameScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testSend() {
+ return new Promise(function(aResolve, aReject) {
+ const outgoingMessage = "test outgoing message";
+
+ frameScript.addMessageListener("message-sent", function messageSentHandler(aMessage) {
+ frameScript.removeMessageListener("message-sent", messageSentHandler);
+ is(aMessage, outgoingMessage, "The message is sent out.");
+ aResolve();
+ });
+
+ connection.send(outgoingMessage);
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ frameScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ frameScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ frameScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- 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) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "http://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ frameScript.removeMessageListener("check-navigator", checknavigatorHandler);
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ info("teardown-complete");
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function testConstructRequestError() {
+ return Promise.all([
+ // XXX: Bug 1305204 - uncomment when bug 1275746 is fixed again.
+ // new Promise(function(aResolve, aReject) {
+ // try {
+ // request = new PresentationRequest("\\\\\\");
+ // }
+ // catch(e) {
+ // is(e.name, "SyntaxError", "Expect to get SyntaxError.");
+ // aResolve();
+ // }
+ // }),
+ new Promise(function(aResolve, aReject) {
+ try {
+ request = new PresentationRequest([]);
+ } catch (e) {
+ is(e.name, "NotSupportedError", "Expect to get NotSupportedError.");
+ aResolve();
+ }
+ }),
+ ]);
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testSend).
+ then(testIncomingMessage).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(testConstructRequestError).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html b/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html
new file mode 100644
index 0000000000..28788b1252
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_fingerprinting_resistance.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+/* global SimpleTest SpecialPowers */
+
+const gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript1UA.js"));
+const kReceiverFile = "file_presentation_fingerprinting_resistance_receiver.html";
+const kReceiverUrl = SimpleTest.getTestFileURL(kReceiverFile);
+
+let runTests = async () => {
+ await setup();
+ let request = await createRequest();
+ let iframe = await testRequestAndReceiver(request);
+ await enableResistFingerprinting();
+ await testRequestResistFingerprinting(request);
+ await testReceiverResistFingerprinting(iframe);
+ teardown();
+};
+
+let setup = async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.presentation.test.stage", 0],
+ ],
+ });
+
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ 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("promise-setup-ready", function promiseSetupReadyHandler() {
+ gScript.removeMessageListener("promise-setup-ready", promiseSetupReadyHandler);
+ gScript.sendAsyncMessage("trigger-on-session-request", kReceiverUrl);
+ });
+};
+
+let createRequest = () => new Promise((resolve, reject) => {
+ let request = new PresentationRequest(kReceiverFile);
+ request.getAvailability().then((availability) => {
+ SimpleTest.ok(availability, "PresentationRequest.getAvailability");
+ availability.onchange = () => {
+ availability.onchange = null;
+ resolve(request);
+ };
+ gScript.sendAsyncMessage("trigger-device-add");
+ }).catch((error) => {
+ SimpleTest.ok(false, "PresentationRequest.getAvailability: " + error);
+ teardown();
+ reject(error);
+ });
+});
+
+let testRequestAndReceiver = (request) => new Promise((resolve, reject) => {
+ gScript.addMessageListener("sender-launch", function senderLaunchHandler(url) {
+ // SimpleTest.is(url, kReceiverUrl, 'sender-launch');
+ gScript.removeMessageListener("sender-launch", senderLaunchHandler);
+
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", kReceiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", kReceiverUrl);
+ iframe.setAttribute("remote", "false");
+ iframe.addEventListener("mozbrowsershowmodalprompt", (event) => {
+ SimpleTest.is(event.detail.message, "true", "navigator.presentation.receiver");
+ resolve(iframe);
+ }, {once: true});
+
+ let promise = new Promise((aInnerResolve) => {
+ document.body.appendChild(iframe);
+ aInnerResolve(iframe);
+ });
+
+ let obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ request.start().then((connection) => {
+ SimpleTest.ok(connection, "PresentationRequest.start");
+ }).catch((error) => {
+ SimpleTest.ok(false, "PresentationRequest.start: " + error);
+ teardown();
+ reject(error);
+ });
+});
+
+let enableResistFingerprinting = () => {
+ const kPref = "privacy.resistFingerprinting";
+ SimpleTest.info(kPref + " = true");
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ [kPref, true],
+ ],
+ });
+};
+
+let testRequestResistFingerprinting = (request) => {
+ return request.getAvailability()
+ .then(() => SimpleTest.ok(false, "PresentationRequest.getAvailability"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.getAvailability"))
+ .then(() => request.start())
+ .then(() => SimpleTest.ok(false, "PresentationRequest.start"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.start"))
+ .then(() => request.reconnect(kReceiverUrl))
+ .then(() => SimpleTest.ok(false, "PresentationRequest.reconnect"))
+ .catch((error) => SimpleTest.is(error.name, "SecurityError", "PresentationRequest.reconnect"));
+};
+
+let testReceiverResistFingerprinting = (iframe) => new Promise((resolve) => {
+ iframe.addEventListener("mozbrowsershowmodalprompt", (event) => {
+ SimpleTest.is(event.detail.message, "false", "navigator.presentation.receiver");
+ resolve();
+ }, {once: true});
+ iframe.setAttribute("src", kReceiverUrl + "#privacy.resistFingerprinting");
+});
+
+let teardown = () => {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+};
+
+SimpleTest.waitForExplicitFinish();
+document.addEventListener("DOMContentLoaded", () => {
+ SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+ {type: "browser", allow: true, context: document},
+ ], runTests);
+});
+</script>
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 0000000000..4ad4aa99ad
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_mixed_security_contexts.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268758">Test allow-presentation sandboxing flag</a>
+<iframe id="iframe" src="https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var readyToStart = false;
+var testSetuped = false;
+
+function setup() {
+ SpecialPowers.addPermission("presentation",
+ true, { url: "https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html",
+ originAttributes: {
+ inIsolatedMozBrowser: false }});
+
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ 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 /, ""));
+ if (command === "ready-to-start") {
+ readyToStart = true;
+ startTest();
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ testSetuped = true;
+ aResolve();
+ });
+}
+
+iframe.onload = startTest();
+
+function startTest() {
+ if (!(testSetuped && readyToStart)) {
+ return;
+ }
+ iframe.contentWindow.postMessage("start", "*");
+}
+
+function runTests() {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ setup().then(startTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..c6a23881e2
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation.js
@@ -0,0 +1,91 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_receiver_auxiliary_navigation.html"
+);
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+ var oop = !location.pathname.includes("_inproc");
+ 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();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+function teardown() {
+ gScript.addMessageListener(
+ "teardown-complete",
+ function teardownCompleteHandler() {
+ gScript.removeMessageListener(
+ "teardown-complete",
+ teardownCompleteHandler
+ );
+ gScript.destroy();
+ SimpleTest.finish();
+ }
+ );
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+}
+
+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],
+ ],
+ },
+ 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 0000000000..31be5be5ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268810">
+ Test for receiver page with sandboxed auxiliary navigation browsing context flag.</a>
+ <script type="application/javascript" src="test_presentation_receiver_auxiliary_navigation.js">
+ </script>
+ </body>
+</html>
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 0000000000..31be5be5ee
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_auxiliary_navigation_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API when sender and receiver at the same side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268810">
+ Test for receiver page with sandboxed auxiliary navigation browsing context flag.</a>
+ <script type="application/javascript" src="test_presentation_receiver_auxiliary_navigation.js">
+ </script>
+ </body>
+</html>
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 0000000000..6febdce57b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_reconnect.html
@@ -0,0 +1,378 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="PresentationSessionFrameScript.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197690">Test for Presentation API at sender side</a>
+<iframe id="iframe" src="file_presentation_reconnect.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var frameScript = SpecialPowers.isMainProcess() ? gScript : contentScript;
+var request;
+var connection;
+var commandHandler = {};
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ 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 /, ""));
+ if (command.name in commandHandler) {
+ commandHandler[command.name](command);
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ request = new PresentationRequest("http://example.com/");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ info("The control channel is closed. " + aReason);
+ });
+
+ frameScript.addMessageListener("check-navigator", function checknavigatorHandler(aSuccess) {
+ ok(aSuccess, "buildDataChannel get correct window object");
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ info("An answer is received.");
+ });
+
+ frameScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ info("Data transport channel is initialized.");
+ });
+
+ frameScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ frameScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ frameScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnectAConnectedConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectAConnectedConnection ---");
+ is(connection.state, "connected", "Make sure the state is connected.");
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ is(aConnection.id, connection.id, "Connection ID should be the same.");
+ is(aConnection.state, "connected", "The state should be connected.");
+ is(aConnection, connection, "The connection should be the same.");
+
+ aResolve();
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testReconnectInvalidID() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectInvalidID ---");
+
+ request.reconnect("dummyID").then(
+ function(aConnection) {
+ ok(false, "Unexpected success.");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Should get NotFoundError.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectInvalidURL() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectInvalidURL ---");
+
+ var request1 = new PresentationRequest("http://invalidURL");
+ request1.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(false, "Unexpected success.");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Should get NotFoundError.");
+ aResolve();
+ }
+ );
+ });
+}
+
+function testReconnectIframeConnectedConnection() {
+ info("--- testReconnectIframeConnectedConnection ---");
+ gScript.sendAsyncMessage("save-control-channel-listener");
+ return Promise.all([
+ new Promise(function(aResolve, aReject) {
+ commandHandler["connection-connected"] = function(command) {
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ var request1 = new PresentationRequest("http://example1.com");
+ request1.reconnect(command.id).then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The state should be connecting.");
+ aConnection.onclose = function() {
+ delete commandHandler["connection-connected"];
+ gScript.sendAsyncMessage("restore-control-channel-listener");
+ aResolve();
+ };
+ aConnection.close();
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ };
+ iframe.contentWindow.postMessage("startConnection", "*");
+ }),
+ new Promise(function(aResolve, aReject) {
+ commandHandler["notify-connection-closed"] = function(command) {
+ delete commandHandler["notify-connection-closed"];
+ aResolve();
+ };
+ }),
+ ]);
+}
+
+function testReconnectIframeClosedConnection() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnectIframeClosedConnection ---");
+ gScript.sendAsyncMessage("save-control-channel-listener");
+ commandHandler["connection-closed"] = function(command) {
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ var request1 = new PresentationRequest("http://example1.com");
+ request1.reconnect(command.id).then(
+ function(aConnection) {
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "The connection should be connected.");
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ ok(true, "The connection is closed.");
+ delete commandHandler["connection-closed"];
+ aResolve();
+ };
+ aConnection.close();
+ gScript.sendAsyncMessage("restore-control-channel-listener");
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ };
+ iframe.contentWindow.postMessage("closeConnection", "*");
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- testReconnect ---");
+ gScript.addMessageListener("start-reconnect", function startReconnectHandler(url) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "http://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+
+ const incomingMessage = "test incoming message";
+ aConnection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ frameScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ info("teardown-complete");
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testReconnectInvalidID).
+ then(testReconnectInvalidURL).
+ then(testReconnectAConnectedConnection).
+ then(testReconnectIframeConnectedConnection).
+ then(testReconnectIframeClosedConnection).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, 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", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..d5902d6223
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sandboxed_presentation.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268758">Test allow-presentation sandboxing flag</a>
+<iframe sandbox="allow-popups allow-scripts allow-same-origin" id="iframe" src="file_presentation_sandboxed_presentation.html"></iframe>
+<script type="application/javascript">
+
+"use strict";
+
+var iframe = document.getElementById("iframe");
+var readyToStart = false;
+var testSetuped = false;
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ addEventListener("message", function listener(event) {
+ var message = event.data;
+ 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 /, ""));
+ if (command === "ready-to-start") {
+ readyToStart = true;
+ startTest();
+ }
+ } else if (/^DONE$/.exec(message)) {
+ window.removeEventListener("message", listener);
+ SimpleTest.finish();
+ }
+ }, false);
+
+ testSetuped = true;
+ aResolve();
+ });
+}
+
+iframe.onload = startTest();
+
+function startTest() {
+ if (!(testSetuped && readyToStart)) {
+ return;
+ }
+ iframe.contentWindow.postMessage("start", "*");
+}
+
+function runTests() {
+ ok(navigator.presentation, "navigator.presentation should be available.");
+ setup().then(startTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..7da672a080
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test onTerminateRequest at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276378">Test onTerminateRequest at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testOnTerminateRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onterminate = function() {
+ connection.onterminate = null;
+ is(connection.state, "terminated", "Connection should be closed.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-incoming-terminate-request");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testOnTerminateRequest).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..6bcf379058
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_startWithDevice.html
@@ -0,0 +1,173 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test startWithDevice for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1239242">Test startWithDevice for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("https://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionWithDevice() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ ok(false, "Device prompt should not be triggered.");
+ teardown();
+ aReject();
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.startWithDevice("id").then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionWithDeviceNotFoundError() {
+ return new Promise(function(aResolve, aReject) {
+ request.startWithDevice("").then(
+ function(aConnection) {
+ ok(false, "Should not establish connection to an unknown device");
+ teardown();
+ aReject();
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "Expect NotFoundError occurred when establishing a connection");
+ aResolve();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnectionWithDevice).
+ then(testStartConnectionWithDeviceNotFoundError).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.test.enabled", true],
+ ["dom.presentation.test.stage", 0]]},
+ runTests);
+
+</script>
+</body>
+</html>
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 0000000000..52196d9913
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for B2G PresentationConnection API at receiver side</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+ 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();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ ok(navigator.presentation, "navigator.presentation should be available in in-process pages.");
+ is(navigator.presentation.receiver, null, "Non-receiving in-process pages shouldn't get a presentation receiver instance.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+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", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..1c553f2fa9
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_error.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing errors of B2G Presentation API at receiver side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver_establish_connection_error.html");
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", receiverUrl);
+ iframe.setAttribute("mozbrowser", "true");
+ iframe.setAttribute("mozpresentation", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ iframe.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)) {
+ iframe.removeEventListener("mozbrowsershowmodalprompt",
+ receiverListener);
+ teardown();
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(iframe);
+
+ aResolve(iframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, 0x80004004 /* NS_ERROR_ABORT */, "The control channel is closed abnormally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+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.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..067f9691d1
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_timeout.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing timeout of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing timeout of B2G Presentation API at receiver side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ var promise = new Promise(function(aResolve, aReject) {
+ // In order to trigger timeout, do not resolve the promise.
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+function testIncomingSessionRequestReceiverLaunchTimeout() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, 0x80530017 /* NS_ERROR_DOM_TIMEOUT_ERR */, "The control channel is closed due to timeout.");
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", "http://example.com");
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequestReceiverLaunchTimeout().
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["presentation.receiver.loading.timeout", 10]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..6521846a10
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
@@ -0,0 +1,116 @@
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("PresentationSessionChromeScript.js")
+);
+var receiverUrl = SimpleTest.getTestFileURL(
+ "file_presentation_unknown_content_type.test"
+);
+
+var obs = SpecialPowers.Services.obs;
+
+var receiverIframe;
+
+function setup() {
+ 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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+}
+
+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();
+
+ 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.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 0000000000..acf800e609
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_inproc.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for unknown content type of B2G Presentation API at receiver side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287717">Test for unknown content type of B2G Presentation API at receiver side</a>
+ <script type="application/javascript" src="test_presentation_tcp_receiver_establish_connection_unknown_content_type.js">
+ </script>
+</body>
+</html>
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 0000000000..c78b48b39d
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_establish_connection_unknown_content_type_oop.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for unknown content type of B2G Presentation API at receiver side (OOP)</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287717">Test for unknown content type of B2G Presentation API at receiver side (OOP)</a>
+ <script type="application/javascript" src="test_presentation_tcp_receiver_establish_connection_unknown_content_type.js">
+ </script>
+</body>
+</html>
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 0000000000..cc6d73745b
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_receiver_oop.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G PresentationConnection API at receiver side (OOP)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test B2G PresentationConnection API at receiver side (OOP)</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_receiver.html");
+var nonReceiverUrl = SimpleTest.getTestFileURL("file_presentation_non_receiver.html");
+
+var isReceiverFinished = false;
+var isNonReceiverFinished = false;
+
+var obs = SpecialPowers.Services.obs;
+
+function setup() {
+ gScript.sendAsyncMessage("trigger-device-add");
+
+ // Create a receiver OOP iframe.
+ var receiverIframe = document.createElement("iframe");
+ receiverIframe.setAttribute("remote", "true");
+ receiverIframe.setAttribute("mozbrowser", "true");
+ receiverIframe.setAttribute("mozpresentation", receiverUrl);
+ receiverIframe.setAttribute("src", receiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ receiverIframe.addEventListener("mozbrowsershowmodalprompt", function receiverListener(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.");
+ receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener);
+
+ isReceiverFinished = true;
+
+ if (isNonReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+
+ aResolve(receiverIframe);
+ });
+ obs.notifyObservers(promise, "setup-request-promise");
+
+ // Create a non-receiver OOP iframe.
+ var nonReceiverIframe = document.createElement("iframe");
+ nonReceiverIframe.setAttribute("remote", "true");
+ nonReceiverIframe.setAttribute("mozbrowser", "true");
+ nonReceiverIframe.setAttribute("src", nonReceiverUrl);
+
+ // This event is triggered when the iframe calls "alert".
+ nonReceiverIframe.addEventListener("mozbrowsershowmodalprompt", function nonReceiverListener(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.");
+ nonReceiverIframe.removeEventListener("mozbrowsershowmodalprompt", nonReceiverListener);
+
+ isNonReceiverFinished = true;
+
+ if (isReceiverFinished) {
+ teardown();
+ }
+ }
+ });
+
+ document.body.appendChild(nonReceiverIframe);
+
+ gScript.addMessageListener("offer-received", function offerReceivedHandler() {
+ gScript.removeMessageListener("offer-received", offerReceivedHandler);
+ info("An offer is received.");
+ });
+
+ gScript.addMessageListener("answer-sent", function answerSentHandler(aIsValid) {
+ gScript.removeMessageListener("answer-sent", answerSentHandler);
+ ok(aIsValid, "A valid answer is sent.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed normally.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The data transport should be closed normally.");
+ });
+}
+
+function testIncomingSessionRequest() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("receiver-launching", function launchReceiverHandler(aSessionId) {
+ gScript.removeMessageListener("receiver-launching", launchReceiverHandler);
+ info("Trying to launch receiver page.");
+
+ aResolve();
+ });
+
+ gScript.sendAsyncMessage("trigger-incoming-session-request", receiverUrl);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ setup();
+ testIncomingSessionRequest();
+}
+
+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", false],
+ ["dom.presentation.receiver.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false],
+ ["dom.ipc.browser_frames.oop_by_default", true]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..e03c43c54c
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender.html
@@ -0,0 +1,258 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("https://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ var connectionFromEvent;
+ request.onconnectionavailable = function(aEvent) {
+ request.onconnectionavailable = null;
+ connectionFromEvent = aEvent.connection;
+ ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+ if (connection) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ };
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ is(connection.state, "connecting", "The initial state should be connecting.");
+
+ if (connectionFromEvent) {
+ is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+ }
+ connection.onconnect = function() {
+ connection.onconnect = null;
+ is(connection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testSend() {
+ return new Promise(function(aResolve, aReject) {
+ const outgoingMessage = "test outgoing message";
+
+ gScript.addMessageListener("message-sent", function messageSentHandler(aMessage) {
+ gScript.removeMessageListener("message-sent", messageSentHandler);
+ is(aMessage, outgoingMessage, "The message is sent out.");
+ aResolve();
+ });
+
+ connection.send(outgoingMessage);
+ });
+}
+
+function testIncomingMessage() {
+ return new Promise(function(aResolve, aReject) {
+ const incomingMessage = "test incoming message";
+
+ connection.addEventListener("message", function(aEvent) {
+ is(aEvent.data, incomingMessage, "An incoming message should be received.");
+ aResolve();
+ }, {once: true});
+
+ gScript.sendAsyncMessage("trigger-incoming-message", incomingMessage);
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function testReconnect() {
+ return new Promise(function(aResolve, aReject) {
+ info("--- 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) {
+ gScript.removeMessageListener("start-reconnect", startReconnectHandler);
+ is(url, "https://example.com/", "URLs should be the same.");
+ gScript.sendAsyncMessage("trigger-reconnected-acked", url);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler() {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ request.reconnect(connection.id).then(
+ function(aConnection) {
+ ok(aConnection, "Connection should be available.");
+ ok(aConnection.id, "Connection ID should be set.");
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ is(aConnection, connection, "The reconnected connection should be the same.");
+
+ aConnection.onconnect = function() {
+ aConnection.onconnect = null;
+ is(aConnection.state, "connected", "Connection should be connected.");
+ aResolve();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testSend).
+ then(testIncomingMessage).
+ then(testCloseConnection).
+ then(testReconnect).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+
+</script>
+</body>
+</html>
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 0000000000..a4b4b1ffc7
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_default_request.html
@@ -0,0 +1,151 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test default request for B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test default request for B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ navigator.presentation.defaultRequest = new PresentationRequest("https://example.com");
+
+ navigator.presentation.defaultRequest.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler() {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ info("An offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ is(navigator.presentation.receiver, undefined, "Sender shouldn't get a presentation receiver instance.");
+
+ navigator.presentation.defaultRequest.onconnectionavailable = function(aEvent) {
+ navigator.presentation.defaultRequest.onconnectionavailable = null;
+ connection = aEvent.connection;
+ ok(connection, "|connectionavailable| event is fired with a connection.");
+ ok(connection.id, "Connection ID should be set.");
+ 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();
+ };
+ };
+
+ // Simulate the UA triggers |start()| of the default request.
+ navigator.presentation.defaultRequest.start();
+ });
+}
+
+function testCloseConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ connection.close();
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+ ok(navigator.presentation, "navigator.presentation should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testCloseConnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.receiver.enabled", false],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+
+</script>
+</body>
+</html>
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 0000000000..d08e376999
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_disconnect.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for disconnection of B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for disconnection of B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+var connection;
+
+function testSetup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ });
+
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ });
+
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ });
+
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ });
+
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-answer");
+ });
+
+ gScript.addMessageListener("answer-received", function answerReceivedHandler() {
+ gScript.removeMessageListener("answer-received", answerReceivedHandler);
+ info("An answer is received.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ });
+
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ });
+
+ gScript.addMessageListener("data-transport-notification-enabled", function dataTransportNotificationEnabledHandler() {
+ gScript.removeMessageListener("data-transport-notification-enabled", dataTransportNotificationEnabledHandler);
+ info("Data notification is enabled for data transport channel.");
+ });
+
+ request.start().then(
+ function(aConnection) {
+ connection = aConnection;
+ ok(connection, "Connection should be available.");
+ ok(connection.id, "Connection ID should be set.");
+ 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();
+ };
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testDisconnection() {
+ return new Promise(function(aResolve, aReject) {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ });
+
+ connection.onclose = function() {
+ connection.onclose = null;
+ is(connection.state, "closed", "Connection should be closed.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-data-transport-close", SpecialPowers.Cr.NS_ERROR_FAILURE);
+ });
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ testSetup().
+ then(testStartConnection).
+ then(testDisconnection).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..af3fa93024
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_tcp_sender_establish_connection_error.html
@@ -0,0 +1,514 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+ <meta charset="utf-8">
+ <title>Test for connection establishing errors of B2G Presentation API at sender side</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1069230">Test for connection establishing errors of B2G Presentation API at sender side</a>
+<script type="application/javascript">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScript.js"));
+var request;
+
+function setup() {
+ return new Promise(function(aResolve, aReject) {
+ request = new PresentationRequest("http://example.com");
+
+ request.getAvailability().then(
+ function(aAvailability) {
+ is(aAvailability.value, false, "Sender: should have no available device after setup");
+ aAvailability.onchange = function() {
+ aAvailability.onchange = null;
+ ok(aAvailability.value, "Device should be available.");
+ aResolve();
+ };
+
+ gScript.sendAsyncMessage("trigger-device-add");
+ },
+ function(aError) {
+ ok(false, "Error occurred when getting availability: " + aError);
+ teardown();
+ aReject();
+ }
+ );
+ });
+}
+
+function testStartConnectionCancelPrompt() {
+ info("--- testStartConnectionCancelPrompt ---");
+ return Promise.all([
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-cancel", SpecialPowers.Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ resolve();
+ });
+ }),
+ request.start().then(
+ function(aConnection) {
+ ok(false, "|start| shouldn't succeed in this case.");
+ },
+ function(aError) {
+ is(aError.name, "NotAllowedError", "NotAllowedError is expected when the prompt is canceled.");
+ }
+ ),
+ ]);
+}
+
+function testStartConnectionNoDevice() {
+ info("--- testStartConnectionNoDevice ---");
+ return Promise.all([
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-cancel", SpecialPowers.Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ resolve();
+ });
+ }),
+ request.start().then(
+ function(aConnection) {
+ ok(false, "|start| shouldn't succeed in this case.");
+ },
+ function(aError) {
+ is(aError.name, "NotFoundError", "NotFoundError is expected when no available device.");
+ }
+ ),
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_ERROR_FAILURE, "The control channel is closed with NS_ERROR_FAILURE");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_ERROR_FAILURE);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed with NS_OK");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_OK);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ is(aReason, SpecialPowers.Cr.NS_ERROR_ABORT, "The control channel is closed with NS_ERROR_ABORT");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_ERROR_ABORT);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady() {
+ info("--- testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady -- ");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ is(aReason, SpecialPowers.Cr.NS_OK, "The control channel is closed with NS_OK");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-control-channel-close", SpecialPowers.Cr.NS_OK);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function testStartConnectionUnexpectedDataTransportClose() {
+ info("--- testStartConnectionUnexpectedDataTransportClose ---");
+ return Promise.all([
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+ gScript.removeMessageListener("device-prompt", devicePromptHandler);
+ info("Device prompt is triggered.");
+ gScript.sendAsyncMessage("trigger-device-prompt-select");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+ gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+ info("A control channel is established.");
+ gScript.sendAsyncMessage("trigger-control-channel-open");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-opened", function controlChannelOpenedHandler() {
+ gScript.removeMessageListener("control-channel-opened", controlChannelOpenedHandler);
+ info("The control channel is opened.");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("control-channel-closed", function controlChannelClosedHandler(aReason) {
+ gScript.removeMessageListener("control-channel-closed", controlChannelClosedHandler);
+ info("The control channel is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("offer-sent", function offerSentHandler(aIsValid) {
+ gScript.removeMessageListener("offer-sent", offerSentHandler);
+ ok(aIsValid, "A valid offer is sent out.");
+ info("recv offer-sent.");
+ gScript.sendAsyncMessage("trigger-incoming-transport");
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-initialized", function dataTransportInitializedHandler() {
+ gScript.removeMessageListener("data-transport-initialized", dataTransportInitializedHandler);
+ info("Data transport channel is initialized.");
+ gScript.sendAsyncMessage("trigger-data-transport-close", SpecialPowers.Cr.NS_ERROR_UNEXPECTED);
+ resolve();
+ });
+ }),
+
+ new Promise((resolve) => {
+ gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+ gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+ info("The data transport is closed. " + aReason);
+ resolve();
+ });
+ }),
+
+ request.start().then(
+ function(aConnection) {
+ is(aConnection.state, "connecting", "The initial state should be connecting.");
+ return new Promise((resolve) => {
+ aConnection.onclose = function() {
+ aConnection.onclose = null;
+ is(aConnection.state, "closed", "Connection should be closed.");
+ resolve();
+ };
+ });
+ },
+ function(aError) {
+ ok(false, "Error occurred when establishing a connection: " + aError);
+ teardown();
+ }
+ ),
+
+ ]);
+}
+
+function teardown() {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ SimpleTest.finish();
+ });
+
+ gScript.sendAsyncMessage("teardown");
+}
+
+function runTests() {
+ ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+ setup().
+ then(testStartConnectionCancelPrompt).
+ then(testStartConnectionNoDevice).
+ then(testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportInit).
+ then(testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportInit).
+ then(testStartConnectionUnexpectedControlChannelCloseBeforeDataTransportReady).
+ then(testStartConnectionUnexpectedControlChannelCloseNoReasonBeforeDataTransportReady).
+ then(testStartConnectionUnexpectedDataTransportClose).
+ then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+ {type: "presentation-device-manage", allow: false, context: document},
+], function() {
+ SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+ ["dom.presentation.controller.enabled", true],
+ ["dom.presentation.session_transport.data_channel.enable", false]]},
+ runTests);
+});
+
+</script>
+</body>
+</html>
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 0000000000..ed43986534
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,325 @@
+"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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ 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.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 0000000000..7a541911f3
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error.js
@@ -0,0 +1,266 @@
+"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.includes("_inproc");
+ receiverIframe.setAttribute("remote", oop);
+
+ receiverIframe.setAttribute("src", receiverUrl);
+ receiverIframe.addEventListener(
+ "mozbrowserloadend",
+ function() {
+ info("Receiver loaded.");
+ },
+ { once: true }
+ );
+
+ // 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
+ );
+ }
+ }
+ );
+
+ var promise = new Promise(function(aResolve, aReject) {
+ document.body.appendChild(receiverIframe);
+ aResolve(receiverIframe);
+ });
+
+ var obs = SpecialPowers.Services.obs;
+ obs.notifyObservers(promise, "setup-request-promise");
+ });
+
+ 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.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 0000000000..9d702c2e1e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+ Test for constrol channel establish error during PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate_establish_connection_error.js'>
+ </script>
+ </body>
+</html>
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 0000000000..9d702c2e1e
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_establish_connection_error_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for control channel establish error during PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1289292'>
+ Test for constrol channel establish error during PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate_establish_connection_error.js'>
+ </script>
+ </body>
+</html>
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 0000000000..24e6f1dc99
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+ Test for PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate.js'>
+ </script>
+ </body>
+</html>
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 0000000000..24e6f1dc99
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+ <!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+ <head>
+ <meta charset='utf-8'>
+ <title>Test for PresentationConnection.terminate()</title>
+ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+ <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+ </head>
+ <body>
+ <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+ Test for PresentationConnection.terminate()</a>
+ <script type='application/javascript' src='test_presentation_terminate.js'>
+ </script>
+ </body>
+</html>
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 0000000000..4ccc0a54ef
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_multicast_dns_device_provider.js
@@ -0,0 +1,1465 @@
+/* -*- 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 Cm = Components.manager;
+
+const { Promise } = ChromeUtils.import("resource://gre/modules/Promise.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.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) {
+ return new Promise(resolve => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ timer.initWithCallback(
+ {
+ notify() {
+ resolve();
+ },
+ },
+ aMs,
+ timer.TYPE_ONE_SHOT
+ );
+ });
+}
+
+function MockFactory(aClass) {
+ this._cls = aClass;
+}
+MockFactory.prototype = {
+ createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("", 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(aLock) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ QueryInterface: ChromeUtils.generateQI(["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() {
+ this.reset();
+
+ let oldContract = this.unregister();
+ this.hookedMap.get(this._contractID).push(oldContract);
+ registrar.registerFactory(
+ this.classID,
+ "",
+ this._contractID,
+ this._newFactory
+ );
+
+ registerCleanupFunction(() => {
+ this.cleanup.apply(this);
+ });
+ },
+
+ reset() {},
+
+ cleanup() {
+ this.reset();
+
+ this.unregister();
+ let prevContract = this.hookedMap.get(this._contractID).pop();
+
+ if (prevContract.classID) {
+ registrar.registerFactory(
+ prevContract.classID,
+ "",
+ this._contractID,
+ prevContract.factory
+ );
+ }
+ },
+
+ unregister() {
+ var classID, factory;
+
+ try {
+ classID = registrar.contractIDToCID(this._contractID);
+ factory = Cm.getClassObject(Cc[this._contractID], Ci.nsIFactory);
+ } catch (ex) {
+ classID = "";
+ factory = null;
+ }
+
+ if (factory) {
+ try {
+ registrar.unregisterFactory(classID, factory);
+ } catch (e) {
+ factory = null;
+ }
+ }
+
+ return { classID, factory };
+ },
+};
+
+function MockDNSServiceInfo() {}
+MockDNSServiceInfo.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["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: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ addDevice(device) {
+ this.devices[device.id] = device;
+ },
+ removeDevice(device) {
+ delete this.devices[device.id];
+ },
+ updateDevice(device) {
+ this.devices[device.id] = device;
+ },
+ onSessionRequest(device, url, presentationId, controlChannel) {},
+
+ count() {
+ 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0,
+ };
+ 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: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ Assert.ok(false, "should not register service if not discoverable");
+ },
+ resolveService(serviceInfo, listener) {},
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+
+ // Try register
+ provider.listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(serviceInfo, listener) {},
+ serviceRegistered: 0,
+ serviceUnregistered: 0,
+ };
+ 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: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(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
+ )
+ );
+ },
+ };
+
+ 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(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
+ )
+ );
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(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;
+ provider = null;
+ run_next_test();
+ },
+ updateDevice() {},
+ removeDevice() {},
+ onSessionRequest() {},
+ };
+
+ provider.listener = listener;
+}
+
+function handleSessionRequest() {
+ Services.prefs.setBoolPref(PREF_DISCOVERY, true);
+ Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
+
+ const testDeviceName = "test-device-name";
+
+ Services.prefs.setCharPref(PREF_DEVICENAME, testDeviceName);
+
+ let mockDevice = createDevice(
+ "device.local",
+ 12345,
+ "service.name",
+ SERVICE_TYPE
+ );
+ let mockSDObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ connect(deviceInfo) {
+ this.request = {
+ deviceInfo,
+ };
+ return {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannel",
+ ]),
+ };
+ },
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return this.version === version;
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ this.device = device;
+ },
+ };
+
+ provider.listener = listener;
+
+ 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {
+ listener.onServiceResolved(
+ createDevice(
+ mockDevice.host,
+ mockDevice.port,
+ mockDevice.serviceName,
+ mockDevice.serviceType
+ )
+ );
+ },
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {},
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ onSessionRequest(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url,
+ presentationId,
+ };
+ },
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ id: mockDevice.host,
+ address: mockDevice.host,
+ port: 54321,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {},
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {},
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ Assert.ok(false, "shouldn't create any new device");
+ },
+ removeDevice(device) {
+ Assert.ok(false, "shouldn't remote any device");
+ },
+ updateDevice(device) {
+ Assert.ok(false, "shouldn't update any device");
+ },
+ onSessionRequest(device, url, presentationId, controlChannel) {
+ Assert.ok(true, "receive onSessionRequest event");
+ this.request = {
+ deviceId: device.id,
+ url,
+ presentationId,
+ };
+ },
+ };
+
+ provider.listener = listener;
+
+ const deviceInfo = {
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ id: "unknown-device.local",
+ address: "unknown-device.local",
+ port: 12345,
+ };
+
+ const testUrl = "http://example.com";
+ const testPresentationId = "test-presentation-id";
+ const testControlChannel = {
+ QueryInterface: ChromeUtils.generateQI(["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 mockObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ Assert.ok(false, "shouldn't perform any device discovery");
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(serviceInfo, listener) {},
+ };
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(
+ createDevice("", 54321, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ resolveService(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: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return false;
+ },
+ port: 54321,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ 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;
+ provider = 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ deferred.resolve();
+ listener.onServiceRegistered(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ resolveService(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: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer() {
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ isCompatibleServer(version) {
+ return this.version === version;
+ },
+ port: 54321,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ 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;
+ provider = 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(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
+ )
+ );
+ },
+ };
+
+ 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+
+ if (!this.discovered) {
+ listener.onServiceFound(mockDevice1);
+ } else {
+ listener.onServiceFound(mockDevice2);
+ }
+ this.discovered = true;
+
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {
+ listener.onDiscoveryStopped(serviceType);
+ },
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(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);
+ }
+ },
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ addDevice(device) {
+ Assert.ok(!this.isDeviceAdded);
+ Assert.equal(device.id, mockDevice1.host);
+ Assert.equal(device.name, mockDevice1.serviceName);
+ this.isDeviceAdded = true;
+ },
+ removeDevice(device) {
+ Assert.ok(false);
+ },
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(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: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {
+ listener.onDiscoveryStopped(serviceType);
+ },
+ };
+ },
+ registerService(serviceInfo, listener) {},
+ resolveService(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);
+ }
+ },
+ };
+
+ 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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {
+ listener.onDiscoveryStarted(serviceType);
+ listener.onServiceFound(
+ createDevice("", 0, mockDevice.serviceName, mockDevice.serviceType)
+ );
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel() {},
+ };
+ },
+ registerService(serviceInfo, listener) {
+ this.serviceRegistered++;
+ return {
+ QueryInterface: ChromeUtils.generateQI(["nsICancelable"]),
+ cancel: () => {
+ this.serviceUnregistered++;
+ },
+ };
+ },
+ resolveService(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,
+ };
+ 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: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {
+ this.devices.push(device);
+ },
+ removeDevice(device) {},
+ updateDevice(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: ChromeUtils.generateQI(["nsIDNSServiceDiscovery"]),
+ startDiscovery(serviceType, listener) {},
+ registerService(serviceInfo, listener) {
+ Assert.ok(isRetrying, "register service after retrying startServer");
+ provider.listener = null;
+ run_next_test();
+ },
+ resolveService(serviceInfo, listener) {},
+ };
+
+ let mockServerObj = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationControlService"]),
+ startServer(encrypted, port) {
+ if (!isRetrying) {
+ isRetrying = true;
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerStopped(Cr.NS_ERROR_FAILURE);
+ });
+ } else {
+ this.port = 54321;
+ Services.tm.dispatchToMainThread(() => {
+ this.listener.onServerReady(this.port, this.certFingerprint);
+ });
+ }
+ },
+ sessionRequest() {},
+ close() {},
+ id: "",
+ version: LATEST_VERSION,
+ port: 0,
+ certFingerprint: "mock-cert-fingerprint",
+ listener: null,
+ };
+
+ new ContractHook(SD_CONTRACT_ID, mockSDObj);
+ new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
+ let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(
+ Ci.nsIPresentationDeviceProvider
+ );
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationDeviceListener",
+ "nsISupportsWeakReference",
+ ]),
+ addDevice(device) {},
+ removeDevice(device) {},
+ updateDevice(device) {},
+ onSessionRequest(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);
+
+ new ContractHook(INFO_CONTRACT_ID, MockDNSServiceInfo);
+
+ registerCleanupFunction(() => {
+ 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 0000000000..e49a794ebc
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -0,0 +1,288 @@
+/* -*- 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 { Services } = ChromeUtils.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: ChromeUtils.generateQI(["nsIPresentationControlChannel"]),
+ sendOffer(offer) {},
+ sendAnswer(answer) {},
+ disconnect() {},
+ launch() {},
+ terminate() {},
+ reconnect() {},
+ set listener(listener) {},
+ get listener() {},
+};
+
+var testProvider = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDeviceProvider"]),
+
+ forceDiscovery() {},
+ set listener(listener) {},
+ get listener() {},
+};
+
+const forbiddenRequestedUrl = "http://example.com";
+var testDevice = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationDevice"]),
+ id: "id",
+ name: "name",
+ type: "type",
+ establishControlChannel(url, presentationId) {
+ return null;
+ },
+ disconnect() {},
+ isRequestedUrlSupported(requestedUrl) {
+ return forbiddenRequestedUrl !== requestedUrl;
+ },
+};
+
+function addProvider() {
+ Object.defineProperty(testProvider, "listener", {
+ configurable: true,
+ set(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");
+ 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");
+ 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);
+ 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");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .onSessionRequest(
+ testDevice,
+ testUrl,
+ testPresentationId,
+ testControlChannel
+ );
+}
+
+function terminateRequest() {
+ 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");
+ 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");
+ 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");
+ manager
+ .QueryInterface(Ci.nsIPresentationDeviceListener)
+ .removeDevice(testDevice);
+}
+
+function removeProvider() {
+ Object.defineProperty(testProvider, "listener", {
+ configurable: true,
+ set(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);
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 0000000000..6f5f267c4c
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_session_transport.js
@@ -0,0 +1,225 @@
+/* -*- 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 CC = Components.Constructor;
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+
+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);
+
+const serverChannelDescription = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPresentationChannelDescription"]),
+ type: 1,
+ tcpAddress: addresses,
+};
+
+var isClientReady = false;
+var isServerReady = false;
+var isClientClosed = false;
+var isServerClosed = false;
+
+const clientCallback = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportCallback",
+ ]),
+ notifyTransportReady() {
+ Assert.ok(true, "Client transport ready.");
+
+ isClientReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ Assert.ok(true, "Client transport is closed.");
+
+ isClientClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData(aData) {
+ Assert.equal(aData, serverMessage, "Client transport receives data.");
+ run_next_test();
+ },
+};
+
+const serverCallback = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportCallback",
+ ]),
+ notifyTransportReady() {
+ Assert.ok(true, "Server transport ready.");
+
+ isServerReady = true;
+ if (isClientReady && isServerReady) {
+ run_next_test();
+ }
+ },
+ notifyTransportClosed(aReason) {
+ Assert.ok(true, "Server transport is closed.");
+
+ isServerClosed = true;
+ if (isClientClosed && isServerClosed) {
+ run_next_test();
+ }
+ },
+ notifyData(aData) {
+ Assert.equal(aData, clientMessage, "Server transport receives data.");
+ run_next_test();
+ },
+};
+
+const clientListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationSessionTransportBuilderListener",
+ ]),
+ onSessionTransport(aTransport) {
+ Assert.ok(true, "Client Transport is built.");
+ clientTransport = aTransport;
+ clientTransport.callback = clientCallback;
+
+ if (serverTransport) {
+ run_next_test();
+ }
+ },
+};
+
+const serverListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "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(aSocket, aTransport) {
+ print("Test server gets a client connection.");
+ serverBuilder = Cc[
+ "@mozilla.org/presentation/presentationtcpsessiontransport;1"
+ ].createInstance(Ci.nsIPresentationTCPSessionTransportBuilder);
+ serverBuilder.buildTCPSenderTransport(aTransport, serverListener);
+ },
+ onStopListening(aSocket) {
+ print("Test server stops listening.");
+ },
+ close() {
+ 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 0000000000..05726ab4b1
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -0,0 +1,376 @@
+/* -*- 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 { ControllerStateMachine } = ChromeUtils.import(
+ "resource://gre/modules/presentation/ControllerStateMachine.jsm"
+);
+const { ReceiverStateMachine } = ChromeUtils.import(
+ "resource://gre/modules/presentation/ReceiverStateMachine.jsm"
+);
+const { State } = ChromeUtils.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) {
+ executeSoon(function() {
+ receiverState.onCommand(command);
+ });
+};
+
+mockReceiverChannel.sendCommand = function(command) {
+ executeSoon(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(presId) {
+ Assert.equal(
+ controllerState.state,
+ State.CONNECTED,
+ "controller in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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(presId) {
+ Assert.equal(
+ controllerState.state,
+ State.CONNECTED,
+ "controller in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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(presId) {
+ Assert.equal(
+ receiverState.state,
+ State.CONNECTED,
+ "receiver in connected state"
+ );
+ Assert.equal(
+ presId,
+ 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(
+ controllerCandidate
+ ) {
+ Assert.equal(
+ controllerCandidate,
+ 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(disconnectReason) {
+ Assert.equal(disconnectReason, 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(disconnectReason) {
+ Assert.equal(disconnectReason, 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(disconnectReason) {
+ Assert.equal(
+ disconnectReason,
+ 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);
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 0000000000..ee0b3aa074
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -0,0 +1,523 @@
+/* -*- 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 { Services } = ChromeUtils.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() {
+ info("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);
+ }
+ this.tcpPort = aTcpPort;
+}
+
+TestDescription.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["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() {
+ 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(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(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(aAnswer) {
+ Assert.ok(false, "get answer");
+ },
+ onIceCandidate(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() {
+ Assert.equal(
+ this.status,
+ "created",
+ "0. presenterControlChannel: opened"
+ );
+ this.status = "opened";
+ },
+ notifyDisconnected(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: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ onReconnectRequest(deviceInfo, url, presentationId, controlChannel) {
+ Assert.equal(url, "http://example.com", "expected url");
+ Assert.equal(
+ presentationId,
+ "testPresentationId",
+ "expected presentation id"
+ );
+ yayFuncs.presenterControlChannelReconnect();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let controllerControlChannel = pcs.connect(presenterDeviceInfo);
+
+ controllerControlChannel.listener = {
+ status: "created",
+ onOffer(offer) {
+ Assert.ok(false, "get offer");
+ },
+ onAnswer(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(aCandidate) {
+ Assert.ok(false, "get ICE candidate");
+ },
+ notifyConnected() {
+ 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(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() {
+ reconnectControllerControlChannel.reconnect(
+ "testPresentationId",
+ "http://example.com"
+ );
+ },
+ notifyReconnected() {
+ yayFuncs.controllerControlChannelReconnect();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function terminateRequest() {
+ let yayFuncs = makeJointSuccess([
+ "controllerControlChannelConnected",
+ "controllerControlChannelDisconnected",
+ "presenterControlChannelDisconnected",
+ "terminatedByController",
+ "terminatedByReceiver",
+ ]);
+ let controllerControlChannel;
+ let terminatePhase = "controller";
+
+ pcs.listener = {
+ onTerminateRequest(
+ 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() {
+ Assert.ok(true, "control channel notify connected");
+ yayFuncs.controllerControlChannelConnected();
+
+ terminatePhase = "receiver";
+ controllerControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "controllerControlChannel notify disconncted"
+ );
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "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: ChromeUtils.generateQI([
+ "nsITCPPresentationServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected() {
+ presenterControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ CLOSE_CONTROL_CHANNEL_REASON,
+ "4. presenterControlChannel notify disconnected"
+ );
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function terminateRequestAbnormal() {
+ let yayFuncs = makeJointSuccess([
+ "controllerControlChannelConnected",
+ "controllerControlChannelDisconnected",
+ "presenterControlChannelDisconnected",
+ ]);
+ let controllerControlChannel;
+
+ pcs.listener = {
+ onTerminateRequest(
+ 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() {
+ Assert.ok(true, "control channel notify connected");
+ yayFuncs.controllerControlChannelConnected();
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ Cr.NS_ERROR_FAILURE,
+ "controllerControlChannel notify disconncted with error"
+ );
+ yayFuncs.controllerControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsITCPPresentationServerListener",
+ ]),
+ };
+
+ let presenterDeviceInfo = {
+ id: "presentatorID",
+ address: "127.0.0.1",
+ port: PRESENTER_CONTROL_CHANNEL_PORT,
+ certFingerprint: pcs.certFingerprint,
+ QueryInterface: ChromeUtils.generateQI(["nsITCPDeviceInfo"]),
+ };
+
+ let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+ presenterControlChannel.listener = {
+ notifyConnected() {
+ presenterControlChannel.terminate("testPresentationId");
+ },
+ notifyDisconnected(aReason) {
+ Assert.equal(
+ aReason,
+ Cr.NS_ERROR_FAILURE,
+ "4. presenterControlChannel notify disconnected with error"
+ );
+ yayFuncs.presenterControlChannelDisconnected();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPresentationControlChannelListener",
+ ]),
+ };
+}
+
+function setOffline() {
+ pcs.listener = {
+ onServerReady(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() {
+ 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(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);
+
+ registerCleanupFunction(() => {
+ 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 0000000000..635cabe5cd
--- /dev/null
+++ b/dom/presentation/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+
+[test_multicast_dns_device_provider.js]
+[test_presentation_device_manager.js]
+[test_presentation_session_transport.js]
+[test_tcp_control_channel.js]
+skip-if = os == "android" || tsan || socketprocess_networking # Bugs 1422582, 1580136, 1450502, 1584360, 1606813
+[test_presentation_state_machine.js]