Bug 1234492 - Part 2 - Add test case. r=smaug
☠☠ backed out by 6dd2e8d7ecfc ☠ ☠
authorKuoE0 <kuoe0.tw@gmail.com>
Tue, 29 Mar 2016 20:00:00 +0200
changeset 291028 75aff80f34a50705e0d0bed418ba7ff546869e49
parent 291027 f74a43d0b9eb6b7a3ae5e03c508eab14ac01620e
child 291029 8a9823b951f6af481fe247fb5a57dac5a2f8bf13
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1234492
milestone48.0a1
Bug 1234492 - Part 2 - Add test case. r=smaug
dom/presentation/tests/mochitest/PresentationSessionChromeScriptFor1UA.js
dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
dom/presentation/tests/mochitest/file_presentation_1ua_receiver_oop.html
dom/presentation/tests/mochitest/mochitest.ini
dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_at_same_side.html
dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_at_same_side_oop.html
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScriptFor1UA.js
@@ -0,0 +1,401 @@
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+* You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function info(str) {
+  dump("INFO " + str + "\n");
+}
+
+const sessionId = "test-session-id";
+const address = Cc["@mozilla.org/supports-cstring;1"]
+                  .createInstance(Ci.nsISupportsCString);
+address.data = "127.0.0.1";
+const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+addresses.appendElement(address, false);
+var requestPromise = null;
+
+// mockedChannelDescription
+function mockedChannelDescription(role) {
+  this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]);
+  this.role = role;
+	this.type = 1;
+	this.tcpAddress = addresses;
+	this.tcpPort = (role === "sender" ? 1234 : 4321); // either sender or receiver
+}
+const mockedChannelDescriptionOfSender = new mockedChannelDescription("sender");
+const mockedChannelDescriptionOfReceiver = new mockedChannelDescription("receiver");
+
+// mockedServerSocket (sender only)
+const mockedServerSocket = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIServerSocket,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  get port() {
+    return this._port;
+  },
+  set listener(listener) {
+    this._listener = listener;
+  },
+  init: function(port, loopbackOnly, backLog) {
+    this._port = (port == -1 ? 5678 : port);
+  },
+  asyncListen: function(listener) {
+    this._listener = listener;
+  },
+  close: function() {
+    this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED);
+  },
+  onSocketAccepted: function(serverSocket, socketTransport) {
+    this._listener.onSocketAccepted(serverSocket, socketTransport);
+  }
+};
+
+// mockedSocketTransport
+const mockedSocketTransport = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISocketTransport]),
+};
+
+// mockedSessionTransport
+var mockedSessionTransportOfSender   = undefined;
+var mockedSessionTransportOfReceiver = undefined;
+
+function mockedSessionTransport() {}
+
+mockedSessionTransport.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport]),
+  set callback(callback) {
+    this._callback = callback;
+  },
+  get callback() {
+    return this._callback;
+  },
+  get selfAddress() {
+    return this._selfAddress;
+  },
+  initWithSocketTransport: function(transport, callback) {
+    mockedSessionTransportOfSender = this;
+    this.role = "sender";
+    this._callback = callback; // callback is ControllingInfo
+    this.simulateTransportReady();
+  },
+  initWithChannelDescription: function(description, callback) {
+    mockedSessionTransportOfReceiver = this;
+    this.role = "receiver";
+    this._callback = callback; // callback is PresentingInfo
+    var addresses = description
+                      .QueryInterface(Ci.nsIPresentationChannelDescription)
+                      .tcpAddress;
+    this._selfAddress = {
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsINetAddr]),
+      address: (addresses.length > 0) ?
+                addresses.queryElementAt(0, Ci.nsISupportsCString).data : "",
+      port: description
+              .QueryInterface(Ci.nsIPresentationChannelDescription)
+              .tcpPort,
+    };
+  },
+  enableDataNotification: function() {
+    if (this.role === "sender") {
+      sendAsyncMessage("data-transport-notification-of-sender-enabled");
+    } else {
+      sendAsyncMessage("data-transport-notification-of-receiver-enabled");
+    }
+  },
+  send: function(data) {
+    // var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
+    //                    createInstance(Ci.nsIBinaryInputStream);
+    // binaryStream.setInputStream(data);
+    // var message = binaryStream.readBytes(binaryStream.available());
+    var message = data.QueryInterface(Ci.nsISupportsCString).data;
+    info("Send message: \"" + message + "\" from " + this.role);
+    if (this.role === "sender") {
+      mockedSessionTransportOfReceiver._callback.notifyData(message);
+    } else {
+      mockedSessionTransportOfSender._callback.notifyData(message);
+    }
+  },
+  close: function(reason) {
+    sendAsyncMessage("data-transport-closed", reason);
+    this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason);
+  },
+  simulateTransportReady: function() {
+    this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+  },
+};
+
+// factory of mockedSessionTransport
+const mockedSessionTransportFactory = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    var result = new mockedSessionTransport();
+    return result.QueryInterface(aIID);
+  }
+};
+
+// control channel of sender
+const mockedControlChannelOfSender = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  set listener(listener) {
+    if (listener) {
+      sendAsyncMessage("controlling-info-created");
+    }
+    this._listener = listener;
+  },
+  get listener() {
+    return this._listener;
+  },
+  notifyOpened: function() {
+    // send offer after notifyOpened immediately
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .notifyOpened();
+  },
+  sendOffer: function(offer) {
+    sendAsyncMessage("offer-sent");
+  },
+  onAnswer: function(answer) {
+    sendAsyncMessage("answer-received", answer.role);
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .onAnswer(answer);
+  },
+  close: function(reason) {
+    sendAsyncMessage("control-channel-of-sender-closed", reason);
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .notifyClosed(reason);
+  }
+};
+
+// control channel of receiver
+const mockedControlChannelOfReceiver = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  set listener(listener) {
+    if (listener) {
+      sendAsyncMessage("presenting-info-created");
+    }
+    this._listener = listener;
+  },
+  get listener() {
+    return this._listener;
+  },
+  notifyOpened: function() {
+    // do nothing
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .notifyOpened();
+  },
+  onOffer: function(offer) {
+    sendAsyncMessage("offer-received", offer.role);
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .onOffer(offer);
+  },
+  sendAnswer: function(answer) {
+    sendAsyncMessage("answer-sent");
+    this._listener
+        .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
+        .notifyTransportReady();
+  },
+  close: function(reason) {
+    sendAsyncMessage("control-channel-receiver-closed", reason);
+    this._listener
+        .QueryInterface(Ci.nsIPresentationControlChannelListener)
+        .notifyClosed(reason);
+  }
+};
+
+const mockedDevice = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+  id:   "id",
+  name: "name",
+  type: "type",
+  establishControlChannel: function(url, presentationId) {
+    sendAsyncMessage("control-channel-established");
+    return mockedControlChannelOfSender;
+  },
+};
+
+const mockedDevicePrompt = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  set request(request) {
+    this._request = request;
+  },
+  get request() {
+    return this._request;
+  },
+  promptDeviceSelection: function(request) {
+    this._request = request;
+    sendAsyncMessage("device-prompt");
+  },
+  simulateSelect: function() {
+    this._request.select(mockedDevice);
+  },
+  simulateCancel: function() {
+    this._request.cancel();
+  }
+};
+
+const mockedRequestUIGlue = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  sendRequest: function(aUrl, aSessionId) {
+    return requestPromise;
+  },
+};
+
+function registerMockedFactory(contractId, mockedClassId, mockedFactory) {
+  var originalClassId, originalFactory;
+
+  var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+  if (!registrar.isCIDRegistered(mockedClassId)) {
+    try {
+      originalClassId = registrar.contractIDToCID(contractId);
+      originalFactory = Cm.getClassObject(Cc[contractId], Ci.nsIFactory);
+    } catch (ex) {
+      originalClassId = "";
+      originalFactory = null;
+    }
+    if (originalFactory) {
+      registrar.unregisterFactory(originalClassId, originalFactory);
+    }
+    registrar.registerFactory(mockedClassId, "", contractId, mockedFactory);
+  }
+
+  return { contractId: contractId,
+           mockedClassId: mockedClassId,
+           mockedFactory: mockedFactory,
+           originalClassId: originalClassId,
+           originalFactory: originalFactory };
+}
+
+function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
+  if (originalFactory) {
+    registrar.unregisterFactory(mockedClassId, mockedFactory);
+    registrar.registerFactory(originalClassId, "", contractId, originalFactory);
+  }
+}
+
+function tearDown() {
+  var deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                      .getService(Ci.nsIPresentationDeviceManager);
+  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener).removeDevice(mockedDevice);
+
+  mockedServerSocket.listener = null;
+  mockedSessionTransportOfSender.callback = null;
+  mockedSessionTransportOfReceiver.callback = null;
+  mockedControlChannelOfSender.listener = null;
+  mockedControlChannelOfReceiver.listener = null;
+
+  // Register original factories.
+  for (var data in originalFactoryData) {
+    registerOriginalFactory(data.contractId, data.mockedClassId,
+                            data.mockedFactory, data.originalClassId,
+                            data.originalFactory);
+  }
+  sendAsyncMessage("teardown-complete");
+}
+
+// Register mocked factories.
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                      .getService(Ci.nsIUUIDGenerator);
+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/presentationsessiontransport;1",
+                                               uuidGenerator.generateUUID(),
+                                               mockedSessionTransportFactory));
+originalFactoryData.push(registerMockedFactory("@mozilla.org/presentation/requestuiglue;1",
+                                               uuidGenerator.generateUUID(),
+                                               mockedRequestUIGlue));
+
+// Add mocked device into device list.
+addMessageListener("trigger-device-add", function() {
+  var deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                      .getService(Ci.nsIPresentationDeviceManager);
+  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener).addDevice(mockedDevice);
+});
+
+// Select the mocked device for presentation.
+addMessageListener("trigger-device-prompt-select", function() {
+  mockedDevicePrompt.simulateSelect();
+});
+
+// Trigger nsIPresentationDeviceManager::OnSessionRequest
+// to create session at receiver.
+addMessageListener("trigger-on-session-request", function(url) {
+  var deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                        .getService(Ci.nsIPresentationDeviceManager);
+  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
+	             .onSessionRequest(mockedDevice,
+                                 url,
+                                 sessionId,
+                                 mockedControlChannelOfReceiver);
+});
+
+// Trigger control channel opened
+addMessageListener("trigger-control-channel-open", function(reason) {
+  mockedControlChannelOfSender.notifyOpened();
+  mockedControlChannelOfReceiver.notifyOpened();
+});
+
+// Trigger server socket of controlling info accepted
+addMessageListener("trigger-socket-accepted", function() {
+});
+
+addMessageListener("trigger-on-offer", function() {
+  mockedControlChannelOfReceiver.onOffer(mockedChannelDescriptionOfSender);
+  mockedServerSocket.onSocketAccepted(mockedServerSocket, mockedSocketTransport);
+});
+
+addMessageListener("trigger-on-answer", function() {
+  mockedControlChannelOfSender.onAnswer(mockedChannelDescriptionOfReceiver);
+});
+
+addMessageListener("forward-command", function(command_data) {
+  let command = JSON.parse(command_data);
+  sendAsyncMessage(command.name, command.data);
+});
+
+addMessageListener("teardown", function() {
+  tearDown();
+});
+
+var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+obs.addObserver(function observer(aSubject, aTopic, aData) {
+  obs.removeObserver(observer, aTopic);
+  requestPromise = aSubject;
+}, "setup-request-promise", false);
+
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -0,0 +1,127 @@
+<!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;version=1.7">
+
+"use strict";
+
+function is(a, b, msg) {
+  if (a === b) {
+    window.parent.postMessage("OK " + msg, "*");
+  } else {
+    window.parent.postMessage("KO " + msg + " | reason: " + a + " != " + b, "*");
+  }
+}
+
+function ok(a, msg) {
+  window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
+}
+
+function info(msg) {
+  window.parent.postMessage("INFO " + msg, "*");
+}
+
+function command(name, data) {
+  window.parent.postMessage("COMMAND " + JSON.stringify({name: name, data: data}), "*");
+}
+
+function finish() {
+  window.parent.postMessage("DONE", "*");
+}
+
+var connection;
+
+function testConnectionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    info("Receiver: --- testConnectionAvailable ---");
+    ok(navigator.presentation, "Receiver: navigator.presentation should be available.");
+    // FIXME Sometimes navigator.presentation.receiver is initialized lately.
+    // See bug 1234128 - navigator.presentation.receiver is null in 1-UA use case.
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1234128
+    while (!navigator.presentation.receiver) {
+      info("Receiver: navigator.presentation.receiver is null, see Bug 1234128");
+    }
+    ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+    navigator.presentation.receiver.getConnection()
+    .then((aConnection) => {
+      connection = aConnection;
+      ok(connection.id, "Receiver: Connection ID should be set: " + connection.id);
+      is(connection.state, "closed", "Receiver: Connection state at receiver side should be closed by default.");
+      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.onstatechange = function() {
+      connection.onstatechange = 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 messageHandler(evt) {
+      connection.removeEventListener("message", messageHandler);
+      let msg = evt.data;
+      is(msg, "msg-sender-to-receiver", "Receiver: Receiver should receive message from sender.");
+      aResolve();
+      command("forward-command", JSON.stringify({ name: "message-from-sender-received" }));
+    });
+    command("forward-command", JSON.stringify({ name: "trigger-message-from-sender" }));
+  });
+}
+
+function testSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("Receiver: --- testSendMessage ---");
+    window.addEventListener("message", function messageHandler(evt) {
+      var message = evt.data;
+      if (message.type === "trigger-message-from-receiver") {
+        connection.send("msg-receiver-to-sender");
+      }
+      if (message.type === "message-from-receiver-received") {
+        window.removeEventListener("message", messageHandler);
+        aResolve();
+      }
+    });
+  });
+}
+
+function testTerminateConnection() {
+  return new Promise(function(aResolve, aReject) {
+    info("Receiver: --- testTerminateConnection ---");
+    connection.onstatechange = function() {
+      connection.onstatechange = null;
+      is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+      aResolve();
+    };
+    connection.terminate();
+  });
+}
+
+testConnectionAvailable()
+.then(testConnectionReady)
+.then(testIncomingMessage)
+.then(testSendMessage)
+.then(testTerminateConnection)
+.then(finish);
+
+</script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver_oop.html
@@ -0,0 +1,132 @@
+<!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;version=1.7">
+
+"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: name, data: 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.");
+    // FIXME Sometimes navigator.presentation.receiver is initialized lately.
+    // See bug 1234128 - navigator.presentation.receiver is null in 1-UA use case.
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1234128
+    while (!navigator.presentation.receiver) {
+      info("Receiver: navigator.presentation.receiver is null, see Bug 1234128");
+    }
+    ok(navigator.presentation.receiver, "Receiver: navigator.presentation.receiver should be available.");
+    navigator.presentation.receiver.getConnection()
+    .then((aConnection) => {
+      connection = aConnection;
+      ok(connection.id, "Receiver: Connection ID should be set: " + connection.id);
+      is(connection.state, "closed", "Receiver: Connection state at receiver side should be closed by default.");
+      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.onstatechange = function() {
+      connection.onstatechange = null;
+      is(connection.state, "connected", "Receiver: Connection state should become connected.");
+      aResolve();
+    };
+	if (connection.state === "connected") {
+      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 messageHandler(evt) {
+      connection.removeEventListener("message", messageHandler);
+      let msg = evt.data;
+      is(msg, "msg-sender-to-receiver", "Receiver: Receiver should receive message from sender.");
+      aResolve();
+      command("forward-command", JSON.stringify({ name: "message-from-sender-received" }));
+    });
+    command("forward-command", JSON.stringify({ name: "trigger-message-from-sender" }));
+  });
+}
+
+function testSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("Receiver: --- testSendMessage ---");
+    window.addEventListener("hashchange", function hashchangeHandler() {
+      var message = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
+      if (message.type === "trigger-message-from-receiver") {
+        info("Receiver: Send message to sender.");
+        connection.send("msg-receiver-to-sender");
+      }
+      if (message.type === "message-from-receiver-received") {
+        window.removeEventListener("hashchange", hashchangeHandler);
+        aResolve();
+      }
+    });
+  });
+}
+
+function testTerminateConnection() {
+  return new Promise(function(aResolve, aReject) {
+    info("Receiver: --- testTerminateConnection ---");
+    connection.onstatechange = function() {
+      connection.onstatechange = null;
+      is(connection.state, "terminated", "Receiver: Connection should be terminated.");
+      aResolve();
+    };
+    connection.terminate();
+  });
+}
+
+testConnectionAvailable()
+.then(testConnectionReady)
+.then(testIncomingMessage)
+.then(testSendMessage)
+.then(testTerminateConnection)
+.then(finish);
+
+</script>
+  </body>
+</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -1,19 +1,26 @@
 [DEFAULT]
 support-files =
   PresentationDeviceInfoChromeScript.js
   PresentationSessionChromeScript.js
