Bug 1069230 - Presentation API implementation. Part 9 - Tests. r=kikuo
authorSean Lin <selin@mozilla.com>
Thu, 23 Apr 2015 11:44:01 +0800
changeset 288289 c4af32fef209d1cca97b4288608ae9c91bb466fc
parent 288288 e445b8e084a50433069ec09614b5aaea164ba746
child 288290 3efaef5dd40e63f2c891a6d3c25ad102e1ef4932
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskikuo
bugs1069230
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1069230 - Presentation API implementation. Part 9 - Tests. r=kikuo
dom/presentation/PresentationSessionInfo.cpp
dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
dom/presentation/tests/mochitest/file_presentation_receiver.html
dom/presentation/tests/mochitest/file_presentation_receiver_oop.html
dom/presentation/tests/mochitest/file_presentation_receiver_start_session_error.html
dom/presentation/tests/mochitest/mochitest.ini
dom/presentation/tests/mochitest/test_presentation_receiver.html
dom/presentation/tests/mochitest/test_presentation_receiver_oop.html
dom/presentation/tests/mochitest/test_presentation_receiver_start_session_error.html
dom/presentation/tests/mochitest/test_presentation_receiver_start_session_timeout.html
dom/presentation/tests/mochitest/test_presentation_sender.html
dom/presentation/tests/mochitest/test_presentation_sender_disconnect.html
dom/presentation/tests/mochitest/test_presentation_sender_start_session_error.html
--- a/dom/presentation/PresentationSessionInfo.cpp
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -513,16 +513,20 @@ PresentationRequesterInfo::OnSocketAccep
 }
 
 NS_IMETHODIMP
 PresentationRequesterInfo::OnStopListening(nsIServerSocket* aServerSocket,
                                            nsresult aStatus)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  if (aStatus == NS_BINDING_ABORTED) { // The server socket was manually closed.
+    return NS_OK;
+  }
+
   Shutdown(aStatus);
 
   if (!IsSessionReady()) {
     // It happens before the session is ready. Reply the callback.
     return ReplyError(aStatus);
   }
 
   // It happens after the session is ready. Notify session state change.
@@ -790,17 +794,17 @@ PresentationResponderInfo::ResolvedCallb
   }
 }
 
 void
 PresentationResponderInfo::RejectedCallback(JSContext* aCx,
                                             JS::Handle<JS::Value> aValue)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  NS_WARNING("The receiver page fails to become ready before timeout.");
