Bug 1479570. Get Add a getter to get a docshell from nsIWindowlessBrowser. r=kmag
authorBoris Zbarsky <bzbarsky@mit.edu>
Fri, 03 Aug 2018 00:05:07 +0000
changeset 429992 cca51f08f1b557a68a7f5a6dcc51b8a8a9b54efb
parent 429991 d2dfd67e6fc80eeecc2b14b319043c30709041b3
child 429993 92b8a54b4ad42d1d0203d265083b691e769fad1e
push id106034
push userbtara@mozilla.com
push dateFri, 03 Aug 2018 10:28:59 +0000
treeherdermozilla-inbound@d138efcfa006 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1479570
milestone63.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 1479570. Get Add a getter to get a docshell from nsIWindowlessBrowser. r=kmag Differential Revision: https://phabricator.services.mozilla.com/D2669
browser/components/shell/HeadlessShell.jsm
devtools/shared/tests/unit/test_console_filtering.js
docshell/test/chrome/test_bug565388.xul
docshell/test/chrome/test_bug846906.xul
docshell/test/chrome/test_docRedirect.xul
docshell/test/unit/test_setUsePrivateBrowsing.js
dom/tests/browser/browser_ConsoleAPI_originAttributes.js
gfx/tests/browser/browser_windowless_troubleshoot_crash.js
js/xpconnect/tests/unit/test_nuke_sandbox_event_listeners.js
js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
js/xpconnect/tests/unit/test_xray_named_element_access.js
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/parent/ext-downloads.js
toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
toolkit/modules/HiddenFrame.jsm
widget/headless/tests/test_headless.js
xpfe/appshell/nsAppShellService.cpp
xpfe/appshell/nsIWindowlessBrowser.idl
xpfe/appshell/test/test_windowlessBrowser.xul
--- a/browser/components/shell/HeadlessShell.jsm
+++ b/browser/components/shell/HeadlessShell.jsm
@@ -43,19 +43,19 @@ function loadContentWindow(webNavigation
     progressListeners.set(progressListener, progressListener);
     webProgress.addProgressListener(progressListener,
                                     Ci.nsIWebProgress.NOTIFY_LOCATION);
   });
 }
 
 async function takeScreenshot(fullWidth, fullHeight, contentWidth, contentHeight, path, url) {
   try {
-    let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-    var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-    let contentWindow = await loadContentWindow(webNavigation, url);
+    var windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+    // nsIWindowlessBrowser inherits from nsIWebNavigation.
+    let contentWindow = await loadContentWindow(windowlessBrowser, url);
     contentWindow.resizeTo(contentWidth, contentHeight);
 
     let canvas = contentWindow.document.createElementNS("http://www.w3.org/1999/xhtml", "html:canvas");
     let context = canvas.getContext("2d");
     let width = fullWidth ? contentWindow.innerWidth + contentWindow.scrollMaxX - contentWindow.scrollMinX
                           : contentWindow.innerWidth;
     let height = fullHeight ? contentWindow.innerHeight + contentWindow.scrollMaxY - contentWindow.scrollMinY
                             : contentWindow.innerHeight;
@@ -77,18 +77,18 @@ async function takeScreenshot(fullWidth,
 
     let blob = await getBlob();
     let reader = await readBlob(blob);
     await OS.File.writeAtomic(path, new Uint8Array(reader.result), {flush: true});
     dump("Screenshot saved to: " + path + "\n");
   } catch (e) {
     dump("Failure taking screenshot: " + e + "\n");
   } finally {
-    if (webNavigation) {
-      webNavigation.close();
+    if (windowlessBrowser) {
+      windowlessBrowser.close();
     }
   }
 }
 
 let HeadlessShell = {
   async handleCmdLineArgs(cmdLine, URLlist) {
     try {
       // Don't quit even though we don't create a window
--- a/devtools/shared/tests/unit/test_console_filtering.js
+++ b/devtools/shared/tests/unit/test_console_filtering.js
@@ -49,18 +49,17 @@ function createFakeAddonWindow({addonId}
     localizeCallback() {},
   });
   policy.active = true;
 
   const baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
   const principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
   const chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-  const docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell);
+  const { docShell } = chromeWebNav;
   docShell.createAboutBlankContentViewer(principal);
   const addonWindow = docShell.contentViewer.DOMDocument.defaultView;
 
   return {addonWindow, chromeWebNav};
 }
 
 /**
  * Tests that the consoleID property of the ConsoleAPI options gets passed
--- a/docshell/test/chrome/test_bug565388.xul
+++ b/docshell/test/chrome/test_bug565388.xul
@@ -46,24 +46,22 @@ function test() {
     QueryInterface: function(iid) {
       if (iid.equals(Ci.nsIWebProgressListener) ||
           iid.equals(Ci.nsISupportsWeakReference))
         return this;
       throw Cr.NS_ERROR_NO_INTERFACE;
     }
   }
 
-  var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].
-  createInstance(Ci.nsIPrincipal);
-  var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
-  getService(Ci.nsIAppShellService).
-  createWindowlessBrowser(true);
-  var docShell = webNav.
-  QueryInterface(Ci.nsIInterfaceRequestor).
-  getInterface(Ci.nsIDocShell);
+  var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
+                          .getService(Ci.nsIPrincipal);
+  var webNav = Cc["@mozilla.org/appshell/appShellService;1"]
+                 .getService(Ci.nsIAppShellService)
+                 .createWindowlessBrowser(true);
+  var docShell = webNav.docShell;
   docShell.createAboutBlankContentViewer(systemPrincipal);
   var win = docShell.contentViewer.DOMDocument.defaultView;
 
   progressListener.add(docShell, function(){
     is(win.document.documentURI, "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window/>");
     webNav.close();
     SimpleTest.finish();
   });
--- a/docshell/test/chrome/test_bug846906.xul
+++ b/docshell/test/chrome/test_bug846906.xul
@@ -28,17 +28,17 @@ https://bugzilla.mozilla.org/show_bug.cg
      "Windowless browser should implement nsIWindowlessBrowser");
 
   var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
   ok(webNavigation, "Windowless browser should implement nsIWebNavigation");
 
   var interfaceRequestor = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor);
   ok(interfaceRequestor, "Should be able to query interface requestor interface");
 
-  var docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  var docShell = windowlessBrowser.docShell;
   ok(docShell, "Should be able to get doc shell interface");
 
   var document = webNavigation.document;
   ok(document, "Should be able to get document");
 
   var iframe = document.createElement("iframe");
   ok(iframe, "Should be able to create iframe");
 
--- a/docshell/test/chrome/test_docRedirect.xul
+++ b/docshell/test/chrome/test_docRedirect.xul
@@ -66,20 +66,19 @@ https://bugzilla.mozilla.org/show_bug.cg
           iid.equals(Ci.nsISupportsWeakReference))
         return this;
       throw Cr.NS_ERROR_NO_INTERFACE;
     }
   }
 
   var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
       getService(Ci.nsIAppShellService).createWindowlessBrowser(true);
-  let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor).
-      getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
   docShell.createAboutBlankContentViewer(
-      Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal));
+      Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal));
 
   progressListener.add(docShell, function(success) {
     webNav.close();
     SimpleTest.is(success, true, "Received document redirect event");
     SimpleTest.finish();
   });
 
   var win = docShell.contentViewer.DOMDocument.defaultView;
--- a/docshell/test/unit/test_setUsePrivateBrowsing.js
+++ b/docshell/test/unit/test_setUsePrivateBrowsing.js
@@ -1,20 +1,19 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 add_task(async function() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
 
-  let loadContext = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsILoadContext);
+  let docShell = webNav.docShell;
 
-  let docShell = webNav.getInterface(Ci.nsIDocShell);
+  let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
 
   equal(loadContext.usePrivateBrowsing, false, "Should start out in non-private mode");
 
   loadContext.usePrivateBrowsing = true;
   equal(loadContext.usePrivateBrowsing, true,
         "Should be able to change to private mode prior to a document load");
 
   loadContext.usePrivateBrowsing = false;
--- a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
+++ b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
@@ -64,18 +64,17 @@ function test()
   });
   policy.active = true;
 
   let baseURI = Services.io.newURI(url);
   let principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
 
   let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-  let interfaceRequestor = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor);
-  let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  let docShell = chromeWebNav.docShell;
   docShell.createAboutBlankContentViewer(principal);
 
   info("fake webextension docShell created");
 
   registerCleanupFunction(function() {
     policy.active = false;
     if (chromeWebNav) {
       chromeWebNav.close();
--- a/gfx/tests/browser/browser_windowless_troubleshoot_crash.js
+++ b/gfx/tests/browser/browser_windowless_troubleshoot_crash.js
@@ -1,16 +1,15 @@
 let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 add_task(async function test_windowlessBrowserTroubleshootCrash() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
 
   let onLoaded = new Promise((resolve, reject) => {
-    let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDocShell);
+    let docShell = webNav.docShell;
     let listener = {
       observe(contentWindow, topic, data) {
         let observedDocShell = contentWindow.docShell
                                             .sameTypeRootTreeItem
                                             .QueryInterface(Ci.nsIDocShell);
           if (docShell === observedDocShell) {
             Services.obs.removeObserver(listener, "content-document-global-created");
             resolve();
--- a/js/xpconnect/tests/unit/test_nuke_sandbox_event_listeners.js
+++ b/js/xpconnect/tests/unit/test_nuke_sandbox_event_listeners.js
@@ -13,18 +13,17 @@ function promiseEvent(target, event) {
 }
 
 add_task(async function() {
   let principal = Services.scriptSecurityManager
     .createCodebasePrincipalFromOrigin("http://example.com/");
 
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(principal);
 
   let window = webnav.document.defaultView;
   let sandbox = Cu.Sandbox(window, {sandboxPrototype: window});
 
   function sandboxContent() {
     window.onload = function SandboxOnLoad() {};
--- a/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
+++ b/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
@@ -9,18 +9,17 @@ function getWindowlessBrowser(url) {
   let ssm = Services.scriptSecurityManager;
 
   let uri = NetUtil.newURI(url);
 
   let principal = ssm.createCodebasePrincipal(uri, {});
 
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(principal);
 
   return webnav;
 }
 
 function StubPolicy(id) {
   return new WebExtensionPolicy({
--- a/js/xpconnect/tests/unit/test_xray_named_element_access.js
+++ b/js/xpconnect/tests/unit/test_xray_named_element_access.js
@@ -2,18 +2,17 @@
 "use strict"
 
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 add_task(async function() {
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(null);
 
   let window = webnav.document.defaultView;
   let unwrapped = Cu.waiveXrays(window);
 
   window.document.body.innerHTML = '<div id="foo"></div>';
 
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1072,24 +1072,22 @@ class HiddenXULWindow {
     // The invisible page is currently wrapped in a XUL window to fix an issue
     // with using the canvas API from a background page (See Bug 1274775).
     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
     this._windowlessBrowser = windowlessBrowser;
 
     // The windowless browser is a thin wrapper around a docShell that keeps
     // its related resources alive. It implements nsIWebNavigation and
     // forwards its methods to the underlying docShell, but cannot act as a
-    // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
+    // docShell itself.  Getting .docShell gives us the
     // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
     // access to the webNav methods that are already available on the
     // windowless browser, but contrary to appearances, they are not the same
     // object.
-    this.chromeShell = this._windowlessBrowser
-                           .QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDocShell)
+    this.chromeShell = this._windowlessBrowser.docShell
                            .QueryInterface(Ci.nsIWebNavigation);
 
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       let attrs = this.chromeShell.getOriginAttributes();
       attrs.privateBrowsingId = 1;
       this.chromeShell.setOriginAttributes(attrs);
     }
 
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -113,19 +113,18 @@ class ContentPage {
     this.browserReady = this._initBrowser();
   }
 
   async _initBrowser() {
     this.windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
 
     let system = Services.scriptSecurityManager.getSystemPrincipal();
 
-    let chromeShell = this.windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
-                                            .getInterface(Ci.nsIDocShell)
-                                            .QueryInterface(Ci.nsIWebNavigation);
+    let chromeShell = this.windowlessBrowser.docShell
+                          .QueryInterface(Ci.nsIWebNavigation);
 
     chromeShell.createAboutBlankContentViewer(system);
     chromeShell.useGlobalHistory = false;
     chromeShell.loadURI("chrome://extensions/content/dummy.xul", 0, null, null, null);
 
     await promiseObserved("chrome-document-global-created",
                           win => win.document == chromeShell.document);
 
--- a/toolkit/components/extensions/parent/ext-downloads.js
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -703,19 +703,17 @@ this.downloads = class extends Extension
               path = Services.io.newFileURI(file).spec;
             } else {
               path = OS.Path.basename(download.target.path);
               pathPrefix = "//";
             }
 
             return new Promise((resolve, reject) => {
               let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-              chromeWebNav
-                .QueryInterface(Ci.nsIInterfaceRequestor)
-                .getInterface(Ci.nsIDocShell)
+              chromeWebNav.docShell
                 .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
 
               let img = chromeWebNav.document.createElement("img");
               img.width = size;
               img.height = size;
 
               let handleLoad;
               let handleError;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -803,18 +803,17 @@ function loadImage(img, data) {
   return new Promise((resolve) => {
     img.src = data;
     img.onload = resolve;
   });
 }
 
 add_task(async function test_getFileIcon() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
-  let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
 
   let system = Services.scriptSecurityManager.getSystemPrincipal();
   docShell.createAboutBlankContentViewer(system);
 
   let img = webNav.document.createElement("img");
 
   let msg = await runInExtension("download", {url: TXT_URL});
   equal(msg.status, "success", "download() succeeded");
--- a/toolkit/modules/HiddenFrame.jsm
+++ b/toolkit/modules/HiddenFrame.jsm
@@ -99,14 +99,14 @@ HiddenFrame.prototype = {
         this._listener = null;
         this._webProgress = null;
         // Get the window reference via the document.
         this._frame = this._browser.document.ownerGlobal;
         this._deferred.resolve(this._frame);
       }
     };
     this._webProgress.addProgressListener(this._listener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-    let docShell = this._browser.getInterface(Ci.nsIDocShell);
+    let docShell = this._browser.docShell;
     docShell.createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
     docShell.useGlobalHistory = false;
     this._browser.loadURI(XUL_PAGE, 0, null, null, null);
   }
 };
--- a/widget/headless/tests/test_headless.js
+++ b/widget/headless/tests/test_headless.js
@@ -13,35 +13,32 @@ const BASE = `${ROOT}/`;
 const HEADLESS_URL = `${BASE}/headless.html`;
 const HEADLESS_BUTTON_URL = `${BASE}/headless_button.html`;
 registerCleanupFunction(() => { server.stop(() => {})});
 
 // Refrences to the progress listeners to keep them from being gc'ed
 // before they are called.
 const progressListeners = new Map();
 
-function loadContentWindow(webNavigation, uri) {
+function loadContentWindow(windowlessBrowser, uri) {
   return new Promise((resolve, reject) => {
-    webNavigation.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
-    let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
-                  .getInterface(Ci.nsIDocShell);
+    windowlessBrowser.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+    let docShell = windowlessBrowser.docShell;
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIWebProgress);
     let progressListener = {
       onLocationChange: function (progress, request, location, flags) {
         // Ignore inner-frame events
         if (progress != webProgress) {
           return;
         }
         // Ignore events that don't change the document
         if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
           return;
         }
-        let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
         let contentWindow = docShell.domWindow;
         webProgress.removeProgressListener(progressListener);
         progressListeners.delete(progressListener);
         contentWindow.addEventListener("load", (event) => {
           resolve(contentWindow);
         }, { once: true });
       },
       QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
@@ -50,18 +47,17 @@ function loadContentWindow(webNavigation
     progressListeners.set(progressListener, progressListener);
     webProgress.addProgressListener(progressListener,
                                     Ci.nsIWebProgress.NOTIFY_LOCATION);
   });
 }
 
 add_task(async function test_snapshot() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
   const contentWidth = 400;
   const contentHeight = 300;
   // Verify dimensions.
   contentWindow.resizeTo(contentWidth, contentHeight);
   equal(contentWindow.innerWidth, contentWidth);
   equal(contentWindow.innerHeight, contentHeight);
 
   // Snapshot the test page.
@@ -88,23 +84,23 @@ add_task(async function test_snapshot() 
   for (let i = 0; i < imageData.length; i += 4) {
     if (imageData[i + 2] === 255) {
       found = true;
       break;
     }
   }
   ok(found, "Found blue text on page.");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 add_task(async function test_snapshot_widget_layers() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
   const contentWidth = 1;
   const contentHeight = 2;
   // Verify dimensions.
   contentWindow.resizeTo(contentWidth, contentHeight);
   equal(contentWindow.innerWidth, contentWidth);
   equal(contentWindow.innerHeight, contentHeight);
 
   // Snapshot the test page.
@@ -120,49 +116,49 @@ add_task(async function test_snapshot_wi
     0,
     width,
     height,
     'rgb(255, 255, 255)',
     context.DRAWWINDOW_DRAW_CARET | context.DRAWWINDOW_DRAW_VIEW | context.DRAWWINDOW_USE_WIDGET_LAYERS
   );
   ok(true, "Snapshot with widget layers didn't crash.");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 // Ensure keydown events are triggered on the windowless browser.
 add_task(async function test_keydown() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
 
   let keydown = new Promise((resolve) => {
     contentWindow.addEventListener("keydown", () => {
       resolve();
     }, { once: true });
   })
 
   let tip = Cc["@mozilla.org/text-input-processor;1"]
             .createInstance(Ci.nsITextInputProcessor);
   let begun = tip.beginInputTransactionForTests(contentWindow);
   ok(begun, "nsITextInputProcessor.beginInputTransactionForTests() should succeed");
   tip.keydown(new contentWindow.KeyboardEvent("", {key: "a", code: "KeyA", keyCode: contentWindow.KeyboardEvent.DOM_VK_A}));
 
   await keydown;
   ok(true, "Send keydown didn't crash");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 // Test dragging the mouse on a button to ensure the creation of the drag
 // service doesn't crash in headless.
 add_task(async function test_mouse_drag() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_BUTTON_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_BUTTON_URL);
   contentWindow.resizeTo(400, 400);
 
   let target = contentWindow.document.getElementById('btn');
   let rect = target.getBoundingClientRect();
   let left = rect.left;
   let top = rect.top;
 
   let utils = contentWindow.windowUtils;
@@ -170,10 +166,10 @@ add_task(async function test_mouse_drag(
   utils.sendMouseEvent("mousemove", left, top, 0, 1, 0, false, 0, 0);
   // Wait for a turn of the event loop since the synthetic mouse event
   // that creates the drag service is processed during the refresh driver.
   await new Promise((r) => {executeSoon(r)});
   utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
 
   ok(true, "Send mouse event didn't crash");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
--- a/xpfe/appshell/nsAppShellService.cpp
+++ b/xpfe/appshell/nsAppShellService.cpp
@@ -447,35 +447,20 @@ public:
     mBrowser(aBrowser),
     mContainer(aContainer),
     mClosed(false)
   {
     mWebNavigation = do_QueryInterface(aBrowser);
     mInterfaceRequestor = do_QueryInterface(aBrowser);
   }
   NS_DECL_ISUPPORTS
+  NS_DECL_NSIWINDOWLESSBROWSER
   NS_FORWARD_SAFE_NSIWEBNAVIGATION(mWebNavigation)
   NS_FORWARD_SAFE_NSIINTERFACEREQUESTOR(mInterfaceRequestor)
 
-  NS_IMETHOD
-  Close() override
-  {
-    NS_ENSURE_TRUE(!mClosed, NS_ERROR_UNEXPECTED);
-    NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
-                 "WindowlessBrowser::Close called when not safe to run scripts");
-
-    mClosed = true;
-
-    mWebNavigation = nullptr;
-    mInterfaceRequestor = nullptr;
-
-    nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(mBrowser);
-    return window->Destroy();
-  }
-
 protected:
   virtual ~WindowlessBrowser()
   {
     if (mClosed) {
       return;
     }
 
     NS_WARNING("Windowless browser was not closed prior to destruction");
@@ -495,16 +480,43 @@ private:
   // we don't use the container but just hold a reference to it.
   nsCOMPtr<nsISupports> mContainer;
 
   bool mClosed;
 };
 
 NS_IMPL_ISUPPORTS(WindowlessBrowser, nsIWindowlessBrowser, nsIWebNavigation, nsIInterfaceRequestor)
 
+NS_IMETHODIMP
+WindowlessBrowser::Close()
+{
+  NS_ENSURE_TRUE(!mClosed, NS_ERROR_UNEXPECTED);
+  NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
+               "WindowlessBrowser::Close called when not safe to run scripts");
+
+  mClosed = true;
+
+  mWebNavigation = nullptr;
+  mInterfaceRequestor = nullptr;
+
+  nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(mBrowser);
+  return window->Destroy();
+}
+
+NS_IMETHODIMP
+WindowlessBrowser::GetDocShell(nsIDocShell** aDocShell)
+{
+  nsCOMPtr<nsIDocShell> docShell = do_GetInterface(mInterfaceRequestor);
+  if (!docShell) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  docShell.forget(aDocShell);
+  return NS_OK;
+}
+
 
 NS_IMETHODIMP
 nsAppShellService::CreateWindowlessBrowser(bool aIsChrome, nsIWindowlessBrowser **aResult)
 {
   /* First, we create an instance of nsWebBrowser. Instances of this class have
    * an associated doc shell, which is what we're interested in.
    */
   nsCOMPtr<nsIWebBrowser> browser = do_CreateInstance(NS_WEBBROWSER_CONTRACTID);
--- a/xpfe/appshell/nsIWindowlessBrowser.idl
+++ b/xpfe/appshell/nsIWindowlessBrowser.idl
@@ -1,16 +1,18 @@
 /* -*- Mode: IDL; tab-width: 4; 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 "nsIWebNavigation.idl"
 
+interface nsIDocShell;
+
 /**
  * This interface represents a nsIWebBrowser instance with no associated OS
  * window. Its main function is to manage the lifetimes of those windows.
  * A strong reference to this object must be held until the window is
  * ready to be destroyed.
  */
 [scriptable, uuid(abb46f48-abfc-41bf-aa9a-7feccefcf977)]
 interface nsIWindowlessBrowser : nsIWebNavigation
@@ -18,10 +20,16 @@ interface nsIWindowlessBrowser : nsIWebN
   /**
    * "Closes" the windowless browser and destroys its associated nsIWebBrowser
    * and docshell.
    *
    * This method *must* be called for every windowless browser before its last
    * reference is released.
    */
   void close();
+
+  /**
+   * Get the docshell for this browser.  This is the docshell that gets
+   * navigated when the browser's nsIWebNavigation interface is used.
+   */
+  readonly attribute nsIDocShell docShell;
 };
 
--- a/xpfe/appshell/test/test_windowlessBrowser.xul
+++ b/xpfe/appshell/test/test_windowlessBrowser.xul
@@ -26,18 +26,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 ChromeUtils.import('resource://gre/modules/Services.jsm');
 
 function testWindowlessBrowser(chromePrivileged) {
   var webNav = Services.appShell.createWindowlessBrowser(chromePrivileged);
 
   ok(webNav, "createWindowlessBrowser should return a wevNav");
 
-  let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
-  let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
 
   ok(docShell, "docShell should be defined");
   ok(docShell.contentViewer.DOMDocument.defaultView, "docShell defaultView should be defined");
 
   var win = docShell.contentViewer.DOMDocument.defaultView;
 
   ok(win.screenX == 0, "window.screenX should be 0 in a windowless browser");
   ok(win.screenY == 0, "window.screenY should be 0 in a windowless browser");