+  PresentationSessionChromeScriptFor1UA.js
+  file_presentation_1ua_receiver.html
+  file_presentation_1ua_receiver_oop.html
+  file_presentation_non_receiver_inner_iframe_oop.html
+  file_presentation_non_receiver_oop.html
   file_presentation_receiver.html
-  file_presentation_receiver_oop.html
-  file_presentation_non_receiver_oop.html
   file_presentation_receiver_establish_connection_error.html
   file_presentation_receiver_inner_iframe_oop.html
-  file_presentation_non_receiver_inner_iframe_oop.html
+  file_presentation_receiver_oop.html
 
+[test_presentation_1ua_sender_and_receiver_at_same_side.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
+[test_presentation_1ua_sender_and_receiver_at_same_side_oop.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_device_info.html]
 [test_presentation_device_info_permission.html]
 [test_presentation_sender_disconnect.html]
 skip-if = toolkit == 'android' # Bug 1129785
 [test_presentation_sender_establish_connection_error.html]
 skip-if = toolkit == 'android' # Bug 1129785
 [test_presentation_sender.html]
 skip-if = toolkit == 'android' # Bug 1129785
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_at_same_side.html
@@ -0,0 +1,272 @@
+<!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 when sender and receiver at the same side</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=1234492">
+    Test for B2G Presentation API when sender and receiver at the same side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScriptFor1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_receiver.html');
+var request;
+var connection;
+var receiverIframe;
+
+function setup() {
+  // Create receiver iframe
+  receiverIframe = document.createElement('iframe');
+  receiverIframe.onload = function() {
+    info('Receiver loaded.');
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+    // mockedControlChannelOfSender.notifyOpened();
+    //   -> mockedControlChannelOfSender.sendOffer();
+    //   -> 'offer-sent' event emitted.
+  }
+  receiverIframe.setAttribute('src', receiverUrl);
+  var promise = new Promise(function(aResolve, aReject) {
+    document.body.appendChild(receiverIframe);
+    aResolve(receiverIframe);
+  });
+
+  // Pass receiver iframe to ChromeScript
+  // This iframe would be used when device is chosen
+  var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+                         .getService(SpecialPowers.Ci.nsIObserverService);
+  obs.notifyObservers(promise, 'setup-request-promise', null);
+
+  // This event is triggered when the iframe calls "postMessage".
+  window.addEventListener('message', function messageHandler(evt) {
+    var message = evt.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 /, ''));
+      gScript.sendAsyncMessage(command.name, command.data);
+    } else if (/^DONE$/.exec(message)) {
+      window.removeEventListener('message', messageHandler);
+      teardown();
+    }
+  }, false);
+  return Promise.resolve();
+}
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testSetup ---');
+    request = new PresentationRequest("http://example.com");
+    request.getAvailability()
+    .then((aAvailability) => {
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, "Sender: Device should be available.");
+        aResolve();
+      }
+    })
+    .catch((aError) => {
+      ok(false, "Sender: Error occurred when getting availability: " + aError);
+      teardown();
+      aReject();
+    });
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  var controllingInfoCreated = false, presentingInfoCreated = false;
+
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testStartConnection ---');
+    gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+      gScript.removeMessageListener('device-prompt', devicePromptHandler);
+      gScript.sendAsyncMessage('trigger-device-prompt-select');
+      // mockedDevicePrompt.simulateSelect()
+      //   -> Call PresentationDeviceRequest::Select()
+      //   -> Call PresentationDevice::EstablishControlChannel()
+      //   -> 'control-channel-established' event emitted
+      //   -> Create PresentationControllingInfo
+    });
+
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established', controlChannelEstablishedHandler);
+      gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+      // PresentationDeviceManager.OnSessionRequest()
+      //   -> PresentationService.HandleSessionRequest()
+      //   -> Create PresentationPresentingInfo
+      //   -> mockRequestUIGlue.sendRequest()
+    });
+
+    gScript.addMessageListener('controlling-info-created', function controllingInfoCreatedHandler() {
+      gScript.removeMessageListener('controlling-info-created', controllingInfoCreatedHandler);
+      info('PresentationControllingInfo is created.');
+      controllingInfoCreated = true;
+    });
+
+    gScript.addMessageListener('presenting-info-created', function presentingInfoCreatedHandler() {
+      gScript.removeMessageListener('presenting-info-created', presentingInfoCreatedHandler);
+      info('PresentationPresentingInfo is created.');
+      presentingInfoCreated = true;
+    });
+
+    // Wait for receiver loaded and 'trigger-control-channel-open' event emit
+    // to trigger mockedControlChannelOfSender.sendOffer().
+    gScript.addMessageListener('offer-sent', function offerSentHandler() {
+      gScript.removeMessageListener('offer-sent', offerSentHandler);
+      gScript.sendAsyncMessage('trigger-on-offer');
+      // mockedControlChannelOfReceiver.onOffer()
+      //   -> PresentationPresentingInfo.OnOffer()
+      //   -> PresentationPresentingInfo.InitTransportAndSendAnswer()
+      //   -> mockedControlChannelOfReceiver.sendAnswer()
+      //   -> 'answer-sent' event emitted
+    });
+
+    gScript.addMessageListener('answer-sent', function answerSentHandler() {
+      gScript.removeMessageListener('answer-sent', answerSentHandler);
+      gScript.sendAsyncMessage('trigger-on-answer');
+      // mockedControlChannelOfSender.onAnswer();
+      //   -> PresentationControllingInfo.OnAnswer()
+      //   -> PresentationSessionInfo.ReplySuccess()
+      //   -> PresentationRequesterCallback::NotifySuccess()
+      //   -> Create PresentationConnection
+    });
+
+    var connectionFromEvent;
+    request.onconnectionavailable = (aEvent) => {
+      request.onconnectionavailable = null;
+      connectionFromEvent = aEvent.connection;
+      ok(connectionFromEvent, "Sender: |connectionavailable| event is fired with a connection.");
+
+      if (connection) {
+        is(connection, connectionFromEvent,
+           "Sender: The connection from promise and the one from |connectionavailable| event should be the same.");
+        aResolve();
+      }
+    };
+
+    request.start()
+    .then((aConnection) => {
+
+      if (!controllingInfoCreated || !presentingInfoCreated) {
+        aReject();
+      }
+      ok(controllingInfoCreated && presentingInfoCreated,
+         'Sender: PresentationControllingInfo and PresentationPresentingInfo are both created.');
+
+      connection = aConnection;
+      ok(connection, "Sender: Connection should be available.");
+      ok(connection.id, "Sender: Connection ID should be set.");
+      is(connection.state, "connected", "Sender: Connection state at sender side should be connected by default.");
+
+      if (connectionFromEvent) {
+        is(connection, connectionFromEvent,
+           "Sender: The connection from promise and the one from |connectionavailable| event should be the same.");
+        aResolve();
+      }
+    })
+    .catch((aError) => {
+      ok(false, "Sender: Error occurred when establishing a connection: " + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testSendMessage ---');
+    gScript.addMessageListener('trigger-message-from-sender',
+      function triggerMessageFromSenderHandler() {
+        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() {
+        gScript.removeMessageListener('message-from-sender-received',
+                                      messageFromSenderReceivedHandler);
+        aResolve();
+    });
+  });
+}
+
+function testIncomingMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testIncomingMessage ---');
+    connection.addEventListener('message', function messageHandler(evt) {
+      connection.removeEventListener('message', messageHandler);
+      let msg = evt.data;
+      is(msg, "msg-receiver-to-sender", "Sender: Sender should receive message from Receiver");
+      receiverIframe.contentWindow.postMessage({ type: 'message-from-receiver-received' }, '*');
+      aResolve();
+    });
+    receiverIframe.contentWindow.postMessage({ type: 'trigger-message-from-receiver' }, '*');
+  });
+}
+
+function testTerminateConnection() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testTerminateConnection ---');
+    gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler);
+      info("Sender: The data transport is closed. " + aReason);
+    });
+
+    connection.onstatechange = function() {
+      connection.onstatechange = null;
+      is(connection.state, "terminated", "Sender: Connection should be terminated.");
+      aResolve();
+    };
+
+    connection.terminate();
+  });
+}
+
+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, "Sender: PresentationRequest should be available.");
+
+  setup()
+  .then(testSetup)
+  .then(testStartConnection)
+  .then(testSendMessage)
+  .then(testIncomingMessage)
+  .then(testTerminateConnection);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.presentation.test.enabled", true],
+                                      ["dom.presentation.test.stage", 0]]},
+                            runTests);
+});
+
+</script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_at_same_side_oop.html
@@ -0,0 +1,287 @@
+<!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 when sender and receiver at the same side</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=1234492">
+    Test for B2G Presentation API when sender and receiver at the same side</a>
+<script type="application/javascript;version=1.8">
+
+"use strict";
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("PresentationSessionChromeScriptFor1UA.js"));
+var receiverUrl = SimpleTest.getTestFileURL("file_presentation_1ua_receiver_oop.html");
+var request;
+var connection;
+var receiverIframe;
+
+function setup() {
+  // Create receiver iframe
+  receiverIframe = document.createElement("iframe");
+  receiverIframe.addEventListener("mozbrowserloadend",
+    function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener("mozbrowserloadend",
+                                         mozbrowserloadendHander);
+      info("Receiver loaded.");
+      gScript.sendAsyncMessage("trigger-control-channel-open");
+      // mockedControlChannelOfSender.notifyOpened();
+      //   -> mockedControlChannelOfSender.sendOffer();
+      //   -> "offer-sent" event emitted.
+  });
+
+  receiverIframe.setAttribute("remote", "true");
+  receiverIframe.setAttribute("mozbrowser", "true");
+  receiverIframe.setAttribute("src", receiverUrl);
+  var promise = new Promise(function(aResolve, aReject) {
+    document.body.appendChild(receiverIframe);
+    aResolve(receiverIframe);
+  });
+
+  // Pass receiver iframe to ChromeScript
+  // This iframe would be used when device is chosen
+  var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+                         .getService(SpecialPowers.Ci.nsIObserverService);
+  obs.notifyObservers(promise, "setup-request-promise", null);
+
+  // This event is triggered when the iframe calls "alert".
+  receiverIframe.addEventListener("mozbrowsershowmodalprompt",
+    function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ""));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ""));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ""));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ""));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        receiverIframe.removeEventListener("mozbrowsershowmodalprompt",
+                                           receiverListener);
+        teardown();
+      }
+  }, false);
+  return Promise.resolve();
+}
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    info("Sender: --- testSetup ---");
+    request = new PresentationRequest("http://example.com");
+    request.getAvailability()
+    .then((aAvailability) => {
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, "Sender: Device should be available.");
+        aResolve();
+      }
+    })
+    .catch((aError) => {
+      ok(false, "Sender: Error occurred when getting availability: " + aError);
+      teardown();
+      aReject();
+    });
+    gScript.sendAsyncMessage("trigger-device-add");
+  });
+}
+
+function testStartConnection() {
+  var controllingInfoCreated = false, presentingInfoCreated = false;
+
+  return new Promise(function(aResolve, aReject) {
+    info("Sender: --- testStartConnection ---");
+    gScript.addMessageListener("device-prompt", function devicePromptHandler() {
+      gScript.removeMessageListener("device-prompt", devicePromptHandler);
+      gScript.sendAsyncMessage("trigger-device-prompt-select");
+      // mockedDevicePrompt.simulateSelect()
+      //   -> Call PresentationDeviceRequest::Select()
+      //   -> Call PresentationDevice::EstablishControlChannel()
+      //   -> "control-channel-established" event emitted
+      //   -> Create PresentationControllingInfo
+    });
+
+    gScript.addMessageListener("control-channel-established", function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener("control-channel-established", controlChannelEstablishedHandler);
+      gScript.sendAsyncMessage("trigger-on-session-request", receiverUrl);
+      // PresentationDeviceManager.OnSessionRequest()
+      //   -> PresentationService.HandleSessionRequest()
+      //   -> Create PresentationPresentingInfo
+      //   -> mockRequestUIGlue.sendRequest()
+    });
+
+    gScript.addMessageListener("controlling-info-created", function controllingInfoCreatedHandler() {
+      gScript.removeMessageListener("controlling-info-created", controllingInfoCreatedHandler);
+      info("PresentationControllingInfo is created.");
+      controllingInfoCreated = true;
+    });
+
+    gScript.addMessageListener("presenting-info-created", function presentingInfoCreatedHandler() {
+      gScript.removeMessageListener("presenting-info-created", presentingInfoCreatedHandler);
+      info("PresentationPresentingInfo is created.");
+      presentingInfoCreated = true;
+    });
+
+    // Wait for receiver loaded and "trigger-control-channel-open" event emit
+    // to trigger mockedControlChannelOfSender.sendOffer().
+    gScript.addMessageListener("offer-sent", function offerSentHandler() {
+      gScript.removeMessageListener("offer-sent", offerSentHandler);
+      gScript.sendAsyncMessage("trigger-on-offer");
+      // mockedControlChannelOfReceiver.onOffer()
+      //   -> PresentationPresentingInfo.OnOffer()
+      //   -> PresentationPresentingInfo.InitTransportAndSendAnswer()
+      //   -> mockedControlChannelOfReceiver.sendAnswer()
+      //   -> "answer-sent" event emitted
+    });
+
+    gScript.addMessageListener("answer-sent", function answerSentHandler() {
+      gScript.removeMessageListener("answer-sent", answerSentHandler);
+      gScript.sendAsyncMessage("trigger-on-answer");
+      // mockedControlChannelOfSender.onAnswer();
+      //   -> PresentationControllingInfo.OnAnswer()
+      //   -> PresentationSessionInfo.ReplySuccess()
+      //   -> PresentationRequesterCallback::NotifySuccess()
+      //   -> Create PresentationConnection
+    });
+
+    var connectionFromEvent;
+    request.onconnectionavailable = (aEvent) => {
+      request.onconnectionavailable = null;
+      connectionFromEvent = aEvent.connection;
+      ok(connectionFromEvent, "Sender: |connectionavailable| event is fired with a connection.");
+
+      if (connection) {
+        is(connection, connectionFromEvent,
+           "Sender: The connection from promise and the one from |connectionavailable| event should be the same.");
+        aResolve();
+      }
+    };
+
+    request.start()
+    .then((aConnection) => {
+
+      if (!controllingInfoCreated || !presentingInfoCreated) {
+        aReject();
+      }
+      ok(controllingInfoCreated && presentingInfoCreated,
+         "Sender: PresentationControllingInfo and PresentationPresentingInfo are both created.");
+
+      connection = aConnection;
+      ok(connection, "Sender: Connection should be available.");
+      ok(connection.id, "Sender: Connection ID should be set.");
+      is(connection.state, "connected", "Sender: Connection state at sender side should be connected by default.");
+
+      if (connectionFromEvent) {
+        is(connection, connectionFromEvent,
+           "Sender: The connection from promise and the one from |connectionavailable| event should be the same.");
+        aResolve();
+      }
+    })
+    .catch((aError) => {
+      ok(false, "Sender: Error occurred when establishing a connection: " + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testSendMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("Sender: --- testSendMessage ---");
+    gScript.addMessageListener("trigger-message-from-sender",
+      function triggerMessageFromSenderHandler() {
+        gScript.removeMessageListener("trigger-message-from-sender",
+                                      triggerMessageFromSenderHandler);
+        info("Sender: Send message to receiver.");
+        connection.send("msg-sender-to-receiver");
+    });
+
+    gScript.addMessageListener("message-from-sender-received",
+      function messageFromSenderReceivedHandler() {
+        gScript.removeMessageListener("message-from-sender-received",
+                                      messageFromSenderReceivedHandler);
+        aResolve();
+    });
+  });
+}
+
+function testIncomingMessage() {
+  return new Promise(function(aResolve, aReject) {
+    info("Sender: --- testIncomingMessage ---");
+    connection.addEventListener("message", function messageHandler(evt) {
+      connection.removeEventListener("message", messageHandler);
+      let msg = evt.data;
+      is(msg, "msg-receiver-to-sender", "Sender: Sender should receive message from Receiver");
+      receiverIframe.src = receiverUrl + "#"
+                         + encodeURIComponent(JSON.stringify({ type: "message-from-receiver-received" }));
+      info(receiverIframe.src);
+      aResolve();
+    });
+    receiverIframe.src = receiverUrl + "#"
+                       + encodeURIComponent(JSON.stringify({ type: "trigger-message-from-receiver" }));
+    info(receiverIframe.src);
+  });
+}
+
+function testTerminateConnection() {
+  return new Promise(function(aResolve, aReject) {
+    info("Sender: --- testTerminateConnection ---");
+    gScript.addMessageListener("data-transport-closed", function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener("data-transport-closed", dataTransportClosedHandler);
+      info("Sender: The data transport is closed. " + aReason);
+    });
+
+    connection.onstatechange = function() {
+      connection.onstatechange = null;
+      is(connection.state, "terminated", "Sender: Connection should be terminated.");
+      aResolve();
+    };
+
+    connection.terminate();
+  });
+}
+
+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, "Sender: PresentationRequest should be available.");
+
+  setup()
+  .then(testSetup)
+  .then(testStartConnection)
+  .then(testSendMessage)
+  .then(testIncomingMessage)
+  .then(testTerminateConnection);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: "presentation-device-manage", allow: false, context: document},
+  {type: "presentation", allow: true, context: document},
+  {type: "browser", allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
+                                      ["dom.presentation.test.enabled", true],
+                                      ["dom.presentation.test.stage", 0],
+                                      ["dom.mozBrowserFramesEnabled", true],
+                                      ["dom.ipc.browser_frames.oop_by_default", true]]},
+                            runTests);
+});
+
+</script>
+  </body>
+</html>