Merge inbound to mozilla-central. a=merge
authorNoemi Erli <nerli@mozilla.com>
Sun, 16 Sep 2018 12:50:28 +0300
changeset 492360 7ed950e60f3c1f8a47c117c04124d31e94a66e32
parent 492350 300d0f16cf6f770200d2e57b1f7bed48f434ab65 (diff)
parent 492359 b58b63ffcf089118ab80ba89eb873b18a755bdb5 (current diff)
child 492369 bf8ae063de6ab1353c97f53e8fc8908643d9cc03
child 492378 1e167ef58df92afd693b415023cb2aa65b92e465
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
7ed950e60f3c / 64.0a1 / 20180916100118 / files
nightly linux64
7ed950e60f3c / 64.0a1 / 20180916100118 / files
nightly mac
7ed950e60f3c / 64.0a1 / 20180916100118 / files
nightly win32
7ed950e60f3c / 64.0a1 / 20180916100118 / files
nightly win64
7ed950e60f3c / 64.0a1 / 20180916100118 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -501,23 +501,16 @@ toolbar:not(#TabsToolbar) > #personal-bo
 }
 
 %ifdef XP_MACOSX
 #main-window[inFullscreen="true"] {
   padding-top: 0; /* override drawintitlebar="true" */
 }
 %endif
 
