diff options
Diffstat (limited to '')
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] |