+  NS_WARNING("Launching the receiver page has been rejected.");
 
   if (mTimer) {
     mTimer->Cancel();
     mTimer = nullptr;
   }
 
   ReplyError(NS_ERROR_DOM_ABORT_ERR);
 }
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -0,0 +1,359 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+'use strict';
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+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);
+  }
+}
+
+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);
+
+const mockedChannelDescription = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]),
+  type: 1,
+  tcpAddress: addresses,
+  tcpPort: 1234,
+};
+
+const mockedServerSocket = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIServerSocket,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  get port() {
+    return this._port;
+  },
+  set listener(listener) {
+    this._listener = listener;
+  },
+  init: function(port, loopbackOnly, backLog) {
+    if (port != -1) {
+      this._port = port;
+    } else {
+      this._port = 5678;
+    }
+  },
+  asyncListen: function(listener) {
+    this._listener = listener;
+  },
+  close: function() {
+    this._listener.onStopListening(this, Cr.NS_BINDING_ABORTED);
+  },
+  simulateOnSocketAccepted: function(serverSocket, socketTransport) {
+    this._listener.onSocketAccepted(serverSocket, socketTransport);
+  }
+};
+
+const mockedSocketTransport = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISocketTransport]),
+};
+
+const mockedControlChannel = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
+  set listener(listener) {
+    this._listener = listener;
+  },
+  get listener() {
+    return this._listener;
+  },
+  sendOffer: function(offer) {
+    sendAsyncMessage('offer-sent');
+  },
+  sendAnswer: function(answer) {
+    this._listener.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+  },
+  close: function(reason) {
+    sendAsyncMessage('control-channel-closed', reason);
+    this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyClosed(reason);
+  },
+  simulateReceiverReady: function() {
+    this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady();
+  },
+  simulateOnOffer: function() {
+    sendAsyncMessage('offer-received');
+    this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onOffer(mockedChannelDescription);
+  },
+  simulateOnAnswer: function() {
+    sendAsyncMessage('answer-received');
+    this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).onAnswer(mockedChannelDescription);
+  },
+};
+
+const mockedDevice = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+  id: 'id',
+  name: 'name',
+  type: 'type',
+  establishControlChannel: function(url, presentationId) {
+    sendAsyncMessage('control-channel-established');
+    return mockedControlChannel;
+  },
+  set listener(listener) {
+    this._listener = listener;
+  },
+  get listener() {
+    return this._listener;
+  },
+  simulateSessionRequest: function(url, presentationId, controlChannel) {
+    this._listener.onSessionRequest(this, url, presentationId, controlChannel);
+  },
+};
+
+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 mockedSessionTransport = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationSessionTransport,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  set callback(callback) {
+    this._callback = callback;
+  },
+  get callback() {
+    return this._callback;
+  },
+  get selfAddress() {
+    return this._selfAddress;
+  },
+  initWithSocketTransport: function(transport, callback) {
+    sendAsyncMessage('data-transport-initialized');
+    this._callback = callback;
+    this.simulateTransportReady();
+  },
+  initWithChannelDescription: function(description, callback) {
+    this._callback = callback;
+
+    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,
+    };
+  },
+  send: function(data) {
+    var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
+                       createInstance(Ci.nsIBinaryInputStream);
+    binaryStream.setInputStream(data);
+    var message = binaryStream.readBytes(binaryStream.available());
+    sendAsyncMessage('message-sent', message);
+  },
+  close: function(reason) {
+    sendAsyncMessage('data-transport-closed', reason);
+    this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportClosed(reason);
+  },
+  simulateTransportReady: function() {
+    this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyTransportReady();
+  },
+  simulateIncomingMessage: function(message) {
+    this._callback.QueryInterface(Ci.nsIPresentationSessionTransportCallback).notifyData(message);
+  },
+};
+
+const mockedNetworkInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo]),
+  getAddresses: function(ips, prefixLengths) {
+    ips.value = ["127.0.0.1"];
+    prefixLengths.value = [0];
+    return 1;
+  },
+};
+
+const mockedNetworkManager = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkManager,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  get activeNetworkInfo() {
+    return mockedNetworkInfo;
+  },
+};
+
+var requestPromise = null;
+
+const mockedRequestUIGlue = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue,
+                                         Ci.nsIFactory]),
+  createInstance: function(aOuter, aIID) {
+    if (aOuter) {
+      throw Components.results.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(aIID);
+  },
+  sendRequest: function(aUrl, aSessionId) {
+    sendAsyncMessage('receiver-launching', aSessionId);
+    return requestPromise;
+  },
+};
+
+// Register mocked factories.
+const 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(),
+                                               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 in 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() {
+  mockedDevicePrompt.simulateCancel();
+});
+
+addMessageListener('trigger-incoming-session-request', function(url) {
+  mockedDevice.simulateSessionRequest(url, sessionId, mockedControlChannel);
+});
+
+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-close', function(reason) {
+  mockedControlChannel.close(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 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_receiver.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for B2G Presentation Session API at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript;version=1.7">
+
+"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 session;
+
+function testSessionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    ok(navigator.presentation, "navigator.presentation should be available.");
+
+    session = navigator.presentation.session;
+    ok(session.id, "Session ID should be set: " + session.id);
+    is(session.state, "disconnected", "Session state at receiver side should be disconnected by default.");
+    aResolve();
+  });
+}
+
+function testSessionReady() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "connected", "Session state should become connected.");
+      aResolve();
+    };
+
+    command({ name: 'trigger-incoming-offer' });
+  });
+}
+
+function testCloseSession() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "terminated", "Session should be terminated.");
+      aResolve();
+    };
+
+    session.close();
+  });
+}
+
+testSessionAvailable().
+then(testSessionReady).
+then(testCloseSession).
+then(finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_oop.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for B2G Presentation Session API at receiver side (OOP)</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript;version=1.7">
+
+"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 session;
+
+function testSessionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    ok(navigator.presentation, "navigator.presentation should be available.");
+
+    session = navigator.presentation.session;
+    ok(session.id, "Session ID should be set: " + session.id);
+    is(session.state, "disconnected", "Session state at receiver side should be disconnected by default.");
+    aResolve();
+  });
+}
+
+function testSessionReady() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "connected", "Session state should become connected.");
+      aResolve();
+    };
+
+    command({ name: 'trigger-incoming-offer' });
+  });
+}
+
+function testCloseSession() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "terminated", "Session should be terminated.");
+      aResolve();
+    };
+
+    session.close();
+  });
+}
+
+testSessionAvailable().
+then(testSessionReady).
+then(testCloseSession).
+then(finish);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_receiver_start_session_error.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for startSession errors of B2G Presentation API at receiver side</title>
+</head>
+<body>
+<div id="content"></div>
+<script type="application/javascript;version=1.7">
+
+"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 session;
+
+function testSessionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    ok(navigator.presentation, "navigator.presentation should be available.");
+
+    session = navigator.presentation.session;
+    ok(session.id, "Session ID should be set: " + session.id);
+    is(session.state, "disconnected", "Session state at receiver side should be disconnected by default.");
+    aResolve();
+  });
+}
+
+function testUnexpectedControlChannelClose() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "terminated", "Session state should become terminated.");
+      aResolve();
+    };
+
+    // Trigger the control channel to be closed with error code.
+    command({ name: 'trigger-control-channel-close', data: 0x80004004 /* NS_ERROR_ABORT */ });
+  });
+}
+
+testSessionAvailable().
+then(testUnexpectedControlChannelClose).
+then(finish);
+
+</script>
+</body>
+</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -1,6 +1,24 @@
 [DEFAULT]
 support-files =
   PresentationDeviceInfoChromeScript.js
