Bug 1489308 part 5. Align the work we do on document.open with the spec. r=mccr8,smaug
authorBoris Zbarsky <bzbarsky@mit.edu>
Wed, 27 Feb 2019 23:24:48 +0000
changeset 519456 6504b5468b32786ec2f492e0dc6cd2ed3f1cf55d
parent 519455 015db4a424d9f817a7dbaee4e520e81f20430727
child 519457 c1113b00d864eaf617be6fbc3966b73c2af3e1c4
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmccr8, smaug
bugs1489308, 172261, 255820, 346659, 1232829, 715739, 329869
milestone67.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 1489308 part 5. Align the work we do on document.open with the spec. r=mccr8,smaug The main behavior changes are: 1) We no longer create a new Window when doing document.open(). We use the same Window but remove all the event listeners on it and on the existing DOM tree before removing the document's existing kids. 2) We no longer create a new session history entry. The existing one always gets replaced instead. 3) We now support document.open on documents that are not in a Window. The reasons for the various test changes are as follows: The change to browser_modifiedclick_inherit_principal.js is because we no longer set the docshell to a wyciwyg URL when document.open() happens and the test was depending on that to terminate. browser_wyciwyg_urlbarCopying.js is being removed because it's trying to test wyciwyg URIs, which no longer exist. The changes in docshell/test/navigation are because document.open() no longer affects session history. One of the tests was testing the interactions there and is being removed; another is being repurposed to just test that document.open() does not affect history.length. The change to test_x-frame-options.html is because document.open() now removes event listeners on the window, which it didn't use to do (and in the specific case in this test reused the existing inner too, so the listener was still around in practice). The new behavior matches other browsers. The removal of test_bug172261.html is because document.open() no longer affects session history, so you can't go back across it or forward to the "opened" state, so the situation that test is trying to test no longer exists. The changes to test_bug255820.html are because reloading a document after document.open() will now just load the URL of the document that was the entry document for the open() call, not reload the written content. So there's not much point testing reload behavior, and in this test it was just reloading the toplevel test file inside the frames. The change to test_bug346659.html is because now we no longer create a new Window on document.open(). The change to test_bug1232829.html is because document.open() (implicit in this test) no longer adds history entries, so the back() was just leaving the test page instead of going back across the document.open(). The test is a crashtest in practice, so might still be testing something useful about how document.open() interacts with animations. The change to test_bug715739.html is because the URL of the document after document.open() is now the URL of the entry document, not a wyciwyg URL, so reload() has different behavior than it used to. The change to test_bug329869.html is because now when we go back we're reloading the original document we had, not doing a wyciwyg load, and the security info now doesn't include the untrusted script. The changes to the wpt expectations are removing a bunch of expected failures now that we pass those tests and disabling some tests that are fundamentally racy and hence fail randomly. The latter all have github issues filed for the test problem. The change to testing/web-platform/tests/common/object-association.js is fixing tests that were not matching the spec (and were failing in other browsers). The change to parser-uses-registry-of-owner-document.html is fixing tests that were not matching the spec (and were failing in other browsers). The change to document-write.tentative.html is because the test was buggy: it was using the same iframe element for all its tests and racing loads from some tests against API calls from other tests, etc. It's a wonder it ever managed to pass, independent of these patches (and in fact it doesn't pass according to wpt.fyi data, even in Firefox). The changes in html/browsers/history/the-history-interface are because document.open() no longer adds history entries. The test was failing in all other browsers for the same reason. The changes in html/browsers/history/the-location-interface are because reloading a document.open()-created thing now loads the URL of the page that was the entry document for the open() call. The test was failing in all other browsers. The change to reload_document_open_write.html is because we now reload the url of the document that entered the script that called open() when we reload, not the written content. Other browsers were failing this test too; Gecko with the old document.open implementation was the only one that passed. The change to http-refresh.py is to fix a test bug: it was not returning a Content-Type header, so we were putting up helper app dialogs, etc. The change to test_ext_contentscript.js is because we no create a new global for document.open() calls. Kris Maglione OKed this part. Differential Revision: https://phabricator.services.mozilla.com/D17323
browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
browser/components/urlbar/tests/browser/browser.ini
browser/components/urlbar/tests/browser/browser_wyciwyg_urlbarCopying.js
browser/components/urlbar/tests/browser/test_wyciwyg_copying.html
browser/components/urlbar/tests/legacy/browser.ini
docshell/test/navigation/file_bug1379762-2.html
docshell/test/navigation/file_document_write_1.html
docshell/test/navigation/mochitest.ini
docshell/test/navigation/test_sessionhistory.html
dom/base/Document.cpp
dom/base/Document.h
dom/base/UseCounters.conf
dom/base/test/test_x-frame-options.html
dom/html/nsHTMLDocument.cpp
dom/html/nsHTMLDocument.h
dom/html/test/mochitest.ini
dom/html/test/test_bug172261.html
dom/html/test/test_bug255820.html
dom/tests/mochitest/bugs/test_bug346659.html
js/xpconnect/crashtests/crashtests.list
layout/base/nsDocumentViewer.cpp
layout/style/test/test_bug1232829.html
parser/htmlparser/tests/mochitest/test_bug715739.html
security/manager/ssl/tests/mochitest/mixedcontent/test_bug329869.html
testing/web-platform/meta/custom-elements/parser/parser-uses-registry-of-owner-document.html.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/015.html.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/016.html.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-immediate.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-multisecond-header.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-multisecond-meta.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-while-navigating.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort.sub.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/aborted-parser.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/active.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/bailout-exception-vs-return-origin.sub.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/bailout-side-effects-ignore-opens-during-unload.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/beforeunload.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/document.open-03.html.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/history.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/no-new-global.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/quirks.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/reload.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/tasks.window.js.ini
testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/url-fragment.window.js.ini
testing/web-platform/tests/common/object-association.js
testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html
testing/web-platform/tests/custom-elements/reactions/Document.html
testing/web-platform/tests/feature-policy/experimental-features/document-write.tentative.html
testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html
testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html
testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html
testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html
testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/closing-the-input-stream/load-event-after-location-set-during-write.window.js
testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/http-refresh.py
toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
uriloader/base/nsDocLoader.cpp
uriloader/base/nsDocLoader.h
--- a/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
+++ b/browser/base/content/test/general/browser_modifiedclick_inherit_principal.js
@@ -8,17 +8,23 @@ const kURL =
  * Check that when manually opening content JS links in new tabs/windows,
  * we use the correct principal, and we don't clear the URL bar.
  */
 add_task(async function() {
  await BrowserTestUtils.withNewTab(kURL, async function(browser) {
    let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
    await ContentTask.spawn(browser, null, async function() {
      let a = content.document.createElement("a");
-     a.href = "javascript:document.write('spoof'); void(0);";
+     // newTabPromise won't resolve until it has a URL that's not "about:blank".
+     // But doing document.open() from inside that same document does not change
+     // the URL of the docshell.  So we need to do some URL change to cause
+     // newTabPromise to resolve, since the document is at about:blank the whole
+     // time, URL-wise.  Navigating to '#' should do the trick without changing
+     // anything else about the document involved.
+     a.href = "javascript:document.write('spoof'); location.href='#'; void(0);";
      a.textContent = "Some link";
      content.document.body.appendChild(a);
    });
    info("Added element");
    await BrowserTestUtils.synthesizeMouseAtCenter("a", {button: 1}, browser);
    let newTab = await newTabPromise;
    is(newTab.linkedBrowser.contentPrincipal.origin, "http://example.com",
       "Principal should be for example.com");
--- a/browser/components/urlbar/tests/browser/browser.ini
+++ b/browser/components/urlbar/tests/browser/browser.ini
@@ -123,12 +123,8 @@ support-files =
   searchSuggestionEngineSlow.xml
   searchSuggestionEngine.sjs
 [browser_urlbarTokenAlias.js]
 [browser_urlbarUpdateForDomainCompletion.js]
 [browser_urlbarValueOnTabSwitch.js]
 skip-if = true # Bug 1524950 - Backspace handling not fully working
 [browser_userTypedValue.js]
 support-files = file_userTypedValue.html
-[browser_wyciwyg_urlbarCopying.js]
-subsuite = clipboard
-support-files =
-  test_wyciwyg_copying.html
deleted file mode 100644
--- a/browser/components/urlbar/tests/browser/browser_wyciwyg_urlbarCopying.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const TEST_PATH = getRootDirectory(gTestPath)
-  .replace("chrome://mochitests/content", "http://mochi.test:8888");
-const TEST_URL = `${TEST_PATH}test_wyciwyg_copying.html`;
-
-function testURLBarCopy(targetValue) {
-  return new Promise((resolve, reject) => {
-    info("Expecting copy of: " + targetValue);
-    waitForClipboard(targetValue, function() {
-      gURLBar.focus();
-      gURLBar.select();
-
-      goDoCommand("cmd_copy");
-    }, resolve, () => {
-      ok(false, "Clipboard copy failed");
-      reject();
-    });
-  });
-}
-
-add_task(async function() {
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
-
-  await BrowserTestUtils.synthesizeMouseAtCenter("#btn", {}, tab.linkedBrowser);
-  let currentURL = gBrowser.currentURI.spec;
-  ok(/^wyciwyg:\/\//i.test(currentURL), currentURL + " is a wyciwyg URI");
-
-  await testURLBarCopy(TEST_URL);
-
-  while (gBrowser.tabs.length > 1)
-    gBrowser.removeCurrentTab();
-});
deleted file mode 100644
--- a/browser/components/urlbar/tests/browser/test_wyciwyg_copying.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<html>
-<body>
-<script>
-  function go() {
-    let w = window.open();
-    w.document.open();
-    w.document.write("<html><body>test document</body></html>");
-    w.document.close();
-  }
-</script>
-<button id="btn" onclick="go();">test</button>
-</body>
-</html>
--- a/browser/components/urlbar/tests/legacy/browser.ini
+++ b/browser/components/urlbar/tests/legacy/browser.ini
@@ -147,12 +147,8 @@ support-files =
   ../browser/searchSuggestionEngine2.xml
   ../browser/searchSuggestionEngine.sjs
 [../browser/browser_urlbar_speculative_connect_not_with_client_cert.js]
 [../browser/browser_urlbar_remoteness_switch.js]
 run-if = e10s
 [../browser/browser_urlbar_remove_match.js]
 [../browser/browser_userTypedValue.js]
 support-files = ../browser/file_userTypedValue.html
-[../browser/browser_wyciwyg_urlbarCopying.js]
-subsuite = clipboard
-support-files =
-  ../browser/test_wyciwyg_copying.html
deleted file mode 100644
--- a/docshell/test/navigation/file_bug1379762-2.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>Bug 1379762</title>
-  </head>
-  <script type="text/just-data">
-    onunload = null; // enable bfcache
-    ++opener.testCount;
-    onpageshow = function(e) {
-      opener.ok(!e.persisted, "Pageshow should not be coming from bfcache " + opener.testCount);
-    }
-    if (opener.testCount == 1) {
-      onload = function () {
-        setTimeout(function() {
-          document.write(testScript);
-        }, 0);
-      }
-    } else if (opener.testCount == 2) {
-      // Do this async, just in case.
-      setTimeout(function() {
-        history.back();
-      }, 0);
-    } else if (opener.testCount == 3) {
-      // Do this async, just in case.
-      setTimeout(function() {
-        history.forward();
-      }, 0);
-    } else if (opener.testCount == 4) {
-      onload = function() {
-        opener.nextTest();
-        window.close();
-      }
-    }
-  </script>
-  <script>
-    var data = document.querySelector("script[type='text/just-data']").textContent;
-    // Store the string that does all out work in a global variable, so we can
-    // get at it later.
-    // eslint-disable-next-line no-useless-concat
-    var testScript = "<script>" + data + "</" + "script>";
-    document.write(testScript);
-  </script>
-</html>
--- a/docshell/test/navigation/file_document_write_1.html
+++ b/docshell/test/navigation/file_document_write_1.html
@@ -1,30 +1,19 @@
 <html>
   <head>
     <script>
-      function run() {
+      function start() {
+        var length = history.length;
         document.open();
         document.write("<h5 id='dynamic'>document.written content</h5>");
         document.close();
-        window.history.go(-1);
-      }
-
-      function start() {
-        if (++opener.testCount == 1) {
-          setTimeout(run, 0);
-        }
+        opener.is(history.length, length,
+                  "document.open/close should not change history");
+        opener.nextTest();
+        window.close();
       }
-
-      window.addEventListener("pageshow",
-        function() {
-          ++opener.file_document_write_1_loadCount;
-          if (opener.file_document_write_1_loadCount == 2) {
-            opener.setTimeout("isTestDynamic()", 0);
-          }
-          opener.ok(opener.file_document_write_1_loadCount <= 2);
-        });
     </script>
   </head>
   <body onload="start();">
     <h5>static content</h5>
   </body>
 </html>
--- a/docshell/test/navigation/mochitest.ini
+++ b/docshell/test/navigation/mochitest.ini
@@ -71,16 +71,16 @@ skip-if = (toolkit == 'android') || (!de
 [test_grandchild.html]
 [test_not-opener.html]
 [test_opener.html]
 [test_popup-navigates-children.html]
 [test_reserved.html]
 skip-if = (toolkit == 'android') || (debug && e10s) #too slow on Android 4.3 aws only; bug 1030403; bug 1263213 for debug e10s
 [test_sessionhistory.html]
 skip-if = toolkit == 'android' # RANDOM on android
-support-files = file_bug1379762-1.html file_bug1379762-2.html
+support-files = file_bug1379762-1.html
 [test_sibling-matching-parent.html]
 [test_sibling-off-domain.html]
 [test_triggeringprincipal_frame_nav.html]
 [test_triggeringprincipal_window_open.html]
 [test_triggeringprincipal_parent_iframe_window_open.html]
 [test_triggeringprincipal_iframe_iframe_window_open.html]
 [test_contentpolicy_block_window.html]
--- a/docshell/test/navigation/test_sessionhistory.html
+++ b/docshell/test/navigation/test_sessionhistory.html
@@ -29,17 +29,16 @@ var testFiles =
     "file_bug534178.html",           // Session history entry clean-up.
     "file_fragment_handling_during_load.html",
     "file_nested_frames.html",
     "file_shiftReload_and_pushState.html",
     "file_scrollRestoration.html",
     "file_bug1300461.html",
     "file_bug1326251.html",
     "file_bug1379762-1.html",
-    "file_bug1379762-2.html",
   ];
 var testCount = 0; // Used by the test files.
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.requestFlakyTimeout("untriaged");
 
 var testWindow;
 function nextTest_() {
@@ -49,25 +48,16 @@ function nextTest_() {
     info("Running " + nextFile);
     testWindow = window.open(nextFile, "", "width=360,height=480");
     testWindow.onunload = function() { }; // to prevent bfcache
   } else {
     SimpleTest.finish();
   }
 }
 
-// Needed by file_document_write_1.html
-window.file_document_write_1_loadCount = 0;
-function isTestDynamic() {
-  var dyn = testWindow.document.getElementById("dynamic");
-  is(dyn, null, "Should have gone back to the static page!");
-  nextTest();
-  testWindow.close();
-}
-
 function nextTest() {
   setTimeout(nextTest_, 0);
 }
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -1281,16 +1281,18 @@ Document::Document(const char* aContentT
       mReportedUseCounters(false),
       mHasReportedShadowDOMUsage(false),
       mDocTreeHadAudibleMedia(false),
       mDocTreeHadPlayRevoked(false),
 #ifdef DEBUG
       mWillReparent(false),
 #endif
       mHasDelayedRefreshEvent(false),
+      mLoadEventFiring(false),
+      mSkipLoadEventAfterClose(false),
       mPendingFullscreenRequests(0),
       mXMLDeclarationBits(0),
       mOnloadBlockCount(0),
       mAsyncOnloadBlockCount(0),
       mCompatMode(eCompatibility_FullStandards),
       mReadyState(ReadyState::READYSTATE_UNINITIALIZED),
       mAncestorIsLoading(false),
 #ifdef MOZILLA_INTERNAL_API
@@ -2051,32 +2053,20 @@ bool PrincipalAllowsL10n(nsIPrincipal* p
 
   // UI resources also get access.
   rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_IS_UI_RESOURCE,
                            &hasFlags);
   NS_ENSURE_SUCCESS(rv, false);
   return hasFlags;
 }
 
-void Document::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
-                          nsIPrincipal* aPrincipal) {
-  MOZ_ASSERT(aURI, "Null URI passed to ResetToURI");
-
-  MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug,
-          ("DOCUMENT %p ResetToURI %s", this, aURI->GetSpecOrDefault().get()));
-
-  mSecurityInfo = nullptr;
-
-  nsCOMPtr<nsILoadGroup> group = do_QueryReferent(mDocumentLoadGroup);
-  if (!aLoadGroup || group != aLoadGroup) {
-    mDocumentLoadGroup = nullptr;
-  }
-
+void Document::DisconnectNodeTree() {
   // Delete references to sub-documents and kill the subdocument map,
-  // if any. It holds strong references
+  // if any. This is not strictly needed, but makes the node tree
+  // teardown a bit faster.
   delete mSubDocuments;
   mSubDocuments = nullptr;
 
   // Destroy link map now so we don't waste time removing
   // links one by one
   DestroyElementMaps();
 
   bool oldVal = mInUnlinkOrDeletion;
@@ -2099,16 +2089,33 @@ void Document::ResetToURI(nsIURI* aURI, 
       }
       nsNodeUtils::ContentRemoved(this, content, previousSibling);
       content->UnbindFromTree();
     }
     MOZ_ASSERT(!mCachedRootElement,
                "After removing all children, there should be no root elem");
   }
   mInUnlinkOrDeletion = oldVal;
+}
+
+void Document::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
+                          nsIPrincipal* aPrincipal) {
+  MOZ_ASSERT(aURI, "Null URI passed to ResetToURI");
+
+  MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug,
+          ("DOCUMENT %p ResetToURI %s", this, aURI->GetSpecOrDefault().get()));
+
+  mSecurityInfo = nullptr;
+
+  nsCOMPtr<nsILoadGroup> group = do_QueryReferent(mDocumentLoadGroup);
+  if (!aLoadGroup || group != aLoadGroup) {
+    mDocumentLoadGroup = nullptr;
+  }
+
+  DisconnectNodeTree();
 
   // Reset our stylesheets
   ResetStylesheetsToURI(aURI);
 
   // Release the listener manager
   if (mListenerManager) {
     mListenerManager->Disconnect();
     mListenerManager = nullptr;
@@ -8161,31 +8168,32 @@ void Document::NotifyLoading(const bool&
     nsPIDOMWindowInner* inner = GetInnerWindow();
     if (inner) {
       inner->SetActiveLoadingState(is_loading);
     }
     EnumerateSubDocuments(SetLoadingInSubDocument, &is_loading);
   }
 }
 
-void Document::SetReadyStateInternal(ReadyState rs) {
+void Document::SetReadyStateInternal(ReadyState rs,
+                                     bool updateTimingInformation) {
   if (rs == READYSTATE_UNINITIALIZED) {
     // Transition back to uninitialized happens only to keep assertions happy
     // right before readyState transitions to something else. Make this
     // transition undetectable by Web content.
     mReadyState = rs;
     return;
   }
 
-  if (READYSTATE_LOADING == rs) {
+  if (updateTimingInformation && READYSTATE_LOADING == rs) {
     mLoadingTimeStamp = mozilla::TimeStamp::Now();
   }
   NotifyLoading(mAncestorIsLoading, mAncestorIsLoading, mReadyState, rs);
   mReadyState = rs;
-  if (mTiming) {
+  if (updateTimingInformation && mTiming) {
     switch (rs) {
       case READYSTATE_LOADING:
         mTiming->NotifyDOMLoading(Document::GetDocumentURI());
         break;
       case READYSTATE_INTERACTIVE:
         mTiming->NotifyDOMInteractive(Document::GetDocumentURI());
         break;
       case READYSTATE_COMPLETE:
@@ -8203,17 +8211,19 @@ void Document::SetReadyStateInternal(Rea
       Element* root = GetRootElement();
       if (root && root->HasAttr(kNameSpaceID_None, nsGkAtoms::mozpersist)) {
         mXULPersist = new XULPersist(this);
         mXULPersist->Init();
       }
     }
   }
 
-  RecordNavigationTiming(rs);
+  if (updateTimingInformation) {
+    RecordNavigationTiming(rs);
+  }
 
   RefPtr<AsyncEventDispatcher> asyncDispatcher =
       new AsyncEventDispatcher(this, NS_LITERAL_STRING("readystatechange"),
                                CanBubble::eNo, ChromeOnlyDispatch::eNo);
   asyncDispatcher->RunDOMEventWhenSafe();
 }
 
 void Document::GetReadyState(nsAString& aReadyState) const {
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -1554,16 +1554,21 @@ class Document : public nsINode,
    */
   void ClearStaleServoData();
 
   /**
    * Returns the top window root from the outer window.
    */
   already_AddRefed<nsPIWindowRoot> GetWindowRoot();
 
+  /**
+   * Do the tree-disconnection that ResetToURI and document.open need to do.
+   */
+  void DisconnectNodeTree();
+
  private:
   class SelectorCacheKey {
    public:
     explicit SelectorCacheKey(const nsAString& aString) : mKey(aString) {
       MOZ_COUNT_CTOR(SelectorCacheKey);
     }
 
     nsString mKey;
@@ -1996,17 +2001,22 @@ class Document : public nsINode,
   virtual void EndLoad();
 
   enum ReadyState {
     READYSTATE_UNINITIALIZED = 0,
     READYSTATE_LOADING = 1,
     READYSTATE_INTERACTIVE = 3,
     READYSTATE_COMPLETE = 4
   };
-  void SetReadyStateInternal(ReadyState rs);
+  // Set the readystate of the document.  If updateTimingInformation is true,
+  // this will record relevant timestamps in the document's performance timing.
+  // Some consumers (document.open is the only one right now, actually) don't
+  // want to do that, though.
+  void SetReadyStateInternal(ReadyState rs,
+                             bool updateTimingInformation = true);
   ReadyState GetReadyStateEnum() { return mReadyState; }
 
   void SetAncestorLoading(bool aAncestorIsLoading);
   void NotifyLoading(const bool& aCurrentParentIsLoading,
                      bool aNewParentIsLoading, const ReadyState& aCurrentState,
                      ReadyState aNewState);
 
   // notify that a content node changed state.  This must happen under
@@ -2689,16 +2699,39 @@ class Document : public nsINode,
    * suppressed. The document is responsible for resuming the queue after
    * event handling is unsuppressed.
    */
   void AddSuspendedChannelEventQueue(mozilla::net::ChannelEventQueue* aQueue);
 
   void SetHasDelayedRefreshEvent() { mHasDelayedRefreshEvent = true; }
 
   /**
+   * Flag whether we're about to fire the window's load event for this document.
+   */
+  void SetLoadEventFiring(bool aFiring) { mLoadEventFiring = aFiring; }
+
+  /**
+   * Test whether we should be firing a load event for this document after a
+   * document.close().  This is public and on Document, instead of being private
+   * to nsHTMLDocument, because we need to go through the normal docloader logic
+   * for the readystate change to READYSTATE_COMPLETE with the normal timing and
+   * semantics of firing the load event; we just don't want to fire the load
+   * event if this tests true.  So we need the docloader to be able to access
+   * this state.
+   *
+   * This method should only be called at the point when the load event is about
+   * to be fired.  It resets the "skip" flag, so it is not idempotent.
+   */
+  bool SkipLoadEventAfterClose() {
+    bool skip = mSkipLoadEventAfterClose;
+    mSkipLoadEventAfterClose = false;
+    return skip;
+  }
+
+  /**
    * Increment https://html.spec.whatwg.org/#ignore-destructive-writes-counter
    */
   void IncrementIgnoreDestructiveWritesCounter() {
     ++mIgnoreDestructiveWritesCounter;
   }
 
   /**
    * Decrement https://html.spec.whatwg.org/#ignore-destructive-writes-counter
@@ -4217,16 +4250,31 @@ class Document : public nsINode,
 
  protected:
 #endif
 
   // Whether an event triggered by the refresh driver was delayed because this
   // document has suppressed events.
   bool mHasDelayedRefreshEvent : 1;
 
+  // The HTML spec has a "iframe load in progress" flag, but that doesn't seem
+  // to have the right semantics.  See
+  // <https://github.com/whatwg/html/issues/4292>. What we have instead is a
+  // flag that is set while the window's 'load' event is firing if this document
+  // is the window's document.
+  bool mLoadEventFiring : 1;
+
+  // The HTML spec has a "mute iframe load" flag, but that doesn't seem to have
+  // the right semantics.  See <https://github.com/whatwg/html/issues/4292>.
+  // What we have instead is a flag that is set if completion of our document
+  // via document.close() should skip firing the load event.  Note that this
+  // flag is only relevant for HTML documents, but lives here for reasons that
+  // are documented above on SkipLoadEventAfterClose().
+  bool mSkipLoadEventAfterClose : 1;
+
   uint8_t mPendingFullscreenRequests;
 
   uint8_t mXMLDeclarationBits;
 
   // Currently active onload blockers.
   uint32_t mOnloadBlockCount;
 
   // Onload blockers which haven't been activated yet.
--- a/dom/base/UseCounters.conf
+++ b/dom/base/UseCounters.conf
@@ -118,17 +118,16 @@ method console.timeLog
 method console.timeEnd
 method console.exception
 method console.timeStamp
 method console.profile
 method console.profileEnd
 
 // document.open information
 custom DocumentOpen calls document.open in a way that creates a new Window object
-custom DocumentOpenReplace calls document.open in a way that creates a new Window object and replaces the old history entry.
 
 custom FilteredCrossOriginIFrame cross-origin <iframe> within a CSS/SVG filter
 
 // Custom Elements
 method CustomElementRegistry.define
 
 // Shadow DOM
 method Element.attachShadow
--- a/dom/base/test/test_x-frame-options.html
+++ b/dom/base/test/test_x-frame-options.html
@@ -109,33 +109,35 @@ var testFramesLoaded = function() {
   // call tests to check principal comparison, e.g. a document can open a window
   // to a data: or javascript: document which frames an
   // X-Frame-Options: SAMEORIGIN document and the frame should load
   testFrameInJSURI();
 };
 
 // test that a document can be framed under a javascript: URL opened by the
 // same site as the frame
+// We can't set a load event listener before calling document.open/document.write, because those will remove such listeners.  So we need to define a function that the new window will be able to call.
+function frameInJSURILoaded(win) {
+  var test = win.document.getElementById("sameorigin3")
+                .contentDocument.getElementById("test");
+  ok(test != null, "frame under javascript: URL should have loaded.");
+  win.close();
+
+  // run last test
+  if (!isUnique) {
+    testFrameInDataURI();
+  } else {
+    testFrameNotLoadedInDataURI();
+  }
+}
+
 var testFrameInJSURI = function() {
   var html = '<iframe id="sameorigin3" src="http://mochi.test:8888/tests/dom/base/test/file_x-frame-options_page.sjs?testid=sameorigin3&xfo=sameorigin"></iframe>';
   var win = window.open();
-  win.onload = function() {
-    var test = win.document.getElementById("sameorigin3")
-              .contentDocument.getElementById("test");
-    ok(test != null, "frame under javascript: URL should have loaded.");
-    win.close();
-
-    // run last test
-    if (!isUnique) {
-      testFrameInDataURI();
-    } else {
-      testFrameNotLoadedInDataURI();
-    }
-  };
-  win.location.href = "javascript:document.write('"+html+"');document.close();";
+  win.location.href = "javascript:document.open(); onload = opener.frameInJSURILoaded.bind(null, window); document.write('"+html+"');document.close();";
 };
 
 // test that a document can be framed under a data: URL opened by the
 // same site as the frame
 var testFrameInDataURI = function() {
   var html = '<iframe id="sameorigin4" src="http://mochi.test:8888/tests/dom/base/test/file_x-frame-options_page.sjs?testid=sameorigin4&xfo=sameorigin"></iframe>';
   var win = window.open();
   win.onload = function() {
--- a/dom/html/nsHTMLDocument.cpp
+++ b/dom/html/nsHTMLDocument.cpp
@@ -11,16 +11,18 @@
 #include "mozilla/dom/HTMLAllCollection.h"
 #include "mozilla/dom/FeaturePolicyUtils.h"
 #include "nsCOMPtr.h"
 #include "nsGlobalWindow.h"
 #include "nsString.h"
 #include "nsPrintfCString.h"
 #include "nsReadableUtils.h"
 #include "nsUnicharUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIDocumentLoader.h"
 #include "nsIHTMLContentSink.h"
 #include "nsIXMLContentSink.h"
 #include "nsHTMLParts.h"
 #include "nsHTMLStyleSheet.h"
 #include "nsGkAtoms.h"
 #include "nsIPresShell.h"
 #include "nsPresContext.h"
 #include "nsPIDOMWindow.h"
@@ -73,16 +75,17 @@
 #include "nsArrayUtils.h"
 #include "nsIEffectiveTLDService.h"
 
 // AHMED 12-2
 #include "nsBidiUtils.h"
 
 #include "mozilla/dom/FallbackEncoding.h"
 #include "mozilla/Encoding.h"
+#include "mozilla/EventListenerManager.h"
 #include "mozilla/HTMLEditor.h"
 #include "mozilla/IdentifierMapEntry.h"
 #include "mozilla/LoadInfo.h"
 #include "nsIEditingSession.h"
 #include "nsNodeInfoManager.h"
 #include "nsIPlaintextEditor.h"
 #include "nsIEditorStyleSheets.h"
 #include "nsIInlineSpellChecker.h"
@@ -96,21 +99,23 @@
 #include "nsIRequest.h"
 #include "nsHtml5TreeOpExecutor.h"
 #include "nsHtml5Parser.h"
 #include "nsSandboxFlags.h"
 #include "nsIImageDocument.h"
 #include "mozilla/dom/HTMLBodyElement.h"
 #include "mozilla/dom/HTMLDocumentBinding.h"
 #include "mozilla/dom/Selection.h"
+#include "mozilla/dom/ShadowIncludingTreeIterator.h"
 #include "nsCharsetSource.h"
 #include "nsIStringBundle.h"
 #include "nsFocusManager.h"
 #include "nsIFrame.h"
 #include "nsIContent.h"
+#include "nsIStructuredCloneContainer.h"
 #include "nsLayoutStylesheetCache.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/Unused.h"
 #include "nsCommandParams.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
@@ -792,16 +797,34 @@ void nsHTMLDocument::BeginLoad() {
 void nsHTMLDocument::EndLoad() {
   bool turnOnEditing =
       mParser && (HasFlag(NODE_IS_EDITABLE) || mContentEditableCount > 0);
   // Note: Document::EndLoad nulls out mParser.
   Document::EndLoad();
   if (turnOnEditing) {
     EditingStateChanged();
   }
+
+  if (!GetWindow()) {
+    // This is a document that's not in a window.  For example, this could be an
+    // XMLHttpRequest responseXML document, or a document created via DOMParser
+    // or DOMImplementation.  We don't reach this code normally for such
+    // documents (which is not obviously correct), but can reach it via
+    // document.open()/document.close().
+    //
+    // Such documents don't fire load events, but per spec should set their
+    // readyState to "complete" when parsing and all loading of subresources is
+    // done.  Parsing is done now, and documents not in a window don't load
+    // subresources, so just go ahead and mark ourselves as complete.
+    SetReadyStateInternal(Document::READYSTATE_COMPLETE,
+                          /* updateTimingInformation = */ false);
+
+    // Reset mSkipLoadEventAfterClose just in case.
+    mSkipLoadEventAfterClose = false;
+  }
 }
 
 void nsHTMLDocument::SetCompatibilityMode(nsCompatibility aMode) {
   NS_ASSERTION(IsHTMLDocument() || aMode == eCompatibility_FullStandards,
                "Bad compat mode for XHTML document!");
 
   if (mCompatMode == aMode) {
     return;
@@ -1161,396 +1184,252 @@ mozilla::dom::Nullable<mozilla::dom::Win
   // XXXbz We ignore aReplace for now.
   rv = win->OpenJS(aURL, aName, aFeatures, getter_AddRefs(newWindow));
   if (!newWindow) {
     return nullptr;
   }
   return WindowProxyHolder(newWindow->GetBrowsingContext());
 }
 
-already_AddRefed<Document> nsHTMLDocument::Open(
-    JSContext* cx, const Optional<nsAString>& /* unused */,
-    const nsAString& aReplace, ErrorResult& aError) {
-  // Implements the "When called with two arguments (or fewer)" steps here:
-  // https://html.spec.whatwg.org/multipage/webappapis.html#opening-the-input-stream
+Document* nsHTMLDocument::Open(JSContext* cx,
+                               const Optional<nsAString>& /* unused */,
+                               const nsAString& /* unused */,
+                               ErrorResult& aError) {
+  // Implements
+  // <https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-open-steps>
 
   MOZ_ASSERT(nsContentUtils::CanCallerAccess(this),
              "XOW should have caught this!");
+
+  // Step 1 -- throw if we're an XML document.
   if (!IsHTMLDocument() || mDisableDocWrite) {
-    // No calling document.open() on XHTML
     aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
+  // Step 2 -- throw if dynamic markup insertion should throw.
   if (ShouldThrowOnDynamicMarkupInsertion()) {
     aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
-  // If we already have a parser we ignore the document.open call.
-  if (mParser || mParserAborted) {
-    // The WHATWG spec says: "If the document has an active parser that isn't
-    // a script-created parser, and the insertion point associated with that
-    // parser's input stream is not undefined (that is, it does point to
-    // somewhere in the input stream), then the method does nothing. Abort
-    // these steps and return the Document object on which the method was
-    // invoked."
-    // Note that aborting a parser leaves the parser "active" with its
-    // insertion point "not undefined". We track this using mParserAborted,
-    // because aborting a parser nulls out mParser.
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  // Implement Step 6 of:
-  // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-open-steps
-  if (ShouldIgnoreOpens()) {
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  // No calling document.open() without a script global object
-  if (!mScriptGlobalObject) {
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  nsPIDOMWindowOuter* outer = GetWindow();
-  if (!outer || (GetInnerWindow() != outer->GetCurrentInnerWindow())) {
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  // check whether we're in the middle of unload.  If so, ignore this call.
-  nsCOMPtr<nsIDocShell> shell(mDocumentContainer);
-  if (!shell) {
-    // We won't be able to create a parser anyway.
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  bool inUnload;
-  shell->GetIsInUnload(&inUnload);
-  if (inUnload) {
-    nsCOMPtr<Document> ret = this;
-    return ret.forget();
-  }
-
-  // Note: We want to use GetEntryDocument here because this document
-  // should inherit the security information of the document that's opening us,
-  // (since if it's secure, then it's presumably trusted).
+  // Step 3 -- get the entry document, so we can use it for security checks.
   nsCOMPtr<Document> callerDoc = GetEntryDocument();
   if (!callerDoc) {
     // If we're called from C++ or in some other way without an originating
     // document we can't do a document.open w/o changing the principal of the
     // document to something like about:blank (as that's the only sane thing to
     // do when we don't know the origin of this call), and since we can't
     // change the principals of a document for security reasons we'll have to
     // refuse to go ahead with this call.
 
     aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
-  // Grab a reference to the calling documents security info (if any)
-  // and URIs as they may be lost in the call to Reset().
-  nsCOMPtr<nsISupports> securityInfo = callerDoc->GetSecurityInfo();
-  nsCOMPtr<nsIURI> uri = callerDoc->GetDocumentURI();
-  nsCOMPtr<nsIURI> baseURI = callerDoc->GetBaseURI();
-  nsCOMPtr<nsIPrincipal> callerPrincipal = callerDoc->NodePrincipal();
-  nsCOMPtr<nsIChannel> callerChannel = callerDoc->GetChannel();
-
-  // We're called from script. Make sure the script is from the same
-  // origin, not just that the caller can access the document. This is
-  // needed to keep document principals from ever changing, which is
-  // needed because of the way we use our XOW code, and is a sane
-  // thing to do anyways.
-
-  bool equals = false;
-  if (NS_FAILED(callerPrincipal->Equals(NodePrincipal(), &equals)) || !equals) {
-#ifdef DEBUG
-    nsCOMPtr<nsIURI> callerDocURI = callerDoc->GetDocumentURI();
-    nsCOMPtr<nsIURI> thisURI = Document::GetDocumentURI();
-    printf("nsHTMLDocument::Open callerDoc %s this %s\n",
-           callerDocURI ? callerDocURI->GetSpecOrDefault().get() : "",
-           thisURI ? thisURI->GetSpecOrDefault().get() : "");
-#endif
-
+  // Step 4 -- make sure we're same-origin (not just same origin-domain) with
+  // the entry document.
+  if (!callerDoc->NodePrincipal()->Equals(NodePrincipal())) {
     aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
 
+  // Step 5 -- if we have an active parser, just no-op.
+  // If we already have a parser we ignore the document.open call.
+  if (mParser || mParserAborted) {
+    // The WHATWG spec used to say: "If the document has an active parser that
+    // isn't a script-created parser, and the insertion point associated with
+    // that parser's input stream is not undefined (that is, it does point to
+    // somewhere in the input stream), then the method does nothing. Abort these
+    // steps and return the Document object on which the method was invoked."
+    // Note that aborting a parser leaves the parser "active" with its insertion
+    // point "not undefined". We track this using mParserAborted, because
+    // aborting a parser nulls out mParser.
+    //
+    // Actually, the spec says something slightly different now, about having
+    // an "active parser whose script nesting level is greater than 0".  It
+    // does not mention insertion points at all.  Not sure whether it matters
+    // in practice.  It seems like "script nesting level" replaced the
+    // insertion point concept?  Anyway,
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1475000 is probably
+    // relevant here.
+    return this;
+  }
+
+  // Step 6 -- check for open() during unload.  Per spec, this is just a check
+  // of the ignore-opens-during-unload counter, but our unload event code
+  // doesn't affect that counter yet (unlike pagehide and beforeunload, which
+  // do), so we check for unload directly.
+  if (ShouldIgnoreOpens()) {
+    return this;
+  }
+
+  nsCOMPtr<nsIDocShell> shell(mDocumentContainer);
+  if (shell) {
+    bool inUnload;
+    shell->GetIsInUnload(&inUnload);
+    if (inUnload) {
+      return this;
+    }
+  }
+
   // At this point we know this is a valid-enough document.open() call
-  // and not a no-op.  Increment our use counters.
+  // and not a no-op.  Increment our use counter.
   SetDocumentAndPageUseCounter(eUseCounter_custom_DocumentOpen);
-  bool isReplace = aReplace.LowerCaseEqualsLiteral("replace");
-  if (isReplace) {
-    SetDocumentAndPageUseCounter(eUseCounter_custom_DocumentOpenReplace);
-  }
-
-  // Stop current loads targeted at the window this document is in.
-  if (mScriptGlobalObject) {
-    nsCOMPtr<nsIContentViewer> cv;
-    shell->GetContentViewer(getter_AddRefs(cv));
-
-    if (cv) {
-      bool okToUnload;
-      if (NS_SUCCEEDED(cv->PermitUnload(&okToUnload)) && !okToUnload) {
-        // We don't want to unload, so stop here, but don't throw an
-        // exception.
-        nsCOMPtr<Document> ret = this;
-        return ret.forget();
-      }
-
-      // Now double-check that our invariants still hold.
-      if (!mScriptGlobalObject) {
-        nsCOMPtr<Document> ret = this;
-        return ret.forget();
-      }
-
-      nsPIDOMWindowOuter* outer = GetWindow();
-      if (!outer || (GetInnerWindow() != outer->GetCurrentInnerWindow())) {
-        nsCOMPtr<Document> ret = this;
-        return ret.forget();
-      }
-    }
-
+
+  // Step 7 -- stop existing navigation of our browsing context (and all other
+  // loads it's doing) if we're the active document of our browsing context.
+  // Note that we do not want to stop anything if there is no existing
+  // navigation.
+  if (shell && IsCurrentActiveDocument() &&
+      shell->GetIsAttemptingToNavigate()) {
     nsCOMPtr<nsIWebNavigation> webnav(do_QueryInterface(shell));
     webnav->Stop(nsIWebNavigation::STOP_NETWORK);
 
-    // The Stop call may have cancelled the onload blocker request or prevented
-    // it from getting added, so we need to make sure it gets added to the
-    // document again otherwise the document could have a non-zero onload block
-    // count without the onload blocker request being in the loadgroup.
+    // The Stop call may have cancelled the onload blocker request or
+    // prevented it from getting added, so we need to make sure it gets added
+    // to the document again otherwise the document could have a non-zero
+    // onload block count without the onload blocker request being in the
+    // loadgroup.
     EnsureOnloadBlocker();
   }
 
-  // The open occurred after the document finished loading.
-  // So we reset the document and then reinitialize it.
-  nsCOMPtr<nsIDocShell> curDocShell = GetDocShell();
-  nsCOMPtr<nsIDocShellTreeItem> parent;
-  if (curDocShell) {
-    curDocShell->GetSameTypeParent(getter_AddRefs(parent));
-  }
-
-  // We are using the same technique as in nsDocShell to figure
-  // out the content policy type. If there is no same type parent,
-  // we know we are loading a new top level document.
-  nsContentPolicyType policyType;
-  if (!parent) {
-    policyType = nsIContentPolicy::TYPE_DOCUMENT;
-  } else {
-    Element* requestingElement = nullptr;
-    nsPIDOMWindowInner* window = GetInnerWindow();
-    if (window) {
-      nsPIDOMWindowOuter* outer =
-          nsPIDOMWindowOuter::GetFromCurrentInner(window);
-      if (outer) {
-        nsGlobalWindowOuter* win = nsGlobalWindowOuter::Cast(outer);
-        requestingElement = win->AsOuter()->GetFrameElementInternal();
-      }
-    }
-    if (requestingElement) {
-      policyType = requestingElement->IsHTMLElement(nsGkAtoms::iframe)
-                       ? nsIContentPolicy::TYPE_INTERNAL_IFRAME
-                       : nsIContentPolicy::TYPE_INTERNAL_FRAME;
-    } else {
-      // If we have lost our frame element by now, just assume we're
-      // an iframe since that's more common.
-      policyType = nsIContentPolicy::TYPE_INTERNAL_IFRAME;
+  // Step 8 -- clear event listeners out of our DOM tree
+  for (nsINode* node : ShadowIncludingTreeIterator(*this)) {
+    if (EventListenerManager* elm = node->GetExistingListenerManager()) {
+      elm->RemoveAllListeners();
     }
   }
 
-  nsCOMPtr<nsIChannel> channel;
-  nsCOMPtr<nsILoadGroup> group = do_QueryReferent(mDocumentLoadGroup);
-  aError = NS_NewChannel(getter_AddRefs(channel), uri, callerDoc,
-                         nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL, policyType,
-                         nullptr,  // PerformanceStorage
-                         group);
-
-  if (aError.Failed()) {
-    return nullptr;
-  }
-
-  if (callerChannel) {
-    nsLoadFlags callerLoadFlags;
-    aError = callerChannel->GetLoadFlags(&callerLoadFlags);
-    if (aError.Failed()) {
-      return nullptr;
-    }
-
-    nsLoadFlags loadFlags;
-    aError = channel->GetLoadFlags(&loadFlags);
-    if (aError.Failed()) {
-      return nullptr;
-    }
-
-    loadFlags |= callerLoadFlags & nsIRequest::INHIBIT_PERSISTENT_CACHING;
-
-    aError = channel->SetLoadFlags(loadFlags);
-    if (aError.Failed()) {
-      return nullptr;
-    }
-
-    // If the user has allowed mixed content on the rootDoc, then we should
-    // propogate it down to the new document channel.
-    bool rootHasSecureConnection = false;
-    bool allowMixedContent = false;
-    bool isDocShellRoot = false;
-    nsresult rvalue = shell->GetAllowMixedContentAndConnectionData(
-        &rootHasSecureConnection, &allowMixedContent, &isDocShellRoot);
-    if (NS_SUCCEEDED(rvalue) && allowMixedContent && isDocShellRoot) {
-      shell->SetMixedContentChannel(channel);
-    }
-  }
-
-  // Before we reset the doc notify the globalwindow of the change,
-  // but only if we still have a window (i.e. our window object the
-  // current inner window in our outer window).
-
-  // Hold onto ourselves on the offchance that we're down to one ref
-  nsCOMPtr<Document> kungFuDeathGrip = this;
-
-  if (nsPIDOMWindowInner* window = GetInnerWindow()) {
-    // Remember the old scope in case the call to SetNewDocument changes it.
-    nsCOMPtr<nsIScriptGlobalObject> oldScope(do_QueryReferent(mScopeObject));
-
-#ifdef DEBUG
-    bool willReparent = mWillReparent;
-    mWillReparent = true;
-
-    Document* templateContentsOwner = mTemplateContentsOwner.get();
-
-    if (templateContentsOwner) {
-      templateContentsOwner->mWillReparent = true;
-    }
-#endif
-
-    // Set our ready state to uninitialized before setting the new document so
-    // that window creation listeners don't use the document in its intermediate
-    // state prior to reset.
-    SetReadyStateInternal(READYSTATE_UNINITIALIZED);
-
-    // Per spec, we pass false here so that a new Window is created.
-    aError = window->SetNewDocument(this, nullptr,
-                                    /* aForceReuseInnerWindow */ false);
-    if (aError.Failed()) {
-      return nullptr;
-    }
-
-#ifdef DEBUG
-    if (templateContentsOwner) {
-      templateContentsOwner->mWillReparent = willReparent;
-    }
-
-    mWillReparent = willReparent;
-#endif
-
-    // Now make sure we're not flagged as the initial document anymore, now
-    // that we've had stuff done to us.  From now on, if anyone tries to
-    // document.open() us, they get a new inner window.
-    SetIsInitialDocument(false);
-
-    nsCOMPtr<nsIScriptGlobalObject> newScope(do_QueryReferent(mScopeObject));
-    JS::Rooted<JSObject*> wrapper(cx, GetWrapper());
-    if (oldScope && newScope != oldScope && wrapper) {
-      JSAutoRealm ar(cx, wrapper);
-      UpdateReflectorGlobal(cx, wrapper, aError);
-      if (aError.Failed()) {
-        return nullptr;
-      }
-
-      // Also reparent the template contents owner document
-      // because its global is set to the same as this document.
-      if (mTemplateContentsOwner) {
-        JS::Rooted<JSObject*> contentsOwnerWrapper(
-            cx, mTemplateContentsOwner->GetWrapper());
-        if (contentsOwnerWrapper) {
-          UpdateReflectorGlobal(cx, contentsOwnerWrapper, aError);
-          if (aError.Failed()) {
-            return nullptr;
-          }
-        }
+  // Step 9 -- clear event listeners from our window, if we have one.
+  //
+  // Note that we explicitly want the inner window, and only if we're its
+  // document.  We want to do this (per spec) even when we're not the "active
+  // document", so we can't go through GetWindow(), because it might forward to
+  // the wrong inner.
+  if (nsPIDOMWindowInner* win = GetInnerWindow()) {
+    if (win->GetExtantDoc() == this) {
+      if (EventListenerManager* elm =
+              nsGlobalWindowInner::Cast(win)->GetExistingListenerManager()) {
+        elm->RemoveAllListeners();
       }
     }
   }
 
-  mDidDocumentOpen = true;
-
-  nsAutoCString contentType(GetContentTypeInternal());
-
-  // Call Reset(), this will now do the full reset
-  Reset(channel, group);
-  if (baseURI) {
-    mDocumentBaseURI = baseURI;
+  // Step 10 -- remove all our DOM kids without firing any mutation events.
+  DisconnectNodeTree();
+
+  // Step 11 -- if we're the current document in our docshell, do the
+  // equivalent of pushState() with the new URL we should have.
+  if (shell && IsCurrentActiveDocument()) {
+    nsCOMPtr<nsIURI> newURI = callerDoc->GetDocumentURI();
+    if (callerDoc != this) {
+      nsCOMPtr<nsIURI> noFragmentURI;
+      nsresult rv = NS_GetURIWithoutRef(newURI, getter_AddRefs(noFragmentURI));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        aError.Throw(rv);
+        return nullptr;
+      }
+      newURI = noFragmentURI.forget();
+    }
+
+    // UpdateURLAndHistory might do various member-setting, so make sure we're
+    // holding strong refs to all the refcounted args on the stack.  We can
+    // assume that our caller is holding on to "this" already.
+    nsCOMPtr<nsIURI> currentURI = GetDocumentURI();
+    bool equalURIs;
+    nsresult rv = currentURI->Equals(newURI, &equalURIs);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      aError.Throw(rv);
+      return nullptr;
+    }
+    nsCOMPtr<nsIStructuredCloneContainer> stateContainer(mStateObjectContainer);
+    rv = shell->UpdateURLAndHistory(this, newURI, stateContainer, EmptyString(),
+                                    /* aReplace = */ true, currentURI,
+                                    equalURIs);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      aError.Throw(rv);
+      return nullptr;
+    }
+
+    // And use the security info of the caller document as well, since
+    // it's the thing providing our data.
+    mSecurityInfo = callerDoc->GetSecurityInfo();
+
+    // This is not mentioned in the spec, but I think that's a spec bug.  See
+    // <https://github.com/whatwg/html/issues/4299>.  In any case, since our
+    // URL may be changing away from about:blank here, we really want to unset
+    // this flag no matter what, since only about:blank can be an initial
+    // document.
+    SetIsInitialDocument(false);
+
+    // And let our docloader know that it will need to track our load event.
+    nsDocShell::Cast(shell)->SetDocumentOpenedButNotLoaded();
   }
 
-  // Restore our type, since Reset() resets it.
-  SetContentTypeInternal(contentType);
-
-  // Store the security info of the caller now that we're done
-  // resetting the document.
-  mSecurityInfo = securityInfo;
-
+  // Per spec nothing happens with our URI in other cases, though note
+  // <https://github.com/whatwg/html/issues/4286>.
+
+  // Note that we don't need to do anything here with base URIs per spec.
+  // That said, this might be assuming that we implement
+  // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fallback-base-url
+  // correctly, which we don't right now for the about:blank case.
+
+  // Step 12, but note <https://github.com/whatwg/html/issues/4292>.
+  mSkipLoadEventAfterClose = mLoadEventFiring;
+
+  // Preliminary to steps 13-16.  Set our ready state to uninitialized before
+  // we do anything else, so we can then proceed to later ready state levels.
+  SetReadyStateInternal(READYSTATE_UNINITIALIZED,
+                        /* updateTimingInformation = */ false);
+
+  mDidDocumentOpen = true;
+
+  // Step 13 -- set our compat mode to standards.
+  SetCompatibilityMode(eCompatibility_FullStandards);
+
+  // Step 14 -- create a new parser associated with document.  This also does
+  // step 16 implicitly.
   mParserAborted = false;
   mParser = nsHtml5Module::NewHtml5Parser();
-  nsHtml5Module::Initialize(mParser, this, uri, shell, channel);
+  nsHtml5Module::Initialize(mParser, this, GetDocumentURI(), shell, nullptr);
   if (mReferrerPolicySet) {
     // CSP may have set the referrer policy, so a speculative parser should
     // start with the new referrer policy.
     nsHtml5TreeOpExecutor* executor = nullptr;
     executor = static_cast<nsHtml5TreeOpExecutor*>(mParser->GetContentSink());
     if (executor && mReferrerPolicySet) {
       executor->SetSpeculationReferrerPolicy(
           static_cast<ReferrerPolicy>(mReferrerPolicy));
     }
   }
 
+  // Some internal non-spec bookkeeping.
   mContentTypeForWriteCalls.AssignLiteral("text/html");
 
-  // Prepare the docshell and the document viewer for the impending
-  // out of band document.write()
-  shell->PrepareForNewContentModel();
-
-  // Now check whether we were opened with a "replace" argument.  If
-  // so, we need to tell the docshell to not create a new history
-  // entry for this load. Otherwise, make sure that we're doing a normal load,
-  // not whatever type of load was previously done on this docshell.
-  shell->SetLoadType(isReplace ? LOAD_NORMAL_REPLACE : LOAD_NORMAL);
-
-  nsCOMPtr<nsIContentViewer> cv;
-  shell->GetContentViewer(getter_AddRefs(cv));
-  if (cv) {
-    cv->LoadStart(this);
+  if (shell) {
+    // Prepare the docshell and the document viewer for the impending
+    // out-of-band document.write()
+    shell->PrepareForNewContentModel();
+
+    nsCOMPtr<nsIContentViewer> cv;
+    shell->GetContentViewer(getter_AddRefs(cv));
+    if (cv) {
+      cv->LoadStart(this);
+    }
   }
 
-  // Add a wyciwyg channel request into the document load group
-  NS_ASSERTION(!mWyciwygChannel,
-               "nsHTMLDocument::Open(): wyciwyg "
-               "channel already exists!");
-
-  // In case the editor is listening and will see the new channel
-  // being added, make sure mWriteLevel is non-zero so that the editor
-  // knows that document.open/write/close() is being called on this
-  // document.
-  ++mWriteLevel;
-
-  CreateAndAddWyciwygChannel();
-
-  --mWriteLevel;
-
-  SetReadyStateInternal(Document::READYSTATE_LOADING);
-
-  // After changing everything around, make sure that the principal on the
-  // document's realm exactly matches NodePrincipal().
-  DebugOnly<JSObject*> wrapper = GetWrapperPreserveColor();
-  MOZ_ASSERT_IF(wrapper, JS::GetRealmPrincipals(js::GetNonCCWObjectRealm(
-                             wrapper)) == nsJSPrincipals::get(NodePrincipal()));
-
-  return kungFuDeathGrip.forget();
+  // Step 15.
+  SetReadyStateInternal(Document::READYSTATE_LOADING,
+                        /* updateTimingInformation = */ false);
+
+  // Step 16 happened with step 14 above.
+
+  // Step 17.
+  return this;
 }
 
 void nsHTMLDocument::Close(ErrorResult& rv) {
   if (!IsHTMLDocument()) {
     // No calling document.close() on XHTML!
 
     rv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
@@ -1593,27 +1472,16 @@ void nsHTMLDocument::Close(ErrorResult& 
   // it be for now, though.  In any case, there's no reason to do
   // this if we have no presshell, since in that case none of the
   // above about reusing frames applies.
   //
   // XXXhsivonen keeping this around for bug 577508 / 253951 still :-(
   if (GetShell()) {
     FlushPendingNotifications(FlushType::Layout);
   }
-
-  // Removing the wyciwygChannel here is wrong when document.close() is
-  // called from within the document itself. However, legacy requires the
-  // channel to be removed here. Otherwise, the load event never fires.
-  NS_ASSERTION(mWyciwygChannel,
-               "nsHTMLDocument::Close(): Trying to remove "
-               "nonexistent wyciwyg channel!");
-  RemoveWyciwygChannel();
-  NS_ASSERTION(!mWyciwygChannel,
-               "nsHTMLDocument::Close(): "
-               "nsIWyciwygChannel could not be removed!");
 }
 
 void nsHTMLDocument::WriteCommon(JSContext* cx, const Sequence<nsString>& aText,
                                  bool aNewlineTerminate,
                                  mozilla::ErrorResult& rv) {
   // Fast path the common case
   if (aText.Length() == 1) {
     WriteCommon(cx, aText[0], aNewlineTerminate, rv);
@@ -1684,18 +1552,18 @@ void nsHTMLDocument::WriteCommon(JSConte
     if (mIgnoreDestructiveWritesCounter) {
       // Instead of implying a call to document.open(), ignore the call.
       nsContentUtils::ReportToConsole(
           nsIScriptError::warningFlag, NS_LITERAL_CSTRING("DOM Events"), this,
           nsContentUtils::eDOM_PROPERTIES, "DocumentWriteIgnored", nullptr, 0,
           mDocumentURI);
       return;
     }
-    nsCOMPtr<Document> ignored =
-        Open(cx, Optional<nsAString>(), EmptyString(), aRv);
+
+    Open(cx, Optional<nsAString>(), EmptyString(), aRv);
 
     // If Open() fails, or if it didn't create a parser (as it won't
     // if the user chose to not discard the current document through
     // onbeforeunload), don't write anything.
     if (aRv.Failed() || !mParser) {
       return;
     }
     MOZ_ASSERT(!JS_IsExceptionPending(cx),
--- a/dom/html/nsHTMLDocument.h
+++ b/dom/html/nsHTMLDocument.h
@@ -150,19 +150,19 @@ class nsHTMLDocument : public mozilla::d
                    JS::MutableHandle<JSObject*> aRetval,
                    mozilla::ErrorResult& rv) {
     JS::Rooted<JS::Value> v(cx);
     if ((aFound = ResolveName(cx, aName, &v, rv))) {
       aRetval.set(v.toObjectOrNull());
     }
   }
   void GetSupportedNames(nsTArray<nsString>& aNames);
-  already_AddRefed<Document> Open(
-      JSContext* cx, const mozilla::dom::Optional<nsAString>& /* unused */,
-      const nsAString& aReplace, mozilla::ErrorResult& aError);
+  Document* Open(JSContext* cx,
+                 const mozilla::dom::Optional<nsAString>& /* unused */,
+                 const nsAString& /* unused */, mozilla::ErrorResult& aError);
   mozilla::dom::Nullable<mozilla::dom::WindowProxyHolder> Open(
       JSContext* cx, const nsAString& aURL, const nsAString& aName,
       const nsAString& aFeatures, bool aReplace, mozilla::ErrorResult& rv);
   void Close(mozilla::ErrorResult& rv);
   void Write(JSContext* cx, const mozilla::dom::Sequence<nsString>& aText,
              mozilla::ErrorResult& rv);
   void Writeln(JSContext* cx, const mozilla::dom::Sequence<nsString>& aText,
                mozilla::ErrorResult& rv);
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -523,17 +523,16 @@ skip-if = (toolkit == 'android') || ((os
 [test_ignoreuserfocus.html]
 [test_fragment_form_pointer.html]
 [test_bug1682.html]
 [test_bug1823.html]
 [test_bug57600.html]
 [test_bug196523.html]
 [test_bug199692.html]
 skip-if = toolkit == 'android' #bug 811644
-[test_bug172261.html]
 [test_bug255820.html]
 [test_bug259332.html]
 [test_bug311681.html]
 [test_bug311681.xhtml]
 [test_bug324378.html]
 [test_bug332848.xhtml]
 [test_bug340017.xhtml]
 [test_bug359657.html]
deleted file mode 100644
--- a/dom/html/test/test_bug172261.html
+++ /dev/null
@@ -1,67 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=172261
--->
-<head>
-  <title>Test for Bug 172261</title>
-  <script type="text/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=172261">Mozilla Bug 172261</a>
-<p id="display">
-  <iframe id="test"></iframe>
-</p>
-<div id="content" style="display: none">
-  
-</div>
-<pre id="test">
-<script class="testbody" type="text/javascript">
-  /** Test for Bug 172261 **/
-  SimpleTest.waitForExplicitFinish();
-  SimpleTest.requestFlakyTimeout("untriaged");
-
-  var callable = false;
-  function toggleCallable() { callable = true; }
-
-  var doTestInIframe = false;
-
-  // Shouldn't do history stuff from inside onload
-  addLoadEvent(function() { setTimeout(startTest, 10) });
-
-  function startTest() {
-    // First, create a dummy document.  Use onunload handlers to make sure
-    // bfcache doesn't screw us up.
-    var doc = $("test").contentDocument;
-    
-    doc.write("<html><body onunload=''>First</body></html>");
-    doc.close();
-
-    // Now write our test document
-    doc.write("<html><script>window.onerror = parent.onerror; if (parent.doTestInIframe) { parent.is(document.domain, parent.document.domain, 'Domains should match');  parent.toggleCallable(); } <"  + "/script><body>Second</body></html>");
-    doc.close();
-
-    $("test").onload = goForward;
-    history.back();
-  }
-
-  function goForward() {
-      $("test").onload = doTest;
-      doTestInIframe = true;
-      history.forward();
-  }
-
-  function doTest() {
-    is($("test").contentDocument.domain, document.domain,
-       "Domains should match 2");
-    is($("test").contentDocument.location.href, location.href,
-         "Locations should match");
-    is(callable, true, "Subframe should be able to call us");
-    SimpleTest.finish();
-  }
-</script>
-</pre>
-</body>
-</html>
-
--- a/dom/html/test/test_bug255820.html
+++ b/dom/html/test/test_bug255820.html
@@ -23,100 +23,76 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 255820 **/
 SimpleTest.waitForExplicitFinish();
 
 is(document.characterSet, "UTF-8",
    "Unexpected character set for our document");
 
-var testsLeft = 4;
+var testsLeft = 3;
 
 function testFinished() {
   --testsLeft;
   if (testsLeft == 0) {
     SimpleTest.finish();
   }
 }
 
 function charsetTestFinished(id, doc, charsetTarget) {
   is(doc.characterSet, charsetTarget, "Unexpected charset for subframe " + id);
   testFinished();
 }
 
-function f2Continue() {
-//   Commented out pending discussion at the WHATWG
-//   $("f2").
-//     setAttribute("onload",
-//                  "charsetTestFinished('f2 reloaded', this.contentDocument, 'us-ascii');");
-  $("f2").
-    setAttribute("onload",
-                 "testFinished();");
-  $("f2").contentWindow.location.reload();
-}
-
 function f3Continue() {
   var doc = $("f3").contentDocument;
   is(doc.defaultView.getComputedStyle(doc.body).color, "rgb(0, 180, 0)",
-     "Wrong color before reload");
-  $("f3").
-    setAttribute("onload",
-                 'var doc = this.contentDocument; ' + 
-                 'is(doc.defaultView.getComputedStyle(doc.body, "").color, ' +
-                 '   "rgb(0, 180, 0)",' +
-                 '   "Wrong color after reload");' +
-                 "charsetTestFinished('f1', this.contentDocument, 'UTF-8')");
-  $("f3").contentWindow.location.reload();
+     "Wrong color");
+  charsetTestFinished('f3', doc, "UTF-8");
 }
 
 function runTest() {
   var doc = $("f1").contentDocument;
   is(doc.characterSet, "UTF-8",
      "Unexpected initial character set for first frame");
   doc.open();
   doc.write('<html></html>');
   doc.close();
-  is(doc.characterSet, "UTF-8",
-     "Unexpected character set for first frame after write");
-  $("f1").
-    setAttribute("onload",
-                 "charsetTestFinished('f1', this.contentDocument, 'UTF-8')");
-  $("f1").contentWindow.location.reload();
+  charsetTestFinished("f1", doc, "UTF-8");
 
   doc = $("f2").contentDocument;
   is(doc.characterSet, "UTF-8",
      "Unexpected initial character set for second frame");
   doc.open();
   var str = '<html><head>';
   str += '<script src="data:application/javascript,"><'+'/script>';
   str += '<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">';
   str += '</head><body>';
   str += '</body></html>';
   doc.write(str);
   doc.close();
   is(doc.characterSet, "UTF-8",
      "Unexpected character set for second frame after write");
   $("f2").
     setAttribute("onload",
-      "charsetTestFinished('f2', this.contentDocument, 'UTF-8');" +
-      "f2Continue()");
+      "charsetTestFinished('f2', this.contentDocument, 'UTF-8');");
 
   doc = $("f3").contentDocument;
   is(doc.characterSet, "UTF-8",
-     "Unexpected initial character set for first frame");
+     "Unexpected initial character set for third frame");
   doc.open();
   var str = '<html><head>';
   str += '<style>body { color: rgb(255, 0, 0) }</style>';
   str += '<link type="text/css" rel="stylesheet" href="data:text/css, body { color: rgb(0, 180, 0) }">';
   str += '</head><body>';
   str += '</body></html>';
   doc.write(str);
   doc.close();
   is(doc.characterSet, "UTF-8",
-     "Unexpected character set for first frame after write");
+     "Unexpected character set for third frame after write");
   $("f3").setAttribute("onload", "f3Continue()");
 }
 
 addLoadEvent(runTest);
 </script>
 </pre>
 </body>
 </html>
--- a/dom/tests/mochitest/bugs/test_bug346659.html
+++ b/dom/tests/mochitest/bugs/test_bug346659.html
@@ -103,17 +103,17 @@ function messageReceiver(evt) {
       break;
     case 4:
       is(testResult, "4", "Props on window opened from new window should be preserved when writing");
       break;
     case 5:
       is(testResult, "undefined", "Props on new window's child should go away when loading");
       break;
     case 6:
-      is(testResult, "undefined", "Props on new window's child should go away when writing");
+      is(testResult, "6", "Props on new window's child should not go away when writing");
       break;
     case 7:
       is(testResult, "7", "Props on different-domain window opened from different-domain new window can stay");
       break;
     case 9:
       is(testResult, "undefined", "Props on different-domain new window's child should go away when loading");
       break;
     case 11:
--- a/js/xpconnect/crashtests/crashtests.list
+++ b/js/xpconnect/crashtests/crashtests.list
@@ -36,17 +36,17 @@ skip-if(cocoaWidget&&isDebugBuild) load 
 load 648206-1.html
 load 720305-1.html
 load 721910.html
 load 723465.html
 load 732870.html
 load 751995.html
 load 761831.html
 asserts(0-1) load 752038.html # We may hit bug 645229 here.
-asserts(1) load 753162.html # We hit bug 675518 or bug 680086 here.
+load 753162.html
 load 754311.html
 asserts(0-1) load 786142.html # We may hit bug 645229 here.
 load 797583.html
 load 806751.html
 load 833856.html
 load 851418.html
 load 854139.html
 load 854604.html
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -1023,16 +1023,19 @@ nsDocumentViewer::LoadComplete(nsresult 
   // Now, fire either an OnLoad or OnError event to the document...
   bool restoring = false;
   // XXXbz imagelib kills off the document load for a full-page image with
   // NS_ERROR_PARSED_DATA_CACHED if it's in the cache.  So we want to treat
   // that one as a success code; otherwise whether we fire onload for the image
   // will depend on whether it's cached!
   if (window &&
       (NS_SUCCEEDED(aStatus) || aStatus == NS_ERROR_PARSED_DATA_CACHED)) {
+    // If this code changes, the code in nsDocLoader::DocLoaderIsEmpty
+    // that fires load events for document.open() cases might need to
+    // be updated too.
     nsEventStatus status = nsEventStatus_eIgnore;
     WidgetEvent event(true, eLoad);
     event.mFlags.mBubbles = false;
     event.mFlags.mCancelable = false;
     // XXX Dispatching to |window|, but using |document| as the target.
     event.mTarget = mDocument;
 
     // If the document presentation is being restored, we don't want to fire
@@ -1089,17 +1092,19 @@ nsDocumentViewer::LoadComplete(nsresult 
       // Notify any devtools about the load.
       RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get();
 
       if (timelines && timelines->HasConsumer(docShell)) {
         timelines->AddMarkerForDocShell(
             docShell, MakeUnique<DocLoadingTimelineMarker>("document::Load"));
       }
 
+      d->SetLoadEventFiring(true);
       EventDispatcher::Dispatch(window, mPresContext, &event, nullptr, &status);
+      d->SetLoadEventFiring(false);
       if (timing) {
         timing->NotifyLoadEventEnd();
       }
 
       nsPIDOMWindowInner* innerWindow = window->GetCurrentInnerWindow();
       if (innerWindow) {
         innerWindow->QueuePerformanceNavigationTiming();
       }
--- a/layout/style/test/test_bug1232829.html
+++ b/layout/style/test/test_bug1232829.html
@@ -14,17 +14,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 // This should be a crashtest but it relies on using a pop-up window which
 // isn't supported in crashtests.
 function boom() {
   var popup = window.open("data:text/html,2");
   setTimeout(function() {
     var frameDoc = document.querySelector("iframe").contentDocument;
     frameDoc.write("3");
-    frameDoc.defaultView.history.back();
     requestAnimationFrame(function() {
       popup.close();
       ok(true, "Didn't crash");
       SimpleTest.finish();
     });
   }, 0);
 }
 
--- a/parser/htmlparser/tests/mochitest/test_bug715739.html
+++ b/parser/htmlparser/tests/mochitest/test_bug715739.html
@@ -28,45 +28,61 @@ function textChildren(node) {
     if (n.nodeType == Node.TEXT_NODE) {
       s += n.nodeValue;
     }
     n = n.nextSibling;
   }
   return s;
 }
 
+var f, d;
+
 function tick() {
   runNumber++;
-  var f = document.getElementsByTagName("iframe")[0];
-  var d = f.contentDocument;
+  f = document.getElementsByTagName("iframe")[0];
+  d = f.contentDocument;
   var text;
 
   if (runNumber == 1) {
-    d.open();
-    f.addEventListener("load", tick);
-    d.write("X");
-    d.write("\u003cscript>document.write('Y');\u003c/script>");
-    d.write("Z");
-    d.close();
+    frames[1].setTimeout(`
+      var d = parent.d;
+      var f = parent.f;
+      d.open();
+      f.addEventListener("load", parent.tick);
+      d.write("X");
+      d.write("\u003cscript>document.write('Y');\u003c/script>");
+      d.write("Z");
+      d.close();
+    `);
     return;
   }
 
   if (runNumber == 2) {
     text = textChildren(d.body);
     is(text, "XYZ", "Wrong text before reload.");
     f.contentWindow.location.reload();
     return;
   }
 
   if (runNumber == 3) {
     text = textChildren(d.body);
-    is(text, "XYZ", "Wrong text after reload.");
+    is(text, "ABC", "Wrong text after reload.");
     SimpleTest.finish();
   }
 }
 
+// We want to trigger a document.open/write with a different window as the
+// entry global.  Let's give that window a blob URL so we don't have to set up
+// extra helper files.
+var blob = new Blob(["ABC"], { type: "text/html" });
+var blobURL = URL.createObjectURL(blob);
+
 </script>
 </pre>
 <div id="content" style="display: none">
   <iframe></iframe>
+  <iframe></iframe>
 </div>
+<script>
+  document.querySelectorAll("iframe")[1].src = blobURL;
+</script>
 </body>
 </html>
--- a/security/manager/ssl/tests/mochitest/mixedcontent/test_bug329869.html
+++ b/security/manager/ssl/tests/mochitest/mixedcontent/test_bug329869.html
@@ -19,17 +19,17 @@
       newElement.src = "http://example.org/tests/security/manager/ssl/tests/" +
                        "mochitest/mixedcontent/bug329869.js";
       document.body.appendChild(newElement);
     }, 0);
   }
 
   function afterNavigationTest()
   {
-    isSecurityState("broken", "security still broken after navigation");
+    isSecurityState("secure", "when we navigate back, we're loading our secure page again and not loading an insecure script, so our security state is secure");
     finish();
   }
 
   </script>
 </head>
 
 <body>
 </body>
deleted file mode 100644
--- a/testing/web-platform/meta/custom-elements/parser/parser-uses-registry-of-owner-document.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[parser-uses-registry-of-owner-document.html]
-  [HTML parser must use the registry of window.document in a document created by document.implementation.createHTMLDocument()]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/015.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[015.html]
-  [global scope unchanged]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/016.html.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[016.html]
-  [Timeout on original window, scope]
-    expected: FAIL
-
-  [Timeout on new window, scope]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-immediate.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-immediate.window.js.ini
@@ -1,23 +1,2 @@
 [abort-refresh-immediate.window.html]
-  expected:
-    if (os == "win"): OK
-    if (os == "android"): OK
-    TIMEOUT
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (fetch())]
-    expected:
-      if (os == "win"): PASS
-      if (os == "android"): PASS
-      TIMEOUT
-
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (image loading)]
-    expected:
-      if (os == "win"): PASS
-      if (os == "android"): PASS
-      TIMEOUT
-
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (XMLHttpRequest)]
-    expected:
-      if (os == "win"): PASS
-      if (os == "android"): PASS
-      TIMEOUT
-
+  disabled: https://github.com/web-platform-tests/wpt/issues/14942
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-multisecond-header.window.js.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-[abort-refresh-multisecond-header.window.html]
-  expected: TIMEOUT
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 1-sec timeout (fetch())]
-    expected:
-      if (os == "win"): FAIL
-      if (os == "android"): FAIL
-      TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 4-sec timeout (image loading)]
-    expected: TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 1-sec timeout (XMLHttpRequest)]
-    expected:
-      if (os == "win"): FAIL
-      if (os == "android"): FAIL
-      TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-refresh-multisecond-meta.window.js.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-[abort-refresh-multisecond-meta.window.html]
-  expected: TIMEOUT
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 4-sec timeout (image loading)]
-    expected: TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 1-sec timeout (XMLHttpRequest)]
-    expected: FAIL
-
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 1-sec timeout (fetch())]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-while-navigating.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort-while-navigating.window.js.ini
@@ -1,29 +1,2 @@
 [abort-while-navigating.window.html]
-  disabled:  https://bugzilla.mozilla.org/show_bug.cgi?id=1490978
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 1-sec timeout (fetch())]
-    expected: TIMEOUT
-
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (fetch())]
-    expected: TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 1-sec timeout (XMLHttpRequest)]
-    expected: TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 1-sec timeout (XMLHttpRequest)]
-    expected: FAIL
-
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 1-sec timeout (fetch())]
-    expected: FAIL
-
-  [document.open() does NOT abort documents that are queued for navigation through Refresh header with 4-sec timeout (image loading)]
-    expected: TIMEOUT
-
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (XMLHttpRequest)]
-    expected: TIMEOUT
-
-  [document.open() aborts documents that are queued for navigation through Refresh header with timeout 0 (image loading)]
-    expected: TIMEOUT
-
-  [document.open() does NOT abort documents that are queued for navigation through <meta> refresh with 4-sec timeout (image loading)]
-    expected: TIMEOUT
-
+  disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1490978 and https://github.com/web-platform-tests/wpt/issues/14943
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/abort.sub.window.js.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[abort.sub.window.html]
-  disabled:
-    if debug and (os == "mac"): https://bugzilla.mozilla.org/show_bug.cgi?id=1495175
-  expected: TIMEOUT
-  [document.open() does not abort documents that are not navigating (image loading)]
-    expected: TIMEOUT
-
-  [document.open() does not abort documents that are not navigating (XMLHttpRequest)]
-    expected: FAIL
-
-  [document.open() does not abort documents that are not navigating (already established WebSocket connection)]
-    expected: FAIL
-
-  [document.open() does not abort documents that are not navigating (establish a WebSocket connection)]
-    expected: FAIL
-
-  [document.open() does not abort documents that are not navigating (fetch())]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/aborted-parser.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/aborted-parser.window.js.ini
@@ -1,7 +1,8 @@
 [aborted-parser.window.html]
   [document.open() after parser is aborted]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1475000
 
   [async document.open() after parser is aborted]
     expected: FAIL
-
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1475000
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/active.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/active.window.js.ini
@@ -1,19 +1,8 @@
 [active.window.html]
   [document.open() removes the document's children (non-active document with an associated Window object; frame is removed)]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
 
   [document.open() removes the document's children (active but not fully active document)]
     expected: FAIL
-
-  [document.open() removes the document's children (non-active document without an associated Window object; createHTMLDocument)]
-    expected: FAIL
-
-  [document.open() removes the document's children (non-active document without an associated Window object; XMLHttpRequest)]
-    expected: FAIL
-
-  [document.open() removes the document's children (non-active document without an associated Window object; DOMParser)]
-    expected: FAIL
-
-  [document.open() removes the document's children (non-active document with an associated Window object; navigated away)]
-    expected: FAIL
-
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/bailout-exception-vs-return-origin.sub.window.js.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-[bailout-exception-vs-return-origin.sub.window.html]
-  [document.open should throw a SecurityError with cross-origin document even when the ignore-opens-during-unload counter is greater than 0 (during unload event)]
-    expected: FAIL
-
-  [document.open should throw a SecurityError with cross-origin document even when there is an active parser executing script]
-    expected: FAIL
-
-  [document.open should throw a SecurityError with cross-origin document even when the ignore-opens-during-unload counter is greater than 0 (during pagehide event)]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/bailout-side-effects-ignore-opens-during-unload.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/bailout-side-effects-ignore-opens-during-unload.window.js.ini
@@ -1,4 +1,4 @@
 [bailout-side-effects-ignore-opens-during-unload.window.html]
   [document.open bailout should not have any side effects (ignore-opens-during-unload is greater than 0 during beforeunload event)]
     expected: FAIL
-
+    bug: https://github.com/web-platform-tests/wpt/issues/14909
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/beforeunload.window.js.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[beforeunload.window.html]
-  [document.open() should not fire a beforeunload event]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/document.open-03.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[document.open-03.html]
-  [document.open and no singleton replacement]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini
@@ -1,37 +1,17 @@
 [event-listeners.window.html]
-  [Event listeners are to be removed]
-    expected: FAIL
-
-  [Event listeners are to be removed from shadow trees as well]
-    expected: FAIL
-
-  [Custom event listeners are to be removed]
-    expected: FAIL
-
-  [IDL attribute event handlers are to be deactivated]
-    expected: FAIL
-
-  [Custom event listeners are to be removed from Window]
-    expected: FAIL
-
-  [Standard event listeners are to be removed from Window]
-    expected: FAIL
-
-  [Standard event listeners are to be removed]
-    expected: FAIL
-
-  [Content attribute event handlers are to be deactivated]
-    expected: FAIL
-
   [Standard event listeners are to be removed from Window for a non-active document that is the associated Document of a Window (frame is removed)]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
 
   [Custom event listeners are to be removed from Window for an active but not fully active document]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
 
   [Custom event listeners are to be removed from Window for a non-active document that is the associated Document of a Window (frame is removed)]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
 
   [Standard event listeners are to be removed from Window for an active but not fully active document]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1520333
 
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/history.window.js.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[history.window.html]
-  [document.open should not add an entry to the session history]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/no-new-global.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/no-new-global.window.js.ini
@@ -1,46 +1,6 @@
 [no-new-global.window.html]
-  [Location maintains its prototype and properties through open()]
-    expected: FAIL
-
-  [Navigator maintains its prototype and properties through open()]
-    expected: FAIL
-
-  [Document maintains its prototype and properties through open()]
-    expected: FAIL
-
-  [Obtaining a variable from a global whose document had open() invoked]
-    expected: FAIL
-
-  [WindowProxy maintains its prototype and properties through open()]
-    expected: FAIL
-
-  [Location maintains object identity through open()]
-    expected: FAIL
-
-  [Navigator maintains object identity through open()]
-    expected: FAIL
-
   [sessionStorage maintains its prototype and properties through open()]
     expected: FAIL
 
-  [BarProp maintains its prototype and properties through open()]
-    expected: FAIL
-
-  [localStorage maintains object identity through open()]
-    expected: FAIL
-
-  [BarProp maintains object identity through open()]
-    expected: FAIL
-
-  [History maintains object identity through open()]
-    expected: FAIL
-
-  [sessionStorage maintains object identity through open()]
-    expected: FAIL
-
   [localStorage maintains its prototype and properties through open()]
     expected: FAIL
-
-  [History maintains its prototype and properties through open()]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/quirks.window.js.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[quirks.window.html]
-  [document.open() sets document to no-quirks mode (write no doctype)]
-    expected: FAIL
-
-  [document.open() sets document to no-quirks mode (write old doctype)]
-    expected: FAIL
-
-  [document.open() sets document to no-quirks mode (write new doctype)]
-    expected: FAIL
-
-  [document.open() sets document to no-quirks mode, not limited-quirks mode]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/reload.window.js.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[reload.window.html]
-  [reload]
-    expected: FAIL
-
-  [Reloading a document.open()'d page should reload the URL of the entry realm's responsible document]
-    expected: FAIL
-
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/tasks.window.js.ini
+++ b/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/tasks.window.js.ini
@@ -1,14 +1,13 @@
 [tasks.window.html]
   expected: TIMEOUT
-  [document.open() and tasks (timeout)]
-    expected: TIMEOUT
-
   [document.open() and tasks (marquee start)]
     expected: TIMEOUT
 
   [tasks without document.open() (Promise rejection)]
     expected: TIMEOUT
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1362272
 
   [document.open() and tasks (Promise rejection)]
     expected: TIMEOUT
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1362272
 
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/url-fragment.window.js.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[url-fragment.window.html]
-  [document.open() and document's URL containing a fragment (entry is not relevant)]
-    expected: FAIL
-
--- a/testing/web-platform/tests/common/object-association.js
+++ b/testing/web-platform/tests/common/object-association.js
@@ -36,31 +36,29 @@ window.testIsPerWindow = propertyName =>
       const after = frame[propertyName];
       assert_equals(after, before);
       t.done();
     });
 
     iframe.src = "/common/blank.html";
   }, `Navigating from the initial about:blank must not replace window.${propertyName}`);
 
-  // Note: document.open()'s spec doesn't match most browsers; see https://github.com/whatwg/html/issues/1698.
-  // However, as explained in https://github.com/whatwg/html/issues/1698#issuecomment-298748641, even an updated spec
-  // will probably still reset Window-associated properties.
+  // Per spec, document.open() should not change any of the Window state.
   async_test(t => {
     const iframe = document.createElement("iframe");
 
     iframe.onload = t.step_func_done(() => {
       const frame = iframe.contentWindow;
       const before = frame[propertyName];
       assert_true(before !== undefined && before !== null, `window.${propertyName} must be implemented`);
 
       frame.document.open();
 
       const after = frame[propertyName];
-      assert_not_equals(after, before);
+      assert_equals(after, before);
 
       frame.document.close();
     });
 
     iframe.src = "/common/blank.html";
     document.body.appendChild(iframe);
   }, `document.open() must replace window.${propertyName}`);
 };
--- a/testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html
+++ b/testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html
@@ -120,37 +120,35 @@ promise_test(function () {
         var instance2 = doc.querySelector('my-custom-element');
         assert_true(instance2 instanceof Element);
         assert_false(instance2 instanceof MyCustomElement);
     });
 }, 'HTML parser must use the registry of window.document in a document created by XMLHttpRequest');
 
 test_with_window(function (contentWindow, contentDocument) {
     const element = define_custom_element_in_window(contentWindow, 'my-custom-element', []);
-    // document-open-steps spec doesn't match most browsers; see https://github.com/whatwg/html/issues/1698.
-    // However, as explained in https://github.com/whatwg/html/issues/1698#issuecomment-298748641
-    // the custom element registry will be replaced after document-open-steps.
+    // document-open-steps spec doesn't do anything with the custom element
+    // registry, so it should just stick around.
     contentDocument.write('<my-custom-element></my-custom-element>');
 
     var instance = contentDocument.querySelector('my-custom-element');
 
     assert_true(instance instanceof contentWindow.HTMLElement);
-    assert_false(instance instanceof element.class);
+    assert_true(instance instanceof element.class);
 
 }, 'document.write() must not instantiate a custom element without a defined insertion point');
 
 test_with_window(function (contentWindow, contentDocument) {
     const element = define_custom_element_in_window(contentWindow, 'my-custom-element', []);
-    // document-open-steps spec doesn't match most browsers; see https://github.com/whatwg/html/issues/1698.
-    // However, as explained in https://github.com/whatwg/html/issues/1698#issuecomment-298748641
-    // the custom element registry will be replaced after document-open-steps.
+    // document-open-steps spec doesn't do anything with the custom element
+    // registry, so it should just stick around.
     contentDocument.writeln('<my-custom-element></my-custom-element>');
 
     var instance = contentDocument.querySelector('my-custom-element');
 
     assert_true(instance instanceof contentWindow.HTMLElement);
-    assert_false(instance instanceof element.class);
+    assert_true(instance instanceof element.class);
 
 }, 'document.writeln() must not instantiate a custom element without a defined insertion point');
 
 </script>
 </body>
 </html>
--- a/testing/web-platform/tests/custom-elements/reactions/Document.html
+++ b/testing/web-platform/tests/custom-elements/reactions/Document.html
@@ -125,42 +125,32 @@ test_with_window(function (contentWindow
     assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
 
     contentDocument.write('');
     assert_array_equals(element.takeLog().types(), ['disconnected']);
 }, 'write on Document must enqueue disconnectedCallback when removing a custom element');
 
 test_with_window(function (contentWindow, contentDocument) {
     contentWindow.document.open();
-    // document.open()'s spec doesn't match most browsers; see https://github.com/whatwg/html/issues/1698.
-    // However, as explained in https://github.com/whatwg/html/issues/1698#issuecomment-298748641
-    // the custom element registry will be replaced after document.open() call,
-    // So call customElements.define() after that in order to register defintion
-    // to correct custom elements registry.
     const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
     contentWindow.document.write('<custom-element></custom-element>');
     assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
 }, 'write on Document must enqueue connectedCallback after constructing a custom element');
 
 test_with_window(function (contentWindow, contentDocument) {
     const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
     contentDocument.body.innerHTML = '<custom-element></custom-element>';
     assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
 
     contentDocument.writeln('');
     assert_array_equals(element.takeLog().types(), ['disconnected']);
 }, 'writeln on Document must enqueue disconnectedCallback when removing a custom element');
 
 test_with_window(function (contentWindow) {
     contentWindow.document.open();
-    // document.open()'s spec doesn't match most browsers; see https://github.com/whatwg/html/issues/1698.
-    // However, as explained in https://github.com/whatwg/html/issues/1698#issuecomment-298748641
-    // the custom element registry will be replaced after document.open() call,
-    // So call customElements.define() after that in order to register defintion
-    // to correct custom elements registry.
     const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
     contentWindow.document.writeln('<custom-element></custom-element>');
     assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
 }, 'writeln on Document must enqueue connectedCallback after constructing a custom element');
 
 </script>
 </body>
 </html>
--- a/testing/web-platform/tests/feature-policy/experimental-features/document-write.tentative.html
+++ b/testing/web-platform/tests/feature-policy/experimental-features/document-write.tentative.html
@@ -4,20 +4,26 @@
 <script src="/resources/testharnessreport.js"></script>
 <script src="/feature-policy/experimental-features/resources/common.js"></script>
 <style>
 html, body {
   height: 100%;
   width: 100%;
 }
 </style>
-<iframe></iframe>
+<body>
 <script>
   "use strict";
 
+  function newIframe() {
+    var i = document.createElement("iframe");
+    document.body.appendChild(i);
+    return i;
+  }
+
   let iframeElement = document.querySelector("iframe");
   let url = url_base + "document-write.html";
 
   let text_to_write = "<div>FOO<\/div>";
   let test_cases = [{
                       api: "open",
                       query: "body",
                       expected_value_enabled: false,
@@ -37,16 +43,17 @@ html, body {
                       query: "div",
                       expected_value_enabled: "FOO"
                     }];
 
   // The feature 'document-write' is enabled by default and when it
   // is enabled, all dynamic markup insertion API work as intended.
   test_cases.forEach((tc) => {
     promise_test(async() => {
+      let iframeElement = newIframe();
       await loadUrlInIframe(iframeElement, url);
       await sendMessageAndGetResponse(iframeElement.contentWindow, tc).then((response) => {
         assert_false(
           response.did_throw_exception,
           `When feature is disabled, invoking 'document.${tc.api}' should not` +
           " throw an exception.");
         if (tc.query) {
           assert_equals(
@@ -57,16 +64,17 @@ html, body {
       });
     }, `Verify 'document.${tc.api}' is not normally blocked.` );
   });
 
 
   // Disabling 'document-write' throws exception on the included API.
   test_cases.forEach((tc) => {
     promise_test(async() => {
+      let iframeElement = newIframe();
       setFeatureState(iframeElement, "document-write", "'none'");
       await loadUrlInIframe(iframeElement, url);
       await sendMessageAndGetResponse(iframeElement.contentWindow, tc).then((response) => {
         assert_true(
           response.did_throw_exception,
           `When feature is enabled, invoking 'document.${tc.api}' should ` +
           " throw an exception.");
         if (tc.query) {
@@ -75,8 +83,9 @@ html, body {
             tc.expected_value_enabled,
             `The added script tag by 'document.${tc.api}' must not have run.`);
         }
       });
     }, `Verify 'document.${tc.api}' is blocked when the feature is disabled.` );
   });
 
 </script>
+</body>
--- a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1-1.html
@@ -1,15 +1,12 @@
 <!doctype html>
 2
 <script>
   onunload = function() {}
   opener.pages.push(2);
   onload = function() {
     setTimeout(function() {
-      document.write("<!doctype html>3<script>opener.pages.push(3); if(!opener.started) {opener.started = true; history.go(-1);} opener.start_test_wait();<\/script>");
+      document.write("<!doctype html>3<script>opener.pages.push(3); if(!opener.started) {opener.started = true; history.go(-1);}<\/script>");
       document.close();
-      if (opener.started) {
-        opener.start_test_wait();
-      }
     }, 100);
   }
 </script>
--- a/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html
+++ b/testing/web-platform/tests/html/browsers/history/the-history-interface/traverse_the_history_write_after_load_1.html
@@ -6,22 +6,21 @@
 <script>
   var t = async_test();
   started = false;
   pages = []
   start_test_wait = t.step_func(
     function() {
       check_result = t.step_func(
         function() {
-          if (pages.length < 4) {
+          if (pages.length < 3) {
               setTimeout(check_result, 500);
               return
           }
-          //The pass condition here is based on the idea that the spec is wrong and browsers are right
-          assert_array_equals(pages, [2, 3, 2, 3], "Pages opened during history navigation");
+          assert_array_equals(pages, [2, 3, 1], "Pages opened during history navigation");
           t.done();
         }
       )
       setTimeout(check_result, 500);
     }
   );
   t.step(function() {
     win = window.open("history_entry.html?urls=traverse_the_history_write_after_load_1-1.html");
--- a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write-1.html
@@ -1,14 +1,19 @@
 <!doctype html>
 1
 <script>
 function f() {
   opener.postMessage("original", "*");
+  if (opener.data.length >= 2) {
+    // If we proceed here, then our document.write will be racing with the
+    // setTimeout in our opener.  Just stop.
+    return;
+  }
   setTimeout(function () {
     document.open();
     document.write("<!doctype html>2<script>opener.postMessage('written', '*');<\/script>");
     document.close();
-  }), 100;
+  });
 }
 
 window.onload = f
 </script>
--- a/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html
+++ b/testing/web-platform/tests/html/browsers/history/the-location-interface/reload_document_open_write.html
@@ -6,21 +6,21 @@
 <script>
 var win = window.open("reload_document_open_write-1.html");
 var t = async_test();
 
 var data = [];
 
 window.onmessage = t.step_func(function(e) {
   data.push(e.data);
-  if (data.length < 3) {
+  if (data.length == 2) {
     win.location.reload();
-  } else {
+  } else if (data.length >= 3) {
     setTimeout(t.step_func(function() {
-      assert_array_equals(data, ["original", "written", "written"]);
+      assert_array_equals(data, ["original", "written", "original"]);
       t.done();
     }), 500);
   }
 });
 
 add_completion_callback(function() {win.close()});
 </script>
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/closing-the-input-stream/load-event-after-location-set-during-write.window.js
@@ -0,0 +1,19 @@
+// Make sure that the load event for an iframe doesn't fire at the
+// point when a navigation triggered by document.write() starts in it,
+// but rather when that navigation completes.
+
+async_test(t => {
+  const frame = document.body.appendChild(document.createElement("iframe"));
+  const doc = frame.contentDocument;
+  const url = URL.createObjectURL(new Blob(["PASS"], { type: "text/html"}));
+
+  frame.onload = t.step_func_done(() => {
+    assert_equals(frame.contentDocument.body.textContent, "PASS",
+                  "Why is our load event firing before the new document loaded?");
+  });
+
+  doc.open();
+  doc.write(`FAIL<script>location = "${url}"</` + "script>");
+  doc.close();
+}, "Setting location from document.write() call should not trigger load event until that load completes");
+
--- a/testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/http-refresh.py
+++ b/testing/web-platform/tests/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/http-refresh.py
@@ -1,3 +1,3 @@
 def main(request, response):
     time = request.url_parts.query if request.url_parts.query else '0'
-    return 200, [['Refresh', time]], ''
+    return 200, [('Refresh', time), ('Content-Type', "text/html")], ''
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
@@ -120,17 +120,24 @@ add_task(async function test_contentscri
     return;
   }
 
   let script = async () => {
     /* globals x */
     browser.test.assertEq(1, x, "Should only run once");
 
     if (top !== window) {
-      await new Promise(resolve => setTimeout(resolve, 0));
+      // Wait for our parent page to load, then set a timeout to wait for the
+      // document.open call, so we make sure to not tear down the extension
+      // until after we've done the document.open.
+      await new Promise(resolve => {
+        top.addEventListener("load",
+                             () => setTimeout(resolve, 0),
+                             {once: true});
+      });
     }
 
     browser.test.sendMessage("content-script", [location.href, top === window]);
   };
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
@@ -166,23 +173,16 @@ add_task(async function test_contentscri
   if (pageURL === "about:blank") {
     equal(pageIsTop, true);
     [pageURL, pageIsTop] = await extension.awaitMessage("content-script");
   }
 
   Assert.deepEqual([pageURL, pageIsTop], [url, true]);
 
   let [frameURL, isTop] = await extension.awaitMessage("content-script");
-  // Sometimes we get a content script load for the initial about:blank
-  // iframe here, sometimes we don't.
-  if (frameURL === "about:blank") {
-    equal(isTop, false);
-    [frameURL, isTop] = await extension.awaitMessage("content-script");
-  }
-
   Assert.deepEqual([frameURL, isTop], [url, false]);
 
   await contentPage.close();
   await extension.unload();
 });
 
 // This test verify that a cached script is still able to catch the document
 // while it is still loading (when we do not block the document parsing as
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "nspr.h"
 #include "mozilla/dom/Document.h"
+#include "mozilla/BasicEvents.h"
 #include "mozilla/Components.h"
+#include "mozilla/EventDispatcher.h"
 #include "mozilla/Logging.h"
 #include "mozilla/IntegerPrintfMacros.h"
 
 #include "nsDocLoader.h"
 #include "nsNetUtil.h"
 #include "nsIHttpChannel.h"
 #include "nsIWebNavigation.h"
 #include "nsIWebProgressListener2.h"
@@ -21,32 +23,36 @@
 #include "nsIURL.h"
 #include "nsCOMPtr.h"
 #include "nscore.h"
 #include "nsIWeakReferenceUtils.h"
 #include "nsAutoPtr.h"
 #include "nsQueryObject.h"
 
 #include "nsIDOMWindow.h"
+#include "nsPIDOMWindow.h"
 
 #include "nsIStringBundle.h"
 #include "nsIScriptSecurityManager.h"
 
 #include "nsITransport.h"
 #include "nsISocketTransport.h"
 #include "nsIDocShell.h"
 #include "mozilla/dom/Document.h"
 #include "nsPresContext.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsILoadURIDelegate.h"
 #include "nsIBrowserDOMWindow.h"
 
 using namespace mozilla;
 using mozilla::DebugOnly;
+using mozilla::eLoad;
+using mozilla::EventDispatcher;
 using mozilla::LogLevel;
+using mozilla::WidgetEvent;
 using mozilla::dom::Document;
 
 //
 // Log module for nsIDocumentLoader logging...
 //
 // To enable logging (see mozilla/Logging.h for full details):
 //
 //    set MOZ_LOG=DocLoader:5
@@ -103,17 +109,18 @@ nsDocLoader::nsDocLoader()
       mMaxSelfProgress(0),
       mCurrentTotalProgress(0),
       mMaxTotalProgress(0),
       mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)),
       mCompletedTotalProgress(0),
       mIsLoadingDocument(false),
       mIsRestoringDocument(false),
       mDontFlushLayout(false),
-      mIsFlushingLayout(false) {
+      mIsFlushingLayout(false),
+      mDocumentOpenedButNotLoaded(false) {
   ClearInternalProgress();
 
   MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: created.\n", this));
 }
 
 nsresult nsDocLoader::SetDocLoaderParent(nsDocLoader* aParent) {
   mParent = aParent;
   return NS_OK;
@@ -259,17 +266,17 @@ bool nsDocLoader::IsBusy() {
   //   3. It's currently flushing layout in DocLoaderIsEmpty().
   //
 
   if (mChildrenInOnload.Count() || mIsFlushingLayout) {
     return true;
   }
 
   /* Is this document loader busy? */
-  if (!mIsLoadingDocument) {
+  if (!IsBlockingLoadEvent()) {
     return false;
   }
 
   bool busy;
   rv = mLoadGroup->IsPending(&busy);
   if (NS_FAILED(rv)) {
     return false;
   }
@@ -372,16 +379,17 @@ nsDocLoader::OnStartRequest(nsIRequest* 
   bool bJustStartedLoading = false;
 
   nsLoadFlags loadFlags = 0;
   request->GetLoadFlags(&loadFlags);
 
   if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) {
     bJustStartedLoading = true;
     mIsLoadingDocument = true;
+    mDocumentOpenedButNotLoaded = false;
     ClearInternalProgress();  // only clear our progress if we are starting a
                               // new load....
   }
 
   //
   // Create a new nsRequestInfo for the request that is starting to
   // load...
   //
@@ -452,19 +460,21 @@ nsDocLoader::OnStopRequest(nsIRequest* a
     nsAutoCString name;
     aRequest->GetName(name);
 
     uint32_t count = 0;
     if (mLoadGroup) mLoadGroup->GetActiveCount(&count);
 
     MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
             ("DocLoader:%p: OnStopRequest[%p](%s) status=%" PRIx32
-             " mIsLoadingDocument=%s, %u active URLs",
+             " mIsLoadingDocument=%s, mDocumentOpenedButNotLoaded=%s,"
+             " %u active URLs",
              this, aRequest, name.get(), static_cast<uint32_t>(aStatus),
-             (mIsLoadingDocument ? "true" : "false"), count));
+             (mIsLoadingDocument ? "true" : "false"),
+             (mDocumentOpenedButNotLoaded ? "true" : "false"), count));
   }
 
   bool bFireTransferring = false;
 
   //
   // Set the Maximum progress to the same value as the current progress.
   // Since the URI has finished loading, all the data is there.  Also,
   // this will allow a more accurate estimation of the max progress (in case
@@ -569,20 +579,19 @@ nsDocLoader::OnStopRequest(nsIRequest* a
   //
   doStopURLLoad(aRequest, aStatus);
 
   // Clear this request out of the hash to avoid bypass of FireOnStateChange
   // when address of the request is reused.
   RemoveRequestInfo(aRequest);
 
   //
-  // Only fire the DocLoaderIsEmpty(...) if the document loader has initiated a
-  // load.  This will handle removing the request from our hashtable as needed.
+  // Only fire the DocLoaderIsEmpty(...) if we may need to fire onload.
   //
-  if (mIsLoadingDocument) {
+  if (IsBlockingLoadEvent()) {
     nsCOMPtr<nsIDocShell> ds =
         do_QueryInterface(static_cast<nsIRequestObserver*>(this));
     bool doNotFlushLayout = false;
     if (ds) {
       // Don't do unexpected layout flushes while we're in process of restoring
       // a document from the bfcache.
       ds->GetRestoringDocument(&doNotFlushLayout);
     }
@@ -610,35 +619,37 @@ NS_IMETHODIMP nsDocLoader::GetDocumentCh
     *aChannel = nullptr;
     return NS_OK;
   }
 
   return CallQueryInterface(mDocumentRequest, aChannel);
 }
 
 void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) {
-  if (mIsLoadingDocument) {
+  if (IsBlockingLoadEvent()) {
     /* In the unimagineably rude circumstance that onload event handlers
        triggered by this function actually kill the window ... ok, it's
        not unimagineable; it's happened ... this deathgrip keeps this object
        alive long enough to survive this function call. */
     nsCOMPtr<nsIDocumentLoader> kungFuDeathGrip(this);
 
     // Don't flush layout if we're still busy.
     if (IsBusy()) {
       return;
     }
 
     NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up");
-    NS_ASSERTION(mDocumentRequest, "No Document Request!");
+    // We may not have a document request if we are in a
+    // document.open() situation.
+    NS_ASSERTION(mDocumentRequest || mDocumentOpenedButNotLoaded,
+                 "No Document Request!");
 
     // The load group for this DocumentLoader is idle.  Flush if we need to.
     if (aFlushLayout && !mDontFlushLayout) {
-      nsCOMPtr<mozilla::dom::Document> doc =
-          do_GetInterface(GetAsSupports(this));
+      nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this));
       if (doc) {
         // We start loads from style resolution, so we need to flush out style
         // no matter what.  If we have user fonts, we also need to flush layout,
         // since the reflow is what starts font loads.
         mozilla::FlushType flushType = mozilla::FlushType::Style;
         // Be safe in case this presshell is in teardown now
         doc->FlushUserFontSet();
         if (doc->GetUserFontSet()) {
@@ -647,19 +658,24 @@ void nsDocLoader::DocLoaderIsEmpty(bool 
         mDontFlushLayout = mIsFlushingLayout = true;
         doc->FlushPendingNotifications(flushType);
         mDontFlushLayout = mIsFlushingLayout = false;
       }
     }
 
     // And now check whether we're really busy; that might have changed with
     // the layout flush.
-    // Note, mDocumentRequest can be null if the flushing above re-entered this
-    // method.
-    if (!IsBusy() && mDocumentRequest) {
+    //
+    // Note, mDocumentRequest can be null while mDocumentOpenedButNotLoaded is
+    // false if the flushing above re-entered this method.
+    if (IsBusy() || (!mDocumentRequest && !mDocumentOpenedButNotLoaded)) {
+      return;
+    }
+
+    if (mDocumentRequest) {
       // Clear out our request info hash, now that our load really is done and
       // we don't need it anymore to CalculateMaxProgress().
       ClearInternalProgress();
 
       MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
               ("DocLoader:%p: Is now idle...\n", this));
 
       nsCOMPtr<nsIRequest> docRequest = mDocumentRequest;
@@ -674,34 +690,95 @@ void nsDocLoader::DocLoaderIsEmpty(bool 
       mLoadGroup->GetStatus(&loadGroupStatus);
 
       //
       // New code to break the circular reference between
       // the load group and the docloader...
       //
       mLoadGroup->SetDefaultLoadRequest(nullptr);
 
-      // Take a ref to our parent now so that we can call DocLoaderIsEmpty() on
-      // it even if our onload handler removes us from the docloader tree.
+      // Take a ref to our parent now so that we can call ChildDoneWithOnload()
+      // on it even if our onload handler removes us from the docloader tree.
       RefPtr<nsDocLoader> parent = mParent;
 
       // Note that if calling ChildEnteringOnload() on the parent returns false
       // then calling our onload handler is not safe.  That can only happen on
       // OOM, so that's ok.
       if (!parent || parent->ChildEnteringOnload(this)) {
         // Do nothing with our state after firing the
         // OnEndDocumentLoad(...). The document loader may be loading a *new*
         // document - if LoadDocument() was called from a handler!
         //
         doStopDocumentLoad(docRequest, loadGroupStatus);
 
         if (parent) {
           parent->ChildDoneWithOnload(this);
         }
       }
+    } else {
+      MOZ_ASSERT(mDocumentOpenedButNotLoaded);
+      mDocumentOpenedButNotLoaded = false;
+
+      // Make sure we do the ChildEnteringOnload/ChildDoneWithOnload even if we
+      // plan to skip firing our own load event, because otherwise we might
+      // never end up firing our parent's load event.
+      RefPtr<nsDocLoader> parent = mParent;
+      if (!parent || parent->ChildEnteringOnload(this)) {
+        nsresult loadGroupStatus = NS_OK;
+        mLoadGroup->GetStatus(&loadGroupStatus);
+        // Make sure we're not canceling the loadgroup.  If we are, then just
+        // like the normal navigation case we should not fire a load event.
+        if (NS_SUCCEEDED(loadGroupStatus) ||
+            loadGroupStatus == NS_ERROR_PARSED_DATA_CACHED) {
+          // Can "doc" or "window" ever come back null here?  Our state machine
+          // is complicated enough I wouldn't bet against it...
+          nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this));
+          if (doc) {
+            doc->SetReadyStateInternal(Document::READYSTATE_COMPLETE,
+                                       /* updateTimingInformation = */ false);
+
+            nsCOMPtr<nsPIDOMWindowOuter> window = doc->GetWindow();
+            if (window && !doc->SkipLoadEventAfterClose()) {
+              MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+                      ("DocLoader:%p: Firing load event for document.open\n",
+                       this));
+
+              // This is a very cut-down version of
+              // nsDocumentViewer::LoadComplete that doesn't do various things
+              // that are not relevant here because this wasn't an actual
+              // navigation.
+              WidgetEvent event(true, eLoad);
+              event.mFlags.mBubbles = false;
+              event.mFlags.mCancelable = false;
+              // Dispatching to |window|, but using |document| as the target,
+              // per spec.
+              event.mTarget = doc;
+              nsEventStatus unused = nsEventStatus_eIgnore;
+              doc->SetLoadEventFiring(true);
+              EventDispatcher::Dispatch(window, nullptr, &event, nullptr,
+                                        &unused);
+              doc->SetLoadEventFiring(false);
+
+              // Now unsuppress painting on the presshell, if we
+              // haven't done that yet.
+              nsCOMPtr<nsIPresShell> shell = doc->GetShell();
+              if (shell && !shell->IsDestroying()) {
+                shell->UnsuppressPainting();
+
+                if (!shell->IsDestroying()) {
+                  shell->LoadComplete();
+                }
+              }
+            }
+          }
+        }
+        if (parent) {
+          parent->ChildDoneWithOnload(this);
+        }
+      }
     }
   }
 }
 
 void nsDocLoader::doStartDocumentLoad(void) {
 #if defined(DEBUG)
   nsAutoCString buffer;
 
--- a/uriloader/base/nsDocLoader.h
+++ b/uriloader/base/nsDocLoader.h
@@ -116,16 +116,18 @@ class nsDocLoader : public nsIDocumentLo
    * Fired when a content blocking event occurs during the time
    * when a document is alive.  This interface should be called
    * by Gecko to notify nsIWebProgressListeners that there is a
    * new content blocking event.  Content blocking events are in
    * nsIWebProgressListeners.idl.
    */
   void OnContentBlockingEvent(nsISupports* aContext, uint32_t aEvent);
 
+  void SetDocumentOpenedButNotLoaded() { mDocumentOpenedButNotLoaded = true; }
+
  protected:
   virtual ~nsDocLoader();
 
   virtual MOZ_MUST_USE nsresult SetDocLoaderParent(nsDocLoader* aLoader);
 
   bool IsBusy();
 
   void Destroy();
@@ -291,16 +293,25 @@ class nsDocLoader : public nsIDocumentLo
 
   /* Flag to indicate whether we should consider ourselves as currently
      flushing layout for the purposes of IsBusy. For example, if Stop has
      been called then IsBusy should return false even if we are still
      flushing. */
   bool mIsFlushingLayout;
 
  private:
+  /**
+   * This flag indicates that the loader is waiting for completion of
+   * a document.open-triggered "document load".  This is set when
+   * document.open() happens and sets up a new parser and cleared out
+   * when we go to fire our load event or end up with a new document
+   * channel.
+   */
+  bool mDocumentOpenedButNotLoaded;
+
   static const PLDHashTableOps sRequestInfoHashOps;
 
   // A list of kids that are in the middle of their onload calls and will let
   // us know once they're done.  We don't want to fire onload for "normal"
   // DocLoaderIsEmpty calls (those coming from requests finishing in our
   // loadgroup) unless this is empty.
   nsCOMArray<nsIDocumentLoader> mChildrenInOnload;
 
@@ -317,13 +328,23 @@ class nsDocLoader : public nsIDocumentLo
   void RemoveRequestInfo(nsIRequest* aRequest);
   nsRequestInfo* GetRequestInfo(nsIRequest* aRequest) const;
   void ClearRequestInfoHash();
   int64_t CalculateMaxProgress();
   ///    void DumpChannelInfo(void);
 
   // used to clear our internal progress state between loads...
   void ClearInternalProgress();
+
+  /**
+   * Used to test whether we might need to fire a load event.  This
+   * can happen when we have a document load going on, or when we've
+   * had document.open() called and haven't fired the corresponding
+   * load event yet.
+   */
+  bool IsBlockingLoadEvent() const {
+    return mIsLoadingDocument || mDocumentOpenedButNotLoaded;
+  }
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID)
 
 #endif /* nsDocLoader_h__ */