-:root[lwthemefooter=true] #browser-bottombox:-moz-lwtheme {
-  background-repeat: no-repeat;
-  background-position: bottom left;
-  background-color: var(--lwt-accent-color);
-  background-image: var(--lwt-footer-image);
-}
-
 /* Hide menu elements intended for keyboard access support */
 #main-menubar[openedwithkey=false] .show-only-for-keyboard {
   display: none;
 }
 
 /* ::::: location bar & search bar ::::: */
 
 /* url bar min-width is defined further down, together with the maximum size
--- a/layout/generic/nsFlexContainerFrame.cpp
+++ b/layout/generic/nsFlexContainerFrame.cpp
@@ -4161,16 +4161,17 @@ nsFlexContainerFrame::ComputeCrossSize(c
       return aAvailableBSizeForContent;
     }
     return std::min(effectiveComputedBSize, aSumLineCrossSizes);
   }
 
   // Row-oriented case, with size-containment:
   // Behave as if we had no content and just use our MinBSize.
   if (aReflowInput.mStyleDisplay->IsContainSize()) {
+    *aIsDefinite = true;
     return aReflowInput.ComputedMinBSize();
   }
 
   // Row-oriented case (cross axis is block axis), with auto BSize:
   // Shrink-wrap our line(s), subject to our min-size / max-size
   // constraints in that (block) axis.
   // XXXdholbert Handle constrained-aAvailableBSizeForContent case here.
   *aIsDefinite = false;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -467,17 +467,20 @@ class Script {
         // the stylesheets on first load. We should fix this up if it does becomes
         // a problem.
         if (this.css.length > 0) {
           context.contentWindow.document.blockParsing(cssPromise, {blockScriptCreated: false});
         }
       }
     }
 
-    let scripts = await this.awaitCompiledScripts(context);
+    let scripts = this.getCompiledScripts(context);
+    if (scripts instanceof Promise) {
+      scripts = await scripts;
+    }
 
     let result;
 
     const {extension} = context;
 
     // The evaluations below may throw, in which case the promise will be
     // automatically rejected.
     ExtensionTelemetry.contentScriptInjection.stopwatchStart(extension, context);
@@ -492,34 +495,45 @@ class Script {
     } finally {
       ExtensionTelemetry.contentScriptInjection.stopwatchFinish(extension, context);
     }
 
     await cssPromise;
     return result;
   }
 
-  async awaitCompiledScripts(context) {
+  /**
+   *  Get the compiled scripts (if they are already precompiled and cached) or a promise which resolves
+   *  to the precompiled scripts (once they have been compiled and cached).
+   *
+   * @param {BaseContext} context
+   *        The document to block the parsing on, if the scripts are not yet precompiled and cached.
+   *
+   * @returns {Array<PreloadedScript> | Promise<Array<PreloadedScript>>}
+   *          Returns an array of preloaded scripts if they are already available, or a promise which
+   *          resolves to the array of the preloaded scripts once they are precompiled and cached.
+   */
+  getCompiledScripts(context) {
     let scriptPromises = this.compileScripts();
     let scripts = scriptPromises.map(promise => promise.script);
 
     // If not all scripts are already available in the cache, block
     // parsing and wait all promises to resolve.
     if (!scripts.every(script => script)) {
       let promise = Promise.all(scriptPromises);
 
       // If we're supposed to inject at the start of the document load,
       // and we haven't already missed that point, block further parsing
       // until the scripts have been loaded.
-      let {document} = context.contentWindow;
+      const {document} = context.contentWindow;
       if (this.runAt === "document_start" && document.readyState !== "complete") {
         document.blockParsing(promise, {blockScriptCreated: false});
       }
 
-      scripts = await promise;
+      return promise;
     }
 
     return scripts;
   }
 }
 
 // Represents a user script.
 class UserScript extends Script {
@@ -533,51 +547,40 @@ class UserScript extends Script {
     super(extension, matcher);
 
     // This is an opaque object that the extension provides, it is associated to
     // the particular userScript and it is passed as a parameter to the custom
     // userScripts APIs defined by the extension.
     this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
     this.apiScriptURL = extension.manifest.user_scripts && extension.manifest.user_scripts.api_script;
 
-    this.promiseAPIScript = null;
-    this.scriptPromises = null;
+    // Add the apiScript to the js scripts to compile.
+    if (this.apiScriptURL) {
+      this.js = [this.apiScriptURL].concat(this.js);
+    }
 
     // WeakMap<ContentScriptContextChild, Sandbox>
     this.sandboxes = new DefaultWeakMap((context) => {
       return this.createSandbox(context);
     });
   }
 
-  compileScripts() {
-    if (this.apiScriptURL && !this.promiseAPIScript) {
-      this.promiseAPIScript = this.scriptCache.get(this.apiScriptURL);
-    }
-
-    if (!this.scriptPromises) {
-      this.scriptPromises = this.js.map(url => this.scriptCache.get(url));
-    }
-
-    if (this.promiseAPIScript) {
-      return [this.promiseAPIScript, ...this.scriptPromises];
-    }
-
-    return this.scriptPromises;
-  }
-
   async inject(context) {
     const {extension} = context;
 
     DocumentManager.lazyInit();
 
-    let scripts = await this.awaitCompiledScripts(context);
+    let scripts = this.getCompiledScripts(context);
+    if (scripts instanceof Promise) {
+      scripts = await scripts;
+    }
 
     let apiScript, sandboxScripts;
 
-    if (this.promiseAPIScript) {
+    if (this.apiScriptURL) {
       [apiScript, ...sandboxScripts] = scripts;
     } else {
       sandboxScripts = scripts;
     }
 
     // Load and execute the API script once per context.
     if (apiScript) {
       context.executeAPIScript(apiScript);
--- a/toolkit/components/extensions/child/ext-userScripts.js
+++ b/toolkit/components/extensions/child/ext-userScripts.js
@@ -2,16 +2,17 @@
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.importGlobalProperties(["crypto", "TextEncoder"]);
 
 var {
   DefaultMap,
   ExtensionError,
+  getUniqueId,
 } = ExtensionUtils;
 
 /**
  * Represents a registered userScript in the child extension process.
  *
  * @param {ExtensionPageContextChild} context
  *        The extension context which has registered the user script.
  * @param {string} scriptId
@@ -60,68 +61,71 @@ this.userScripts = class extends Extensi
     const blobURLsByHash = new Map();
 
     // Keep track of the userScript that are sharing the same blob urls,
     // so that we can revoke any blob url that is not used by a registered
     // userScripts:
     //   Map<blobURL, Set<scriptId>>
     const userScriptsByBlobURL = new DefaultMap(() => new Set());
 
-    function trackBlobURLs(scriptId, options) {
-      for (let url of options.js) {
-        if (userScriptsByBlobURL.has(url)) {
-          userScriptsByBlobURL.get(url).add(scriptId);
-        }
-      }
-    }
+    function revokeBlobURLs(scriptId, options) {
+      let revokedUrls = new Set();
 
-    function revokeBlobURLs(scriptId, options) {
       for (let url of options.js) {
         if (userScriptsByBlobURL.has(url)) {
           let scriptIds = userScriptsByBlobURL.get(url);
           scriptIds.delete(scriptId);
+
           if (scriptIds.size === 0) {
+            revokedUrls.add(url);
             userScriptsByBlobURL.delete(url);
             context.cloneScope.URL.revokeObjectURL(url);
           }
         }
       }
+
+      // Remove all the removed urls from the map of known computed hashes.
+      for (let [hash, url] of blobURLsByHash) {
+        if (revokedUrls.has(url)) {
+          blobURLsByHash.delete(hash);
+        }
+      }
     }
 
     // Convert a script code string into a blob URL (and use a cached one
     // if the script hash is already associated to a blob URL).
-    const getBlobURL = async (text) => {
+    const getBlobURL = async (text, scriptId) => {
       // Compute the hash of the js code string and reuse the blob url if we already have
       // for the same hash.
       const buffer = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(text));
       const hash = String.fromCharCode(...new Uint16Array(buffer));
 
       let blobURL = blobURLsByHash.get(hash);
 
       if (blobURL) {
+        userScriptsByBlobURL.get(blobURL).add(scriptId);
         return blobURL;
       }
 
       const blob = new context.cloneScope.Blob([text], {type: "text/javascript"});
       blobURL = context.cloneScope.URL.createObjectURL(blob);
 
       // Start to track this blob URL.
-      userScriptsByBlobURL.get(blobURL);
+      userScriptsByBlobURL.get(blobURL).add(scriptId);
 
       blobURLsByHash.set(hash, blobURL);
 
       return blobURL;
     };
 
     function convertToAPIObject(scriptId, options) {
       const registeredScript = new UserScriptChild({
         context, scriptId,
         onScriptUnregister: () => revokeBlobURLs(scriptId, options),
       });
-      trackBlobURLs(scriptId, options);
 
       const scriptAPI = Cu.cloneInto(registeredScript.api(), context.cloneScope,
                                      {cloneFunctions: true});
       return scriptAPI;
     }
 
     // Revoke all the created blob urls once the context is destroyed.
     context.callOnClose({
@@ -134,23 +138,24 @@ this.userScripts = class extends Extensi
           context.cloneScope.URL.revokeObjectURL(blobURL);
         }
       },
     });
 
     return {
       userScripts: {
         register(options) {
+          let scriptId = getUniqueId();
           return context.cloneScope.Promise.resolve().then(async () => {
+            options.scriptId = scriptId;
             options.js = await Promise.all(options.js.map(js => {
-              return js.file || getBlobURL(js.code);
+              return js.file || getBlobURL(js.code, scriptId);
             }));
 
-            const scriptId = await context.childManager.callParentAsyncFunction(
-              "userScripts.register", [options]);
+            await context.childManager.callParentAsyncFunction("userScripts.register", [options]);
 
             return convertToAPIObject(scriptId, options);
           });
         },
         setScriptAPIs(exportedAPIMethods) {
           context.setUserScriptAPIs(exportedAPIMethods);
         },
       },
--- a/toolkit/components/extensions/parent/ext-userScripts.js
+++ b/toolkit/components/extensions/parent/ext-userScripts.js
@@ -1,30 +1,29 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   ExtensionError,
-  getUniqueId,
 } = ExtensionUtils;
 
 /**
  * Represents (in the main browser process) a user script.
  *
  * @param {UserScriptOptions} details
  *        The options object related to the user script
  *        (which has the properties described in the user_scripts.json
  *        JSON API schema file).
  */
 class UserScriptParent {
   constructor(details) {
-    this.scriptId = getUniqueId();
+    this.scriptId = details.scriptId;
     this.options = this._convertOptions(details);
   }
 
   destroy() {
     if (this.destroyed) {
       throw new Error("Unable to destroy UserScriptParent twice");
     }
 
@@ -64,27 +63,33 @@ this.userScripts = class extends Extensi
   }
 
   getAPI(context) {
     const {extension} = context;
 
     // Set of the scriptIds registered from this context.
     const registeredScriptIds = new Set();
 
-    function unregisterContentScripts(scriptIds) {
-      for (let scriptId of registeredScriptIds) {
+    const unregisterContentScripts = (scriptIds) => {
+      if (scriptIds.length === 0) {
+        return Promise.resolve();
+      }
+
+      for (let scriptId of scriptIds) {
+        registeredScriptIds.delete(scriptId);
         extension.registeredContentScripts.delete(scriptId);
         this.userScriptsMap.delete(scriptId);
       }
+      extension.updateContentScripts();
 
       return context.extension.broadcast("Extension:UnregisterContentScripts", {
         id: context.extension.id,
         scriptIds,
       });
-    }
+    };
 
     // Unregister all the scriptId related to a context when it is closed,
     // and revoke all the created blob urls once the context is destroyed.
     context.callOnClose({
       close() {
         unregisterContentScripts(Array.from(registeredScriptIds));
       },
     });
@@ -107,16 +112,17 @@ this.userScripts = class extends Extensi
 
           await extension.broadcast("Extension:RegisterContentScript", {
             id: extension.id,
             options: scriptOptions,
             scriptId,
           });
 
           extension.registeredContentScripts.set(scriptId, scriptOptions);
+          extension.updateContentScripts();
 
           return scriptId;
         },
 
         // This method is not available to the extension code, the extension code
         // doesn't have access to the internally used scriptId, on the contrary
         // the extension code will call script.unregister on the script API object
         // that is resolved from the register API method returned promise.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
@@ -153,26 +153,84 @@ add_task(async function test_contentscri
     },
   });
 
   await extension.startup();
 
   let url = `${BASE_URL}/file_document_open.html`;
   let contentPage = await ExtensionTestUtils.loadContentPage(url);
 
-  Assert.deepEqual(await extension.awaitMessage("content-script"),
-                   [url, true]);
+  let [pageURL, pageIsTop] = await extension.awaitMessage("content-script");
+
+  // Sometimes we get a content script load for the initial about:blank
+  // top level frame here, sometimes we don't. Either way is fine, as long as we
+  // don't get two loads into the same document.open() document.
+  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. Either way is fine, as long as we
-  // don't get two loads into the same document.open() document.
+  // 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
+// we do for a non cached script).
+add_task(async function test_cached_contentscript_on_document_start() {
+  let extension =  ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [
+        {
+          "matches": ["http://localhost/*/file_document_open.html"],
+          "js": ["content_script.js"],
+          "run_at": "document_start",
+        },
+      ],
+    },
+
+    files: {
+      "content_script.js": `
+        browser.test.sendMessage("content-script-loaded", {
+          url: window.location.href,
+          documentReadyState: document.readyState,
+        });
+      `,
+    },
+  });
+
+  await extension.startup();
+
+  let url = `${BASE_URL}/file_document_open.html`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+  let msg = await extension.awaitMessage("content-script-loaded");
+  Assert.deepEqual(msg, {
+    url,
+    documentReadyState: "loading",
+  }, "Got the expected url and document.readyState from a non cached script");
+
+  // Reload the page and check that the cached content script is still able to
+  // run on document_start.
+  await contentPage.loadURL(url);
+
+  let msgFromCached = await extension.awaitMessage("content-script-loaded");
+  Assert.deepEqual(msgFromCached, {
+    url,
+    documentReadyState: "loading",
+  }, "Got the expected url and document.readyState from a cached script");
+
+  await extension.unload();
+
+  await contentPage.close();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
@@ -106,62 +106,82 @@ add_task(async function test_userScripts
 // Test that userScripts sandboxes:
 // - can be registered/unregistered from an extension page
 // - have no WebExtensions APIs available
 // - are able to access the target window and document
 add_task(async function test_userScripts_no_webext_apis() {
   async function background() {
     const matches = ["http://localhost/*/file_sample.html"];
 
-    const script = await browser.userScripts.register({
-      js: [{
+    const sharedCode = {code: "console.log(\"js code shared by multiple userScripts\");"};
+
+    let script = await browser.userScripts.register({
+      js: [sharedCode, {
         code: `
-          const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
-          document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
+          window.addEventListener("load", () => {
+            const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
+            document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
+          }, {once: true});
         `,
       }],
-      runAt: "document_end",
+      runAt: "document_start",
+      matches,
+      scriptMetadata: {
+        name: "test-user-script",
+        arrayProperty: ["el1"],
+        objectProperty: {nestedProp: "nestedValue"},
+        nullProperty: null,
+      },
+    });
+
+    // Unregister and then register the same js code again, to verify that the last registered
+    // userScript doesn't get assigned a revoked blob url (otherwise Extensioncontent.jsm
+    // ScriptCache raises an error because it fails to compile the revoked blob url and the user
+    // script will never be loaded).
+    script.unregister();
+    script = await browser.userScripts.register({
+      js: [sharedCode, {
+        code: `
+          window.addEventListener("load", () => {
+            const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
+            document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
+          }, {once: true});
+        `,
+      }],
+      runAt: "document_start",
       matches,
       scriptMetadata: {
         name: "test-user-script",
         arrayProperty: ["el1"],
         objectProperty: {nestedProp: "nestedValue"},
         nullProperty: null,
       },
     });
 
     const scriptToRemove = await browser.userScripts.register({
-      js: [{
-        code: 'document.body.innerHTML = "unexpected unregistered userScript loaded";',
+      js: [sharedCode, {
+        code: `
+          window.addEventListener("load", () => {
+            document.body.innerHTML = "unexpected unregistered userScript loaded";
+          }, {once: true});
+        `,
       }],
-      runAt: "document_end",
+      runAt: "document_start",
       matches,
       scriptMetadata: {
         name: "user-script-to-remove",
       },
     });
 
     browser.test.assertTrue("unregister" in script,
                             "Got an unregister method on the userScript API object");
 
     // Remove the last registered user script.
     await scriptToRemove.unregister();
 
-    await browser.contentScripts.register({
-      js: [{
-        code: `
-          browser.test.sendMessage("page-loaded", {
-            textContent: document.body.textContent,
-            url: window.location.href,
-          }); true;
-        `,
-      }],
-      matches,
-    });
-
     browser.test.sendMessage("background-ready");
   }
 
   let extensionData = {
     manifest: {
       permissions: [
         "http://localhost/*/file_sample.html",
       ],
@@ -175,36 +195,49 @@ add_task(async function test_userScripts
 
   await extension.awaitMessage("background-ready");
 
   // Test in an existing process (where the registered userScripts has been received from the
   // Extension:RegisterContentScript message sent to all the processes).
   info("Test content script loaded in a process created before any registered userScript");
   let url = `${BASE_URL}/file_sample.html#remote-false`;
   let contentPage = await ExtensionTestUtils.loadContentPage(url, {remote: false});
-  const reply = await extension.awaitMessage("page-loaded");
-  Assert.deepEqual(reply, {
+  let result = await contentPage.spawn(undefined, async () => {
+    return {
+      textContent: this.content.document.body.textContent,
+      url: this.content.location.href,
+      readyState: this.content.document.readyState,
+    };
+  });
+  Assert.deepEqual(result, {
     textContent: "userScript loaded - undefined",
     url,
+    readyState: "complete",
   }, "The userScript executed on the expected url and no access to the WebExtensions APIs");
   await contentPage.close();
 
   // Test in a new process (where the registered userScripts has to be retrieved from the extension
   // representation from the shared memory data).
   // NOTE: this part is currently skipped on Android, where e10s content is not yet supported and
   // the xpcshell test crash when we create contentPage2 with `remote = true`.
   if (ExtensionTestUtils.remoteContentScripts) {
     info("Test content script loaded in a process created after the userScript has been registered");
     let url2 = `${BASE_URL}/file_sample.html#remote-true`;
     let contentPage2 = await ExtensionTestUtils.loadContentPage(url2, {remote: true});
-    // Load an url that matches and check that the userScripts has been loaded.
-    const reply2 = await extension.awaitMessage("page-loaded");
-    Assert.deepEqual(reply2, {
+    let result2 = await contentPage2.spawn(undefined, async () => {
+      return {
+        textContent: this.content.document.body.textContent,
+        url: this.content.location.href,
+        readyState: this.content.document.readyState,
+      };
+    });
+    Assert.deepEqual(result2, {
       textContent: "userScript loaded - undefined",
       url: url2,
+      readyState: "complete",
     }, "The userScript executed on the expected url and no access to the WebExtensions APIs");
     await contentPage2.close();
   }
 
   await extension.unload();
 });
 
 add_task(async function test_userScripts_exported_APIs() {
@@ -351,8 +384,82 @@ add_task(async function test_userScripts
     asyncAPIResult: "resolved_value",
     expectedError: "Only serializable parameters are supported",
   }, "Got the expected userScript API results");
 
   await extension.unload();
 
   await contentPage.close();
 });
+
+// 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
+// we do for a non cached script).
+add_task(async function test_cached_userScript_on_document_start() {
+  function apiScript() {
+    browser.userScripts.setScriptAPIs({
+      sendTestMessage([name, params]) {
+        return browser.test.sendMessage(name, params);
+      },
+    });
+  }
+
+  async function background() {
+    function userScript() {
+      this.sendTestMessage("user-script-loaded", {
+        url: window.location.href,
+        documentReadyState: document.readyState,
+      });
+    }
+
+    await browser.userScripts.register({
+      js: [{
+        code: `(${userScript})();`,
+      }],
+      runAt: "document_start",
+      matches: [
+        "http://localhost/*/file_sample.html",
+      ],
+    });
+
+    browser.test.sendMessage("user-script-registered");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "http://localhost/*/file_sample.html",
+      ],
+      user_scripts: {
+        api_script: "api-script.js",
+      },
+    },
+    background,
+    files: {
+      "api-script.js": apiScript,
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitMessage("user-script-registered");
+
+  let url = `${BASE_URL}/file_sample.html`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+  let msg = await extension.awaitMessage("user-script-loaded");
+  Assert.deepEqual(msg, {
+    url,
+    documentReadyState: "loading",
+  }, "Got the expected url and document.readyState from a non cached user script");
+
+  // Reload the page and check that the cached content script is still able to
+  // run on document_start.
+  await contentPage.loadURL(url);
+
+  let msgFromCached = await extension.awaitMessage("user-script-loaded");
+  Assert.deepEqual(msgFromCached, {
+    url,
+    documentReadyState: "loading",
+  }, "Got the expected url and document.readyState from a cached user script");
+
+  await contentPage.close();
+  await extension.unload();
+});