+  PresentationSessionChromeScript.js
+  file_presentation_receiver.html
+  file_presentation_receiver_oop.html
+  file_presentation_receiver_start_session_error.html
 
 [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_start_session_error.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_sender.html]
+skip-if = toolkit == 'android' # Bug 1129785
+[test_presentation_receiver_start_session_error.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
+[test_presentation_receiver_start_session_timeout.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
+[test_presentation_receiver.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
+[test_presentation_receiver_oop.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver.html
@@ -0,0 +1,122 @@
+<!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 Session API at receiver side</title>
+  <script type="application/javascript" 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 Presentation Session 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.Cc["@mozilla.org/observer-service;1"]
+          .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage('trigger-device-add');
+
+    var iframe = document.createElement('iframe');
+    iframe.setAttribute('src', receiverUrl);
+
+    // This event is triggered when the iframe calls "postMessage".
+    window.addEventListener('message', function listener(aEvent) {
+      var message = aEvent.data;
+      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.");
+        window.removeEventListener('message', listener);
+
+        teardown();
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(iframe);
+
+      aResolve(iframe);
+    });
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+
+    gScript.addMessageListener('offer-received', function offerReceivedHandler() {
+      gScript.removeMessageListener('offer-received', offerReceivedHandler);
+      info("An offer is received.");
+    });
+
+    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-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.");
+
+      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.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", 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_receiver_oop.html
@@ -0,0 +1,131 @@
+<!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 Session API at receiver side (OOP)</title>
+  <script type="application/javascript" 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 Presentation Session 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_oop.html');
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+          .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage('trigger-device-add');
+
+    SpecialPowers.addPermission('presentation', true, { url: receiverUrl,
+                                                        appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
+                                                        isInBrowserElement: true });
+
+    var iframe = document.createElement('iframe');
+    iframe.setAttribute('remote', 'true');
+    iframe.setAttribute('mozbrowser', 'true');
+    iframe.setAttribute('src', receiverUrl);
+
+    // This event is triggered when the iframe calls "alert".
+    iframe.addEventListener('mozbrowsershowmodalprompt', function listener(aEvent) {
+      var message = aEvent.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, "Message from iframe: " + message);
+      } else if (/^KO /.exec(message)) {
+        ok(false, "Message from iframe: " + message);
+      } else if (/^INFO /.exec(message)) {
+        info("Message from iframe: " + message);
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, "Messaging from iframe complete.");
+        iframe.removeEventListener('mozbrowsershowmodalprompt', listener);
+
+        teardown();
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(iframe);
+
+      aResolve(iframe);
+    });
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+
+    gScript.addMessageListener('offer-received', function offerReceivedHandler() {
+      gScript.removeMessageListener('offer-received', offerReceivedHandler);
+      info("An offer is received.");
+    });
+
+    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-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.");
+
+      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.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+  {type: 'browser', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", 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>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_receiver_start_session_error.html
@@ -0,0 +1,110 @@
+<!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 startSession errors of B2G Presentation API at receiver 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=1069230">Test for startSession errors of B2G Presentation API at receiver side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_receiver_start_session_error.html');
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+          .getService(SpecialPowers.Ci.nsIObserverService);
+var session;
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage('trigger-device-add');
+
+    var iframe = document.createElement('iframe');
+    iframe.setAttribute('src', receiverUrl);
+
+    // This event is triggered when the iframe calls "postMessage".
+    window.addEventListener('message', function listener(aEvent) {
+      var message = aEvent.data;
+      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.");
+        window.removeEventListener('message', listener);
+
+        teardown();
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(iframe);
+
+      aResolve(iframe);
+    });
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+
+    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.");
+    });
+
+    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);
+  });
+}
+
+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.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", 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_receiver_start_session_timeout.html
@@ -0,0 +1,84 @@
+<!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 startSession timeout of B2G Presentation API at receiver 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=1069230">Test for startSession timeout of B2G Presentation API at receiver side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+
+var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+          .getService(SpecialPowers.Ci.nsIObserverService);
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.sendAsyncMessage('trigger-device-add');
+
+    var promise = new Promise(function(aResolve, aReject) {
+      // In order to trigger timeout, do not resolve the promise.
+    });
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+
+    aResolve();
+  });
+}
+
+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().
+  then(testIncomingSessionRequestReceiverLaunchTimeout).
+  then(teardown);
+}
+
+SimpleTest.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", true],
+                                      ["dom.presentation.test.enabled", true],
+                                      ["dom.presentation.test.stage", 0],
+                                      ["presentation.receiver.loading.timeout", 10]]},
+                            runTests);
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender.html
@@ -0,0 +1,167 @@
+<!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 type="application/javascript" src="/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;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var presentation;
+var session;
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    presentation.onavailablechange = function(aIsAvailable) {
+      presentation.onavailablechange = null;
+      ok(aIsAvailable, "Device should be available.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartSession() {
+  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.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');
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        session = aSession;
+        ok(session, "Session should be availlable.");
+        ok(session.id, "Session ID should be set.");
+        is(session.state, "connected", "Session state at sender side should be connected by default.");
+        aResolve();
+      },
+      function(aError) {
+        ok(false, "Error occurred when starting session: " + 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();
+    });
+
+    session.send(outgoingMessage);
+  });
+}
+
+function testIncomingMessage() {
+  return new Promise(function(aResolve, aReject) {
+    const incomingMessage = "test incoming message";
+
+    session.addEventListener('message', function messageHandler(aEvent) {
+      session.removeEventListener('message', messageHandler);
+      is(aEvent.data, incomingMessage, "An incoming message should be received.");
+      aResolve();
+    });
+
+    gScript.sendAsyncMessage('trigger-incoming-message', incomingMessage);
+  });
+}
+
+function testCloseSession() {
+  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);
+    });
+
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "terminated", "Session should be terminated.");
+      aResolve();
+    };
+
+    session.close();
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  ok(navigator.presentation, "navigator.presentation should be available.");
+  presentation = navigator.presentation;
+
+  testSetup().
+  then(testStartSession).
+  then(testSend).
+  then(testIncomingMessage).
+  then(testCloseSession).
+  then(teardown);
+}
+
+SimpleTest.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", 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_sender_disconnect.html
@@ -0,0 +1,150 @@
+<!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 session disconnection of B2G Presentation API at sender 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=1069230">Test for session disconnection of B2G Presentation API at sender side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var presentation;
+var session;
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    presentation.onavailablechange = function(aIsAvailable) {
+      presentation.onavailablechange = null;
+      ok(aIsAvailable, "Device should be available.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartSession() {
+  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.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-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.");
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        session = aSession;
+        ok(session, "Session should be availlable.");
+        ok(session.id, "Session ID should be set.");
+        is(session.state, "connected", "Session state at sender side should be connected by default.");
+        aResolve();
+      },
+      function(aError) {
+        ok(false, "Error occurred when starting session: " + aError);
+        teardown();
+        aReject();
+      }
+    );
+  });
+}
+
+function testSessionDisconnection() {
+  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);
+    });
+
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "disconnected", "Session should be disconnected.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-data-transport-close', SpecialPowers.Cr.NS_ERROR_FAILURE);
+  });
+}
+
+function testCloseSession() {
+  return new Promise(function(aResolve, aReject) {
+    session.onstatechange = function() {
+      session.onstatechange = null;
+      is(session.state, "terminated", "Session should be terminated.");
+      aResolve();
+    };
+
+    session.close();
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  ok(navigator.presentation, "navigator.presentation should be available.");
+  presentation = navigator.presentation;
+
+  testSetup().
+  then(testStartSession).
+  then(testSessionDisconnection).
+  then(testCloseSession).
+  then(teardown);
+}
+
+SimpleTest.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", 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_sender_start_session_error.html
@@ -0,0 +1,239 @@
+<!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 startSession errors of B2G Presentation API at sender 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=1069230">Test for startSession errors of B2G Presentation API at sender side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var presentation;
+
+function setup() {
+  return new Promise(function(aResolve, aReject) {
+    presentation.onavailablechange = function(aIsAvailable) {
+      presentation.onavailablechange = null;
+      ok(aIsAvailable, "Device should be available.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartSessionNoAvailableDevice() {
+  return new Promise(function(aResolve, aReject) {
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        ok(false, "startSession shouldn't succeed in this case.");
+        aReject();
+      },
+      function(aError) {
+        is(aError.name, "InvalidStateError", "InvalidStateError is expected when starting session.");
+        aResolve();
+      }
+    );
+  });
+}
+
+function testStartSessionCancelPrompt() {
+  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-cancel');
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        ok(false, "startSession shouldn't succeed in this case.");
+        aReject();
+      },
+      function(aError) {
+        is(aError.name, "NS_ERROR_DOM_PROP_ACCESS_DENIED", "NS_ERROR_DOM_PROP_ACCESS_DENIED is expected when starting session.");
+        aResolve();
+      }
+    );
+  });
+}
+
+function testStartSessionUnexpectedControlChannelCloseBeforeDataTransportInit() {
+  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.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-control-channel-close', SpecialPowers.Cr.NS_ERROR_FAILURE);
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        ok(false, "startSession shouldn't succeed in this case.");
+        aReject();
+      },
+      function(aError) {
+        is(aError.name, "NS_ERROR_FAILURE", "NS_ERROR_FAILURE is expected when starting session.");
+        aResolve();
+      }
+    );
+  });
+}
+
+function testStartSessionUnexpectedControlChannelCloseBeforeDataTransportReady() {
+  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.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('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);
+    });
+
+    gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler);
+      info("The data transport is closed. " + aReason);
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        ok(false, "startSession shouldn't succeed in this case.");
+        aReject();
+      },
+      function(aError) {
+        is(aError.name, "NS_ERROR_ABORT", "NS_ERROR_ABORT is expected when starting session.");
+        aResolve();
+      }
+    );
+  });
+}
+
+function testStartSessionUnexpectedDataTransportClose() {
+  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.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('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);
+    });
+
+    gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler);
+      info("The data transport is closed. " + aReason);
+    });
+
+    presentation.startSession("http://example.com").then(
+      function(aSession) {
+        ok(false, "startSession shouldn't succeed in this case.");
+        aReject();
+      },
+      function(aError) {
+        is(aError.name, "NS_ERROR_UNEXPECTED", "NS_ERROR_UNEXPECTED is expected when starting session.");
+        aResolve();
+      }
+    );
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  ok(navigator.presentation, "navigator.presentation should be available.");
+  presentation = navigator.presentation;
+
+  testStartSessionNoAvailableDevice().
+  then(setup).
+  then(testStartSessionCancelPrompt).
+  then(testStartSessionUnexpectedControlChannelCloseBeforeDataTransportInit).
+  then(testStartSessionUnexpectedControlChannelCloseBeforeDataTransportReady).
+  then(testStartSessionUnexpectedDataTransportClose).
+  then(teardown);
+}
+
+SimpleTest.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.ignore_webidl_scope_checks", true],
+                                      ["dom.presentation.test.enabled", true],
+                                      ["dom.presentation.test.stage", 0]]},
+                            runTests);
+});
+
+</script>
+</body>
+</html>