Merge autoland to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Fri, 25 Nov 2016 21:25:18 -0800
changeset 324310 180c7af5922ee89f848c25c3847bd1fb22a71e37
parent 324296 ee388da4aeb6b686a04c16d11ca8606ee2b3e8e0 (current diff)
parent 324309 4e9afb28d651b41e2ee1354ea664550a61936901 (diff)
child 324331 f8f4eaac1701107f794b48891bcca2c95d39d503
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersmerge
milestone53.0a1
Merge autoland to m-c, a=merge
testing/marionette/harness/docs/Makefile
testing/marionette/harness/docs/advanced/actions.rst
testing/marionette/harness/docs/advanced/debug.rst
testing/marionette/harness/docs/advanced/findelement.rst
testing/marionette/harness/docs/advanced/landing.rst
testing/marionette/harness/docs/advanced/stale.rst
testing/marionette/harness/docs/basics.rst
testing/marionette/harness/docs/conf.py
testing/marionette/harness/docs/index.rst
testing/marionette/harness/docs/interactive.rst
testing/marionette/harness/docs/make.bat
testing/marionette/harness/docs/reference.rst
testing/web-platform/meta/webvtt/webvtt-api-for-browsers/vttcue-interface/line.html.ini
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -231,16 +231,17 @@ const gXPInstallObserver = {
     var messageString, action;
     var brandShortName = brandBundle.getString("brandShortName");
 
     var notificationID = aTopic;
     // Make notifications persistent
     var options = {
       displayURI: installInfo.originatingURI,
       persistent: true,
+      hideClose: true,
       timeout: Date.now() + 30000,
     };
 
     switch (aTopic) {
     case "addon-install-disabled": {
       notificationID = "xpinstall-disabled";
 
       if (gPrefService.prefIsLocked("xpinstall.enabled")) {
@@ -444,16 +445,17 @@ const gXPInstallObserver = {
       messageString = messageString.replace("#1", installInfo.installs[0].name);
       messageString = messageString.replace("#2", installInfo.installs.length);
       messageString = messageString.replace("#3", brandShortName);
 
       // Remove notificaion on dismissal, since it's possible to cancel the
       // install through the addons manager UI, making the "restart" prompt
       // irrelevant.
       options.removeOnDismissal = true;
+      options.persistent = false;
 
       PopupNotifications.show(browser, notificationID, messageString, anchorID,
                               action, null, options);
       break; }
     }
   },
   _removeProgressNotification(aBrowser) {
     let notification = PopupNotifications.getNotification("addon-progress", aBrowser);
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -820,17 +820,17 @@ function test_urlBar() {
     gBrowser.selectedTab = gBrowser.addTab("about:blank");
     yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
     gURLBar.value = TESTROOT + "amosigned.xpi";
     gURLBar.focus();
     EventUtils.synthesizeKey("VK_RETURN", {});
     let panel = yield notificationPromise;
 
     let notification = panel.childNodes[0];
-    is(notification.button.label, "", "Button to allow install should be hidden.");
+    ok(!notification.hasAttribute("buttonlabel"), "Button to allow install should be hidden.");
     yield removeTab();
   });
 },
 
 function test_wrongHost() {
   return Task.spawn(function* () {
     let requestedUrl = TESTROOT2 + "enabled.html";
     gBrowser.selectedTab = gBrowser.addTab();
--- a/browser/base/content/test/popupNotifications/browser_popupNotification.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js
@@ -198,33 +198,56 @@ var tests = [
       ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked");
       ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called");
 
       ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked");
       ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked");
       ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called");
     }
   },
-  // Test notification without mainAction
+  // Test notification without mainAction or secondaryActions, it should fall back
+  // to a default button that dismisses the notification in place of the main action.
   { id: "Test#9",
     run: function() {
       this.notifyObj = new BasicNotification(this.id);
       this.notifyObj.mainAction = null;
+      this.notifyObj.secondaryActions = null;
+      this.notification = showNotification(this.notifyObj);
+    },
+    onShown: function(popup) {
+      triggerMainCommand(popup);
+    },
+    onHidden: function(popup) {
+      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
+    }
+  },
+  // Test notification without mainAction but with secondaryActions, it should fall back
+  // to a default button that dismisses the notification in place of the main action
+  // and ignore the passed secondaryActions.
+  { id: "Test#10",
+    run: function() {
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.mainAction = null;
       this.notification = showNotification(this.notifyObj);
     },
     onShown: function(popup) {
-      checkPopup(popup, this.notifyObj);
-      dismissNotification(popup);
+      let notification = popup.childNodes[0];
+      is(notification.getAttribute("secondarybuttonhidden"), "true", "secondary button is hidden");
+      triggerMainCommand(popup);
     },
     onHidden: function(popup) {
-      this.notification.remove();
+      ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked");
+      ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered");
+      ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered");
     }
   },
   // Test two notifications with different anchors
-  { id: "Test#10",
+  { id: "Test#11",
     run: function() {
       this.notifyObj = new BasicNotification(this.id);
       this.firstNotification = showNotification(this.notifyObj);
       this.notifyObj2 = new BasicNotification(this.id);
       this.notifyObj2.id += "-2";
       this.notifyObj2.anchorID = "addons-notification-icon";
       // Second showNotification() overrides the first
       this.secondNotification = showNotification(this.notifyObj2);
--- a/devtools/client/shared/components/reps/attribute.js
+++ b/devtools/client/shared/components/reps/attribute.js
@@ -28,25 +28,25 @@ define(function (require, exports, modul
       object: React.PropTypes.object.isRequired
     },
 
     getTitle: function (grip) {
       return grip.preview.nodeName;
     },
 
     render: function () {
-      let grip = this.props.object;
-      let value = grip.preview.value;
+      let object = this.props.object;
+      let value = object.preview.value;
       let objectLink = this.props.objectLink || span;
 
       return (
-        objectLink({className: "objectLink-Attr"},
+        objectLink({className: "objectLink-Attr", object},
           span({},
             span({className: "attrTitle"},
-              this.getTitle(grip)
+              this.getTitle(object)
             ),
             span({className: "attrEqual"},
               "="
             ),
             StringRepFactory({object: value})
           )
         )
       );
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -1074,29 +1074,37 @@ NetworkMonitor.prototype = {
     event.fromServiceWorker = fromServiceWorker;
     httpActivity.fromServiceWorker = fromServiceWorker;
 
     if (extraStringData) {
       event.headersSize = extraStringData.length;
     }
 
     // Determine the cause and if this is an XHR request.
-    let causeType = channel.loadInfo.externalContentPolicyType;
-    let loadingPrincipal = channel.loadInfo.loadingPrincipal;
-    let causeUri = loadingPrincipal ? loadingPrincipal.URI : null;
+    let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
+    let causeUri = null;
     let stacktrace;
+
+    if (channel.loadInfo) {
+      causeType = channel.loadInfo.externalContentPolicyType;
+      const { loadingPrincipal } = channel.loadInfo;
+      if (loadingPrincipal && loadingPrincipal.URI) {
+        causeUri = loadingPrincipal.URI.spec;
+      }
+    }
+
     // If this is the parent process, there is no stackTraceCollector - the stack
     // trace will be added in NetworkMonitorChild._onNewEvent.
     if (this.owner.stackTraceCollector) {
       stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId);
     }
 
     event.cause = {
       type: causeType,
-      loadingDocumentUri: causeUri ? causeUri.spec : null,
+      loadingDocumentUri: causeUri,
       stacktrace
     };
 
     httpActivity.isXHR = event.isXHR =
         (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
          causeType === Ci.nsIContentPolicy.TYPE_FETCH);
 
     // Determine the HTTP version.
--- a/dom/base/test/test_audioNotificationSilent_audioFile.html
+++ b/dom/base/test/test_audioNotificationSilent_audioFile.html
@@ -7,29 +7,27 @@
 </head>
 <body>
 <script type="application/javascript;version=1.7">
 
 SimpleTest.waitForExplicitFinish();
 
 var generator = runTest();
 var expectedPlaybackActive = null;
-var expectedPlaying = null;
 
 var audio = new Audio();
 audio.src = "audioEndedDuringPlaying.webm";
 
 var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
                                    .getService(SpecialPowers.Ci.nsIObserverService);
 
 var observer = {
   observe: function(subject, topic, data) {
     is(topic, "audio-playback", "audio-playback received");
     is(data, expectedPlaybackActive, "Corrrect audible state");
-    is(!audio.ended, expectedPlaying, "Corrrect playing state");
     continueTest();
   }
 };
 
 function continueTest() {
   try {
     generator.next();
   } catch (e if e instanceof StopIteration) {
@@ -37,27 +35,25 @@ function continueTest() {
   }
 }
 
 function audioPlayingStart() {
   observerService.addObserver(observer, "audio-playback", false);
   ok(true, "Observer set");
 
   expectedPlaybackActive = 'active';
-  expectedPlaying = true;
 
   info("Audio playing start");
   audio.play();
 }
 
 function audioBecomeSilentDuringPlaying() {
   info("Audio would become silent during playing");
 
   expectedPlaybackActive = 'inactive-nonaudible';
-  expectedPlaying = true;
 }
 
 function finish() {
   observerService.removeObserver(observer, "audio-playback");
   ok(true, "Observer removed");
 
   SimpleTest.finish();
 }
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -1,14 +1,15 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/app"
 
 apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
 apply plugin: 'com.android.application'
 apply plugin: 'checkstyle'
 apply plugin: 'com.getkeepsafe.dexcount'
+apply plugin: 'findbugs'
 
 dexcount {
     format = "tree"
 }
 
 android {
     compileSdkVersion 23
     buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
@@ -409,8 +410,34 @@ def makeTaskExecutionListener(artifactRo
 }
 
 // TASK_ID and RUN_ID are provided by docker-worker; see
 // https://docs.taskcluster.net/manual/execution/workers/docker-worker.
 if (System.env.TASK_ID && System.env.RUN_ID) {
     def artifactRootUrl = "https://queue.taskcluster.net/v1/task/${System.env.TASK_ID}/runs/${System.env.RUN_ID}/artifacts"
     gradle.addListener(makeTaskExecutionListener(artifactRootUrl))
 }
+
+// Bug 1320035: Gradle configuration for running findbugs
+android.applicationVariants.all { variant ->
+    task("findbugs${variant.name.capitalize()}", type: FindBugs) {
+        description "Analyze ${variant.name} code with findbugs"
+        group "Verification"
+
+        ignoreFailures = false // We want builds to fail when running this task and issues are found
+        effort = "max"         // Using more memory and time to find issues is acceptable in automation
+        reportLevel = "high"   // For now we only care about high priority bugs. After we have fixed
+                               // the issues with medium/low priority we can lower the report level here.
+
+        classes = files("$project.buildDir/intermediates/classes")
+        source = variant.javaCompile.source
+        classpath = variant.javaCompile.classpath
+
+        reports {
+            html.enabled = true // We only care about HTML reports for humans
+            xml.enabled = false
+
+            html.destination = "$project.buildDir/outputs/findbugs/findbugs-${variant.name}-output.html"
+        }
+
+        dependsOn "assemble${variant.name.capitalize()}"
+    }
+}
\ No newline at end of file
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -116,16 +116,25 @@ this.BrowserIDManager.prototype = {
 
   hashedUID() {
     if (!this._hashedUID) {
       throw new Error("hashedUID: Don't seem to have previously seen a token");
     }
     return this._hashedUID;
   },
 
+  // Return a hashed version of a deviceID, suitable for telemetry.
+  hashedDeviceID(deviceID) {
+    let uid = this.hashedUID();
+    // Combine the raw device id with the metrics uid to create a stable
+    // unique identifier that can't be mapped back to the user's FxA
+    // identity without knowing the metrics HMAC key.
+    return Utils.sha256(deviceID + uid);
+  },
+
   deviceID() {
     return this._signedInUser && this._signedInUser.deviceId;
   },
 
   initialize: function() {
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.addObserver(this, topic, false);
     }
--- a/services/sync/modules/telemetry.js
+++ b/services/sync/modules/telemetry.js
@@ -263,39 +263,36 @@ class TelemetryRecord {
     }
     if (error) {
       this.failureReason = transformError(error);
     }
 
     // We don't bother including the "devices" field if we can't come up with a
     // UID or device ID for *this* device -- If that's the case, any data we'd
     // put there would be likely to be full of garbage anyway.
+    // Note that we currently use the "sync device GUID" rather than the "FxA
+    // device ID" as the latter isn't stable enough for our purposes - see bug
+    // 1316535.
     let includeDeviceInfo = false;
     try {
       this.uid = Weave.Service.identity.hashedUID();
-      let deviceID = Weave.Service.identity.deviceID();
-      if (deviceID) {
-        // Combine the raw device id with the metrics uid to create a stable
-        // unique identifier that can't be mapped back to the user's FxA
-        // identity without knowing the metrics HMAC key.
-        this.deviceID = Utils.sha256(deviceID + this.uid);
-        includeDeviceInfo = true;
-      }
+      this.deviceID = Weave.Service.identity.hashedDeviceID(Weave.Service.clientsEngine.localID);
+      includeDeviceInfo = true;
     } catch (e) {
       this.uid = "0".repeat(32);
       this.deviceID = undefined;
     }
 
     if (includeDeviceInfo) {
       let remoteDevices = Weave.Service.clientsEngine.remoteClients;
       this.devices = remoteDevices.map(device => {
         return {
           os: device.os,
           version: device.version,
-          id: Utils.sha256(device.id + this.uid)
+          id: Weave.Service.identity.hashedDeviceID(device.id),
         };
       });
     }
 
     // Check for engine statuses. -- We do this now, and not in engine.finished
     // to make sure any statuses that get set "late" are recorded
     for (let engine of this.engines) {
       let status = Status.engines[engine.name];
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -168,17 +168,19 @@ add_test(function test_prefAttributes() 
 add_identity_test(this, async function test_updateClientMode() {
   _("Test updateClientMode adjusts scheduling attributes based on # of clients appropriately");
   do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
   do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
   do_check_false(scheduler.numClients > 1);
   do_check_false(scheduler.idle);
 
   // Trigger a change in interval & threshold by adding a client.
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
   scheduler.updateClientMode();
 
   do_check_eq(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD);
   do_check_eq(scheduler.syncInterval, scheduler.activeInterval);
   do_check_true(scheduler.numClients > 1);
   do_check_false(scheduler.idle);
 
   // Resets the number of clients to 0.
@@ -433,28 +435,33 @@ add_identity_test(this, async function t
   await setUp(server);
 
   // Confirm defaults.
   do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
   do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
   do_check_false(scheduler.idle);
 
   // Trigger a change in interval & threshold by adding a client.
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
   do_check_false(scheduler.numClients > 1);
   scheduler.updateClientMode();
   Service.sync();
 
   do_check_eq(scheduler.syncThreshold, MULTI_DEVICE_THRESHOLD);
   do_check_eq(scheduler.syncInterval, scheduler.activeInterval);
   do_check_true(scheduler.numClients > 1);
   do_check_false(scheduler.idle);
 
   // Resets the number of clients to 0.
   clientsEngine.resetClient();
+  // Also re-init the server, or we suck our "foo" client back down.
+  await setUp(server);
+
   Service.sync();
 
   // Goes back to single user if # clients is 1.
   do_check_eq(scheduler.numClients, 1);
   do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
   do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
   do_check_false(scheduler.numClients > 1);
   do_check_false(scheduler.idle);
@@ -596,17 +603,19 @@ add_identity_test(this, async function t
 
   // Single device: nothing changes.
   scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime"));
   do_check_eq(scheduler.idle, true);
   do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
 
   // Multiple devices: switch to idle interval.
   scheduler.idle = false;
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
   scheduler.updateClientMode();
   scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime"));
   do_check_eq(scheduler.idle, true);
   do_check_eq(scheduler.syncInterval, scheduler.idleInterval);
 
   await cleanUpAndGo();
 });
 
@@ -736,17 +745,19 @@ add_identity_test(this, async function t
   scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
   let server = sync_httpd_setup();
 
   let engine = Service.engineManager.get("catapult");
   engine.enabled = true;
   engine.exception = {status: 400};
 
   // Have multiple devices for an active interval.
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
 
   do_check_eq(Status.sync, SYNC_SUCCEEDED);
 
   do_check_true(await setUp(server));
 
   Service.sync();
 
   do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
@@ -778,17 +789,19 @@ add_identity_test(this, async function t
       response.setHeader("X-Weave-Backoff", "" + BACKOFF);
     }
     infoColl(request, response);
   }
   server.registerPathHandler(INFO_COLLECTIONS, infoCollWithBackoff);
 
   // Pretend we have two clients so that the regular sync interval is
   // sufficiently low.
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
   let rec = clientsEngine._store.createRecord("foo", "clients");
   rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
   rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
 
   // Sync once to log in and get everything set up. Let's verify our initial
   // values.
   Service.sync();
   do_check_eq(Status.backoffInterval, 0);
@@ -835,17 +848,19 @@ add_identity_test(this, async function t
     }
     response.setHeader("Retry-After", "" + BACKOFF);
     response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
   }
   server.registerPathHandler(INFO_COLLECTIONS, infoCollWithMaintenance);
 
   // Pretend we have two clients so that the regular sync interval is
   // sufficiently low.
-  clientsEngine._store.create({id: "foo", cleartext: "bar"});
+  clientsEngine._store.create(
+    { id: "foo", cleartext: { os: "mobile", version: "0.01", type: "desktop" } }
+  );
   let rec = clientsEngine._store.createRecord("foo", "clients");
   rec.encrypt(Service.collectionKeys.keyForCollection("clients"));
   rec.upload(Service.resource(clientsEngine.engineURL + rec.id));
 
   // Sync once to log in and get everything set up. Let's verify our initial
   // values.
   Service.sync();
   do_check_false(Status.enforceBackoff);
rename from testing/marionette/harness/docs/Makefile
rename to testing/marionette/client/docs/Makefile
rename from testing/marionette/harness/docs/advanced/actions.rst
rename to testing/marionette/client/docs/advanced/actions.rst
rename from testing/marionette/harness/docs/advanced/debug.rst
rename to testing/marionette/client/docs/advanced/debug.rst
rename from testing/marionette/harness/docs/advanced/findelement.rst
rename to testing/marionette/client/docs/advanced/findelement.rst
rename from testing/marionette/harness/docs/advanced/landing.rst
rename to testing/marionette/client/docs/advanced/landing.rst
rename from testing/marionette/harness/docs/advanced/stale.rst
rename to testing/marionette/client/docs/advanced/stale.rst
rename from testing/marionette/harness/docs/basics.rst
rename to testing/marionette/client/docs/basics.rst
--- a/testing/marionette/harness/docs/basics.rst
+++ b/testing/marionette/client/docs/basics.rst
@@ -1,14 +1,14 @@
 .. py:currentmodule:: marionette_driver.marionette
 
 Marionette Python Client
 ========================
 
-The Marionette python client library allows you to remotely control a
+The Marionette Python client library allows you to remotely control a
 Gecko-based browser or device which is running a Marionette_
 server. This includes Firefox Desktop and Firefox for Android.
 
 The Marionette server is built directly into Gecko and can be started by
 passing in a command line option to Gecko, or by using a Marionette-enabled
 build. The server listens for connections from various clients. Clients can
 then control Gecko by sending commands to the server.
 
@@ -16,21 +16,21 @@ This is the official Python client for M
 `NodeJS client`_ maintained by the Firefox OS automation team.
 
 .. _Marionette: https://developer.mozilla.org/en-US/docs/Marionette
 .. _NodeJS client: https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette
 
 Getting the Client
 ------------------
 
-The python client is officially supported. To install it, first make sure you
+The Python client is officially supported. To install it, first make sure you
 have `pip installed`_ then run:
 
 .. parsed-literal::
-   pip install marionette_client
+   pip install marionette_driver
 
 It's highly recommended to use virtualenv_ when installing Marionette to avoid
 package conflicts and other general nastiness.
 
 You should now be ready to start using Marionette. The best way to learn is to
 play around with it. Start a `Marionette-enabled instance of Firefox`_, fire up
 a python shell and follow along with the
 :doc:`interactive tutorial <interactive>`!
@@ -49,16 +49,18 @@ testing with Marionette.
 
 Session Management
 ------------------
 A session is a single instance of a Marionette client connected to a Marionette
 server. Before you can start executing commands, you need to start a session
 with :func:`start_session() <Marionette.start_session>`:
 
 .. parsed-literal::
+   from marionette_driver.marionette import Marionette
+
    client = Marionette('localhost', port=2828)
    client.start_session()
 
 This returns a session id and an object listing the capabilities of the
 Marionette server. For example, a server running on Firefox Desktop will
 have some features which a server running from Firefox Android won't.
 It's also possible to access the capabilities using the
 :attr:`~Marionette.session_capabilities` attribute. After finishing with a
rename from testing/marionette/harness/docs/conf.py
rename to testing/marionette/client/docs/conf.py
rename from testing/marionette/harness/docs/index.rst
rename to testing/marionette/client/docs/index.rst
rename from testing/marionette/harness/docs/interactive.rst
rename to testing/marionette/client/docs/interactive.rst
rename from testing/marionette/harness/docs/make.bat
rename to testing/marionette/client/docs/make.bat
rename from testing/marionette/harness/docs/reference.rst
rename to testing/marionette/client/docs/reference.rst
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -12,46 +12,103 @@ from copy import deepcopy
 import mozversion
 
 from mozprofile import Profile
 from mozrunner import Runner, FennecEmulatorRunner
 
 
 class GeckoInstance(object):
     required_prefs = {
-        "browser.sessionstore.resume_from_crash": False,
-        "browser.shell.checkDefaultBrowser": False,
-        # Needed for branded builds to prevent opening a second tab on startup
-        "browser.startup.homepage_override.mstone": "ignore",
-        "browser.startup.page": 0,
-        "browser.tabs.remote.autostart.1": False,
-        "browser.tabs.remote.autostart.2": False,
-        "browser.tabs.remote.autostart": False,
-        "browser.urlbar.userMadeSearchSuggestionsChoice": True,
-        "browser.warnOnQuit": False,
-        "datareporting.healthreport.logging.consoleEnabled": False,
-        "datareporting.healthreport.service.enabled": False,
-        "datareporting.healthreport.service.firstRun": False,
-        "datareporting.healthreport.uploadEnabled": False,
-        "datareporting.policy.dataSubmissionEnabled": False,
-        "datareporting.policy.dataSubmissionPolicyAccepted": False,
-        "dom.ipc.reportProcessHangs": False,
-        # Only install add-ons from the profile and the application scope
-        # Also ensure that those are not getting disabled.
-        # see: https://developer.mozilla.org/en/Installing_extensions
-        "extensions.enabledScopes": 5,
-        "extensions.autoDisableScopes": 10,
-        "focusmanager.testmode": True,
-        "marionette.defaultPrefs.enabled": True,
-        "startup.homepage_welcome_url": "",
-        "startup.homepage_welcome_url.additional": "",
-        "toolkit.telemetry.enabled": False,
+        # Increase the APZ content response timeout in tests to 1 minute.
+        # This is to accommodate the fact that test environments tends to be slower
+        # than production environments (with the b2g emulator being the slowest of them
+        # all), resulting in the production timeout value sometimes being exceeded
+        # and causing false-positive test failures. See bug 1176798, bug 1177018,
+        # bug 1210465.
+        "apz.content_response_timeout": 60000,
+
+        # Do not send Firefox health reports to the production server
+        "datareporting.healthreport.documentServerURI": "http://%(server)s/dummy/healthreport/",
+        "datareporting.healthreport.about.reportUrl": "http://%(server)s/dummy/abouthealthreport/",
+
+        # Do not show datareporting policy notifications which can interfer with tests
+        "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+
         # Until Bug 1238095 is fixed, we have to enable CPOWs in order
         # for Marionette tests to work properly.
         "dom.ipc.cpows.forbid-unsafe-from-browser": False,
+        "dom.ipc.reportProcessHangs": False,
+
+        # No slow script dialogs
+        "dom.max_chrome_script_run_time": 0,
+        "dom.max_script_run_time": 0,
+
+        # Only load extensions from the application and user profile
+        # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+        "extensions.autoDisableScopes": 0,
+        "extensions.enabledScopes": 5,
+        # don't block add-ons for e10s
+        "extensions.e10sBlocksEnabling": False,
+        # Disable metadata caching for installed add-ons by default
+        "extensions.getAddons.cache.enabled": False,
+        # Disable intalling any distribution add-ons
+        "extensions.installDistroAddons": False,
+        "extensions.showMismatchUI": False,
+        # Turn off extension updates so they don't bother tests
+        "extensions.update.enabled": False,
+        "extensions.update.notifyUser": False,
+        # Make sure opening about:addons won"t hit the network
+        "extensions.webservice.discoverURL": "http://%(server)s/dummy/discoveryURL",
+
+        # Allow the application to have focus even it runs in the background
+        "focusmanager.testmode": True,
+
+        # Disable useragent updates
+        "general.useragent.updates.enabled": False,
+
+        # Always use network provider for geolocation tests
+        # so we bypass the OSX dialog raised by the corelocation provider
+        "geo.provider.testing": True,
+        # Do not scan Wifi
+        "geo.wifi.scan": False,
+
+        # No hang monitor
+        "hangmonitor.timeout": 0,
+
+        "javascript.options.showInConsole": True,
+        "marionette.defaultPrefs.enabled": True,
+        "media.volume_scale": "0.01",
+
+        # Make sure the disk cache doesn't get auto disabled
+        "network.http.bypass-cachelock-threshold": 200000,
+        # Do not prompt for temporary redirects
+        "network.http.prompt-temp-redirect": False,
+        # Disable speculative connections so they aren"t reported as leaking when they"re
+        # hanging around
+        "network.http.speculative-parallel-limit": 0,
+        # Do not automatically switch between offline and online
+        "network.manage-offline-status": False,
+        # Make sure SNTP requests don't hit the network
+        "network.sntp.pools": "%(server)s",
+
+        # Tests don't wait for the notification button security delay
+        "security.notification_enable_delay": 0,
+
+        # Ensure blocklist updates don't hit the network
+        "services.settings.server": "http://%(server)s/dummy/blocklist/",
+
+        # Disable password capture, so that tests that include forms aren"t
+        # influenced by the presence of the persistent doorhanger notification
+        "signon.rememberSignons": False,
+
+        # Prevent starting into safe mode after application crashes
+        "toolkit.startup.max_resumed_crashes": -1,
+
+        # We want to collect telemetry, but we don't want to send in the results
+        "toolkit.telemetry.server": "https://%(server)s/dummy/telemetry/",
     }
 
     def __init__(self, host=None, port=None, bin=None, profile=None, addons=None,
                  app_args=None, symbols_path=None, gecko_log=None, prefs=None,
                  workspace=None, verbose=0):
         self.runner_class = Runner
         self.app_args = app_args or []
         self.runner = None
@@ -79,21 +136,21 @@ class GeckoInstance(object):
         self.verbose = verbose
 
     @property
     def gecko_log(self):
         if self._gecko_log:
             return self._gecko_log
 
         path = self._gecko_log_option
-        if path != '-':
+        if path != "-":
             if path is None:
-                path = 'gecko.log'
+                path = "gecko.log"
             elif os.path.isdir(path):
-                fname = 'gecko-{}.log'.format(time.time())
+                fname = "gecko-{}.log".format(time.time())
                 path = os.path.join(path, fname)
 
             path = os.path.realpath(path)
             if os.access(path, os.F_OK):
                 os.remove(path)
 
         self._gecko_log = path
         return self._gecko_log
@@ -101,37 +158,37 @@ class GeckoInstance(object):
     def _update_profile(self):
         profile_args = {"preferences": deepcopy(self.required_prefs)}
         profile_args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
         if self.prefs:
             profile_args["preferences"].update(self.prefs)
         if self.verbose:
             level = "TRACE" if self.verbose >= 2 else "DEBUG"
             profile_args["preferences"]["marionette.logging"] = level
-        if '-jsdebugger' in self.app_args:
+        if "-jsdebugger" in self.app_args:
             profile_args["preferences"].update({
                 "devtools.browsertoolbox.panel": "jsdebugger",
                 "devtools.debugger.remote-enabled": True,
                 "devtools.chrome.enabled": True,
                 "devtools.debugger.prompt-connection": False,
                 "marionette.debugging.clicktostart": True,
             })
         if self.addons:
-            profile_args['addons'] = self.addons
+            profile_args["addons"] = self.addons
 
         if hasattr(self, "profile_path") and self.profile is None:
             if not self.profile_path:
                 if self.workspace:
-                    profile_args['profile'] = tempfile.mkdtemp(
-                        suffix='.mozrunner-{:.0f}'.format(time.time()),
+                    profile_args["profile"] = tempfile.mkdtemp(
+                        suffix=".mozrunner-{:.0f}".format(time.time()),
                         dir=self.workspace)
                 self.profile = Profile(**profile_args)
             else:
                 profile_args["path_from"] = self.profile_path
-                profile_name = '{}-{:.0f}'.format(
+                profile_name = "{}-{:.0f}".format(
                     os.path.basename(self.profile_path),
                     time.time()
                 )
                 if self.workspace:
                     profile_args["path_to"] = os.path.join(self.workspace,
                                                            profile_name)
                 self.profile = Profile.clone(**profile_args)
 
@@ -151,40 +208,40 @@ class GeckoInstance(object):
 
     def start(self):
         self._update_profile()
         self.runner = self.runner_class(**self._get_runner_args())
         self.runner.start()
 
     def _get_runner_args(self):
         process_args = {
-            'processOutputLine': [NullOutput()],
+            "processOutputLine": [NullOutput()],
         }
 
-        if self.gecko_log == '-':
-            process_args['stream'] = sys.stdout
+        if self.gecko_log == "-":
+            process_args["stream"] = sys.stdout
         else:
-            process_args['logfile'] = self.gecko_log
+            process_args["logfile"] = self.gecko_log
 
         env = os.environ.copy()
 
         # environment variables needed for crashreporting
         # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
-        env.update({'MOZ_CRASHREPORTER': '1',
-                    'MOZ_CRASHREPORTER_NO_REPORT': '1',
-                    'MOZ_CRASHREPORTER_SHUTDOWN': '1',
+        env.update({"MOZ_CRASHREPORTER": "1",
+                    "MOZ_CRASHREPORTER_NO_REPORT": "1",
+                    "MOZ_CRASHREPORTER_SHUTDOWN": "1",
                     })
 
         return {
-            'binary': self.binary,
-            'profile': self.profile,
-            'cmdargs': ['-no-remote', '-marionette'] + self.app_args,
-            'env': env,
-            'symbols_path': self.symbols_path,
-            'process_args': process_args
+            "binary": self.binary,
+            "profile": self.profile,
+            "cmdargs": ["-no-remote", "-marionette"] + self.app_args,
+            "env": env,
+            "symbols_path": self.symbols_path,
+            "process_args": process_args
         }
 
     def close(self, restart=False):
         if not restart:
             self.profile = None
 
         if self.runner:
             self.runner.stop()
@@ -200,20 +257,47 @@ class GeckoInstance(object):
         if prefs:
             self.prefs = prefs
         else:
             self.prefs = None
         self.start()
 
 
 class FennecInstance(GeckoInstance):
+    fennec_prefs = {
+        # Enable output of dump()
+        "browser.dom.window.dump.enabled": True,
+
+        # Disable Android snippets
+        "browser.snippets.enabled": False,
+        "browser.snippets.syncPromo.enabled": False,
+        "browser.snippets.firstrunHomepage.enabled": False,
+
+        # Disable safebrowsing components
+        "browser.safebrowsing.downloads.enabled": False,
+
+        # Do not restore the last open set of tabs if the browser has crashed
+        "browser.sessionstore.resume_from_crash": False,
+
+        # Disable e10s by default
+        "browser.tabs.remote.autostart.1": False,
+        "browser.tabs.remote.autostart.2": False,
+        "browser.tabs.remote.autostart": False,
+
+        # Do not allow background tabs to be zombified, otherwise for tests that
+        # open additional tabs, the test harness tab itself might get unloaded
+        "browser.tabs.disableBackgroundZombification": True,
+    }
+
     def __init__(self, emulator_binary=None, avd_home=None, avd=None,
                  adb_path=None, serial=None, connect_to_running_emulator=False,
                  package_name=None, *args, **kwargs):
         super(FennecInstance, self).__init__(*args, **kwargs)
+        self.required_prefs.update(FennecInstance.fennec_prefs)
+
         self.runner_class = FennecEmulatorRunner
         # runner args
         self._package_name = package_name
         self.emulator_binary = emulator_binary
         self.avd_home = avd_home
         self.adb_path = adb_path
         self.avd = avd
         self.serial = serial
@@ -222,110 +306,150 @@ class FennecInstance(GeckoInstance):
     @property
     def package_name(self):
         """
         Name of app to run on emulator.
 
         Note that FennecInstance does not use self.binary
         """
         if self._package_name is None:
-            self._package_name = 'org.mozilla.fennec'
-            user = os.getenv('USER')
+            self._package_name = "org.mozilla.fennec"
+            user = os.getenv("USER")
             if user:
-                self._package_name += '_' + user
+                self._package_name += "_" + user
         return self._package_name
 
     def start(self):
         self._update_profile()
         self.runner = self.runner_class(**self._get_runner_args())
         try:
             if self.connect_to_running_emulator:
                 self.runner.device.connect()
             self.runner.start()
         except Exception as e:
             exc, val, tb = sys.exc_info()
-            message = 'Error possibly due to runner or device args: {}'
+            message = "Error possibly due to runner or device args: {}"
             raise exc, message.format(e.message), tb
         # gecko_log comes from logcat when running with device/emulator
         logcat_args = {
-            'filterspec': 'Gecko',
-            'serial': self.runner.device.dm._deviceSerial
+            "filterspec": "Gecko",
+            "serial": self.runner.device.dm._deviceSerial
         }
-        if self.gecko_log == '-':
-            logcat_args['stream'] = sys.stdout
+        if self.gecko_log == "-":
+            logcat_args["stream"] = sys.stdout
         else:
-            logcat_args['logfile'] = self.gecko_log
+            logcat_args["logfile"] = self.gecko_log
         self.runner.device.start_logcat(**logcat_args)
         self.runner.device.setup_port_forwarding(
             local_port=self.marionette_port,
             remote_port=self.marionette_port,
         )
 
     def _get_runner_args(self):
         process_args = {
-            'processOutputLine': [NullOutput()],
+            "processOutputLine": [NullOutput()],
         }
 
         runner_args = {
-            'app': self.package_name,
-            'avd_home': self.avd_home,
-            'adb_path': self.adb_path,
-            'binary': self.emulator_binary,
-            'profile': self.profile,
-            'cmdargs': ['-marionette'] + self.app_args,
-            'symbols_path': self.symbols_path,
-            'process_args': process_args,
-            'logdir': self.workspace or os.getcwd(),
-            'serial': self.serial,
+            "app": self.package_name,
+            "avd_home": self.avd_home,
+            "adb_path": self.adb_path,
+            "binary": self.emulator_binary,
+            "profile": self.profile,
+            "cmdargs": ["-marionette"] + self.app_args,
+            "symbols_path": self.symbols_path,
+            "process_args": process_args,
+            "logdir": self.workspace or os.getcwd(),
+            "serial": self.serial,
         }
         if self.avd:
-            runner_args['avd'] = self.avd
+            runner_args["avd"] = self.avd
 
         return runner_args
 
     def close(self, restart=False):
         super(FennecInstance, self).close(restart)
         if self.runner and self.runner.device.connected:
             self.runner.device.dm.remove_forward(
-                'tcp:{}'.format(int(self.marionette_port))
+                "tcp:{}".format(int(self.marionette_port))
             )
 
 
 class DesktopInstance(GeckoInstance):
     desktop_prefs = {
-        'app.update.auto': False,
-        'app.update.enabled': False,
-        'browser.dom.window.dump.enabled': True,
-        'browser.firstrun-content.dismissed': True,
-        # Bug 1145668 - Has to be reverted to about:blank once Marionette
-        # can correctly handle error pages
-        'browser.newtab.url': 'about:newtab',
-        'browser.newtabpage.enabled': False,
-        'browser.reader.detectedFirstArticle': True,
-        'browser.safebrowsing.blockedURIs.enabled': False,
-        'browser.safebrowsing.forbiddenURIs.enabled': False,
-        'browser.safebrowsing.malware.enabled': False,
-        'browser.safebrowsing.phishing.enabled': False,
-        'browser.search.update': False,
-        'browser.tabs.animate': False,
-        'browser.tabs.warnOnClose': False,
-        'browser.tabs.warnOnOpen': False,
-        'browser.uitour.enabled': False,
-        'extensions.getAddons.cache.enabled': False,
-        'extensions.installDistroAddons': False,
-        'extensions.showMismatchUI': False,
-        'extensions.update.enabled': False,
-        'extensions.update.notifyUser': False,
-        'geo.provider.testing': True,
-        'javascript.options.showInConsole': True,
-        'privacy.trackingprotection.enabled': False,
-        'privacy.trackingprotection.pbmode.enabled': False,
-        'security.notification_enable_delay': 0,
-        'signon.rememberSignons': False,
-        'toolkit.startup.max_resumed_crashes': -1,
+        # Disable application updates
+        "app.update.enabled": False,
+
+        # Enable output of dump()
+        "browser.dom.window.dump.enabled": True,
+
+        # Indicate that the download panel has been shown once so that whichever
+        # download test runs first doesn"t show the popup inconsistently
+        "browser.download.panel.shown": True,
+
+        # Do not show the EULA notification which can interfer with tests
+        "browser.EULA.override": True,
+
+        # Bug 1145668, 1312674
+        # Turn off once Marionette can correctly handle error pages, and doesn't
+        # hang when about:blank gets loaded twice
+        "browser.newtabpage.enabled": True,
+        # Assume the about:newtab page"s intro panels have been shown to not depend on
+        # which test runs first and happens to open about:newtab
+        "browser.newtabpage.introShown": True,
+
+        # Background thumbnails in particular cause grief, and disabling thumbnails
+        # in general can"t hurt - we re-enable them when tests need them
+        "browser.pagethumbnails.capturing_disabled": True,
+
+        # Avoid performing Reader Mode intros during tests
+        "browser.reader.detectedFirstArticle": True,
+
+        # Disable safebrowsing components
+        "browser.safebrowsing.blockedURIs.enabled": False,
+        "browser.safebrowsing.downloads.enabled": False,
+        "browser.safebrowsing.forbiddenURIs.enabled": False,
+        "browser.safebrowsing.malware.enabled": False,
+        "browser.safebrowsing.phishing.enabled": False,
+
+        # Disable updates to search engines
+        "browser.search.update": False,
+
+        # Do not restore the last open set of tabs if the browser has crashed
+        "browser.sessionstore.resume_from_crash": False,
+
+        # Don't check for the default web browser during startup
+        "browser.shell.checkDefaultBrowser": False,
+
+        # Disable e10s by default
+        "browser.tabs.remote.autostart.1": False,
+        "browser.tabs.remote.autostart.2": False,
+        "browser.tabs.remote.autostart": False,
+
+        # Needed for branded builds to prevent opening a second tab on startup
+        "browser.startup.homepage_override.mstone": "ignore",
+        # Start with a blank page by default
+        "browser.startup.page": 0,
+
+        # Disable tab animation
+        "browser.tabs.animate": False,
+
+        # Do not warn on exit when multiple tabs are open
+        "browser.tabs.warnOnClose": False,
+        # Do not warn when closing all other open tabs
+        "browser.tabs.warnOnCloseOtherTabs": False,
+        # Do not warn when multiple tabs will be opened
+        "browser.tabs.warnOnOpen": False,
+
+        # Disable the UI tour
+        "browser.uitour.enabled": False,
+
+        # Disable first-run welcome page
+        "startup.homepage_welcome_url": "about:blank",
+        "startup.homepage_welcome_url.additional": "",
     }
 
     def __init__(self, *args, **kwargs):
         super(DesktopInstance, self).__init__(*args, **kwargs)
         self.required_prefs.update(DesktopInstance.desktop_prefs)
 
 
 class NullOutput(object):
--- a/testing/marionette/evaluate.js
+++ b/testing/marionette/evaluate.js
@@ -94,17 +94,17 @@ this.evaluate = {};
  *     it can be sent to the client.
  *
  * @throws JavaScriptError
  *   If an Error was thrown whilst evaluating the script.
  * @throws ScriptTimeoutError
  *   If the script was interrupted due to script timeout.
  */
 evaluate.sandbox = function(sb, script, args = [], opts = {}) {
-  let timeoutId, timeoutHandler, unloadHandler;
+  let scriptTimeoutID, timeoutHandler, unloadHandler;
 
   let promise = new Promise((resolve, reject) => {
     let src = "";
     sb[COMPLETE] = resolve;
     timeoutHandler = () => reject(new ScriptTimeoutError("Timed out"));
     unloadHandler = () => reject(
         new JavaScriptError("Document was unloaded during execution"));
 
@@ -139,17 +139,17 @@ evaluate.sandbox = function(sb, script, 
     if (opts.debug) {
       sb.window.onerror = (msg, url, line) => {
         let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`);
         reject(err);
       };
     }
 
     // timeout and unload handlers
-    timeoutId = setTimeout(
+    scriptTimeoutID = setTimeout(
         timeoutHandler, opts.timeout || DEFAULT_TIMEOUT);
     sb.window.addEventListener("unload", unloadHandler);
 
     let res;
     try {
       res = Cu.evalInSandbox(
           src, sb, "1.8", opts.filename || "dummy file", 0);
     } catch (e) {
@@ -163,17 +163,17 @@ evaluate.sandbox = function(sb, script, 
     }
 
     if (!opts.async) {
       resolve(res);
     }
   });
 
   return promise.then(res => {
-    sb.window.clearTimeout(timeoutId);
+    clearTimeout(scriptTimeoutID);
     sb.window.removeEventListener("unload", unloadHandler);
     return res;
   });
 };
 
 this.sandbox = {};
 
 /**
--- a/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_execute_script.py
@@ -1,13 +1,14 @@
 # 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/.
 
 import os
+import time
 import urllib
 
 from marionette import MarionetteTestCase, WindowManagerMixin
 from marionette_driver import By, errors
 from marionette_driver.marionette import HTMLElement
 
 
 def inline(doc):
@@ -232,16 +233,39 @@ class TestExecuteContent(MarionetteTestC
         # self.assertTrue(send("return typeof Components == 'undefined'"))
         self.assertTrue(
             send("return typeof window.wrappedJSObject == 'undefined'"))
 
     def test_no_callback(self):
         self.assertTrue(self.marionette.execute_script(
             "return typeof arguments[0] == 'undefined'"))
 
+    def test_window_set_timeout_is_not_cancelled(self):
+        self.marionette.navigate(inline("""
+            <script>
+            window.contentTimeoutTriggered = 0;
+            window.contentTimeoutID = setTimeout(
+                () => window.contentTimeoutTriggered++, 1000);
+            </script>"""))
+
+        # first execute script call should not cancel event
+        self.assertEqual(0, self.marionette.execute_script(
+            "return window.contentTimeoutTriggered", sandbox=None))
+
+        # test that event was not cancelled
+        time.sleep(1)
+        self.assertEqual(1, self.marionette.execute_script(
+            "return window.contentTimeoutTriggered", sandbox=None))
+
+        # ../../../../evaluate.js:/scriptTimeoutID/
+        # sets the script timeout handler using the content frame script
+        # so the in-content setTimeout should always return 2
+        self.assertEqual(2, self.marionette.execute_script(
+            "return window.contentTimeoutID", sandbox=None))
+
 
 class TestExecuteChrome(WindowManagerMixin, TestExecuteContent):
 
     def setUp(self):
         super(TestExecuteChrome, self).setUp()
 
         self.marionette.set_context("chrome")
 
@@ -291,16 +315,19 @@ class TestExecuteChrome(WindowManagerMix
         pass
 
     def test_return_web_element_array(self):
         pass
 
     def test_return_web_element_nodelist(self):
         pass
 
+    def test_window_set_timeout_is_not_cancelled(self):
+        pass
+
 
 class TestElementCollections(MarionetteTestCase):
     def assertSequenceIsInstance(self, seq, typ):
         for item in seq:
             self.assertIsInstance(item, typ)
 
     def test_array(self):
         self.marionette.navigate(inline("<p>foo <p>bar"))
--- a/testing/puppeteer/firefox/firefox_puppeteer/mixins.py
+++ b/testing/puppeteer/firefox/firefox_puppeteer/mixins.py
@@ -78,19 +78,21 @@ class PuppeteerMixin(object):
 
         self._start_handle_count = len(self.marionette.window_handles)
         self._init_tab_handles = self.marionette.window_handles
         self.marionette.set_context('chrome')
 
         self.puppeteer = Puppeteer(self.marionette)
         self.browser = self.puppeteer.windows.current
         self.browser.focus()
+
         with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
-            # Ensure that we have a default page opened
-            self.marionette.navigate(self.puppeteer.prefs.get_pref('browser.newtab.url'))
+            # Bug 1312674 - Navigating to about:blank twice can cause a hang in
+            # Marionette. So try to always have a known default page loaded.
+            self.marionette.navigate('about:')
 
     def tearDown(self, *args, **kwargs):
         self.marionette.set_context('chrome')
 
         try:
             # This code should be run after all other tearDown code
             # so that in case of a failure, further tests will not run
             # in a state that is more inconsistent than necessary.
--- a/testing/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
+++ b/testing/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py
@@ -241,17 +241,17 @@ class BrowserWindow(BaseWindow):
         """
         def callback(win):
             # Prepare action which triggers the opening of the browser window
             if callable(trigger):
                 trigger(win)
             elif trigger == 'menu':
                 self.menubar.select_by_id('tools-menu', 'menu_pageInfo')
             elif trigger == 'shortcut':
-                if win.marionette.session_capabilities['platformName'] == 'WINDOWS_NT':
+                if win.marionette.session_capabilities['platformName'] == 'windows_nt':
                     raise ValueError('Page info shortcut not available on Windows.')
                 win.send_shortcut(win.get_entity('pageInfoCmd.commandkey'),
                                   accel=True)
             elif trigger == 'context_menu':
                 # TODO: Add once we can do right clicks
                 pass
             else:
                 raise ValueError('Unknown opening method: "%s"' % trigger)
deleted file mode 100644
--- a/testing/web-platform/meta/webvtt/webvtt-api-for-browsers/vttcue-interface/line.html.ini
+++ /dev/null
@@ -1,8 +0,0 @@
-[line.html]
-  type: testharness
-  [VTTCue.line, script-created cue]
-    expected: FAIL
-
-  [VTTCue.line, parsed cue]
-    expected: FAIL
-
--- a/testing/web-platform/tests/webvtt/webvtt-api-for-browsers/vttcue-interface/line.html
+++ b/testing/web-platform/tests/webvtt/webvtt-api-for-browsers/vttcue-interface/line.html
@@ -3,53 +3,53 @@
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <div id=log></div>
 <script>
 test(function(){
     var video = document.createElement('video');
     document.body.appendChild(video);
     var c1 = new VTTCue(0, 1, 'text1');
-    assert_equals(c1.line, -1);
+    assert_equals(c1.line, "auto");
     var track = document.createElement('track');
     var t = track.track;
     t.addCue(c1);
-    assert_equals(c1.line, -1);
+    assert_equals(c1.line, "auto");
     video.appendChild(track);
-    assert_equals(c1.line, -1);
+    assert_equals(c1.line, "auto");
     t.mode = 'showing';
-    assert_equals(c1.line, -1);
+    assert_equals(c1.line, "auto");
     var c2 = new VTTCue(0, 1, 'text2');
     var track2 = document.createElement('track');
     var t2 = track2.track;
     t2.addCue(c2);
-    assert_equals(c2.line, -1);
+    assert_equals(c2.line, "auto");
     video.appendChild(track2);
     t2.mode = 'showing';
-    assert_equals(c2.line, -2);
-    assert_equals(c1.line, -1);
+    assert_equals(c2.line, "auto");
+    assert_equals(c1.line, "auto");
     c1.line = -5;
     assert_equals(c1.line, -5);
-    assert_equals(c2.line, -2);
+    assert_equals(c2.line, "auto");
     c1.line = 0;
     c1.snapToLines = false;
     assert_equals(c1.line, 0);
-    assert_equals(c2.line, -2);
+    assert_equals(c2.line, "auto");
 }, document.title+', script-created cue');
 
 var t_parsed = async_test(document.title+', parsed cue');
 t_parsed.step(function(){
     var video = document.createElement('video');
     document.body.appendChild(video);
     var t = document.createElement('track');
     t.onload = this.step_func(function(){
         var c1 = t.track.cues[0];
         var c2 = t.track.cues[1];
         var c3 = t.track.cues[2];
-        assert_equals(c1.line, -1);
+        assert_equals(c1.line, "auto");
         assert_equals(c2.line, 0);
         assert_equals(c3.line, 0);
 
         this.done();
     });
     t.onerror = this.step_func(function() {
       assert_unreached('got error event');
     });
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -526,16 +526,18 @@
                          position="after_end"
                          xbl:inherits="oncommand=menucommand">
             <children/>
           </xul:menupopup>
         </xul:button>
         <xul:button anonid="button"
                     class="popup-notification-button"
                     default="true"
+                    label="&defaultButton.label;"
+                    accesskey="&defaultButton.accesskey;"
                     xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey,disabled=mainactiondisabled"/>
       </xul:hbox>
     </content>
     <resources>
       <stylesheet src="chrome://global/skin/notification.css"/>
     </resources>
     <implementation>
       <field name="checkbox" readonly="true">
--- a/toolkit/locales/en-US/chrome/global/notification.dtd
+++ b/toolkit/locales/en-US/chrome/global/notification.dtd
@@ -2,8 +2,11 @@
    - 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/. -->
 
 <!ENTITY closeNotification.tooltip "Close this message">
 
 <!ENTITY checkForUpdates "Check for updates…">
 
 <!ENTITY learnMore "Learn more…">
+
+<!ENTITY defaultButton.label "OK!">
+<!ENTITY defaultButton.accesskey "O">
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -286,18 +286,18 @@ PopupNotifications.prototype = {
    *        action. If present, it must have the following properties:
    *          - label (string): the button's label.
    *          - accessKey (string): the button's accessKey.
    *          - callback (function): a callback to be invoked when the button is
    *            pressed, is passed an object that contains the following fields:
    *              - checkboxChecked: (boolean) If the optional checkbox is checked.
    *          - [optional] dismiss (boolean): If this is true, the notification
    *            will be dismissed instead of removed after running the callback.
-   *        If null, the notification will not have a button, and
-   *        secondaryActions will be ignored.
+   *        If null, the notification will have a default "OK" action button
+   *        that can be used to dismiss the popup and secondaryActions will be ignored.
    * @param secondaryActions
    *        An optional JavaScript array describing the notification's alternate
    *        actions. The array should contain objects with the same properties
    *        as mainAction. These are used to populate the notification button's
    *        dropdown menu.
    * @param options
    *        An options JavaScript object holding additional properties for the
    *        notification. The following properties are currently supported:
@@ -677,19 +677,20 @@ PopupNotifications.prototype = {
       if (n.mainAction) {
         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
         popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonEvent(event, 'buttoncommand');");
         popupnotification.setAttribute("dropmarkerpopupshown", "PopupNotifications._onButtonEvent(event, 'dropmarkerpopupshown');");
         popupnotification.setAttribute("learnmoreclick", "PopupNotifications._onButtonEvent(event, 'learnmoreclick');");
         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
       } else {
+        // Enable the default button to let the user close the popup if the close button is hidden
+        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonEvent(event, 'buttoncommand');");
         popupnotification.removeAttribute("buttonlabel");
         popupnotification.removeAttribute("buttonaccesskey");
-        popupnotification.removeAttribute("buttoncommand");
         popupnotification.removeAttribute("dropmarkerpopupshown");
         popupnotification.removeAttribute("learnmoreclick");
         popupnotification.removeAttribute("menucommand");
       }
 
       if (n.options.popupIconClass) {
         let classes = "popup-notification-icon " + n.options.popupIconClass;
         popupnotification.setAttribute("iconclass", classes);
@@ -718,17 +719,17 @@ PopupNotifications.prototype = {
       } else
         popupnotification.removeAttribute("origin");
 
       if (n.options.hideClose)
         popupnotification.setAttribute("closebuttonhidden", "true");
 
       popupnotification.notification = n;
 
-      if (n.secondaryActions && n.secondaryActions.length > 0) {
+      if (n.mainAction && n.secondaryActions && n.secondaryActions.length > 0) {
         let telemetryStatId = TELEMETRY_STAT_ACTION_2;
 
         let secondaryAction = n.secondaryActions[0];
         popupnotification.setAttribute("secondarybuttonlabel", secondaryAction.label);
         popupnotification.setAttribute("secondarybuttonaccesskey", secondaryAction.accessKey);
         popupnotification.setAttribute("secondarybuttoncommand", "PopupNotifications._onButtonEvent(event, 'secondarybuttoncommand');");
 
         for (let i = 1; i < n.secondaryActions.length; i++) {
@@ -1313,27 +1314,29 @@ PopupNotifications.prototype = {
 
     if (type == "secondarybuttoncommand") {
       action = notification.secondaryActions[0];
       telemetryStatId = TELEMETRY_STAT_ACTION_2;
     }
 
     notification._recordTelemetryStat(telemetryStatId);
 
-    try {
-      action.callback.call(undefined, {
-        checkboxChecked: notificationEl.checkbox.checked
-      });
-    } catch (error) {
-      Cu.reportError(error);
-    }
+    if (action) {
+      try {
+        action.callback.call(undefined, {
+          checkboxChecked: notificationEl.checkbox.checked
+        });
+      } catch (error) {
+        Cu.reportError(error);
+      }
 
-    if (action.dismiss) {
-      this._dismiss();
-      return;
+      if (action.dismiss) {
+        this._dismiss();
+        return;
+      }
     }
 
     this._remove(notification);
     this._update();
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
--- a/toolkit/themes/shared/popupnotification.inc.css
+++ b/toolkit/themes/shared/popupnotification.inc.css
@@ -39,17 +39,16 @@
 .popup-notification-learnmore-link {
   margin-top: .5em !important;
 }
 
 .popup-notification-button-container {
   background-color: var(--arrowpanel-dimmed);
   border-top: 1px solid var(--panel-separator-color);
   display: flex;
-  justify-content: flex-end;
 }
 
 .popup-notification-button-container > toolbarseparator {
   -moz-appearance: none;
   border: 0;
   border-left: 1px solid var(--panel-separator-color);
   margin: 7px 0 7px;
   min-width: 0;
@@ -99,16 +98,20 @@
 .popup-notification-button[default]:hover:not([disabled]) {
   background-color: #0675d3;
 }
 
 .popup-notification-button[default]:hover:active:not([disabled]) {
   background-color: #0568ba;
 }
 
+.popup-notification-button[anonid="secondarybutton"][hidden="true"] ~ .popup-notification-button[default] {
+  flex: 1;
+}
+
 .popup-notification-button > .button-box {
   padding: 0;
   margin: 0;
   /* prevent double border on windows when focused */
   border: none;
 }
 
 .popup-notification-dropmarker {