Bug 1332273 - Implement browser.contentScriptOptions API. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 03 Apr 2017 23:27:52 +0200
changeset 556125 04738f940b5e166191262aec2611a8fc92d7d65b
parent 554958 aaa0cd3bd620daf6be29c72625f6e63fd0bc1d46
child 622795 0d941cb4fdfc4b05cd92173a0246ffab6365f2e5
push id52448
push userluca.greco@alcacoop.it
push dateWed, 05 Apr 2017 11:59:59 +0000
bugs1332273
milestone55.0a1
Bug 1332273 - Implement browser.contentScriptOptions API. MozReview-Commit-ID: EVuebKm9stq
browser/components/extensions/ext-tabs.js
browser/components/extensions/test/mochitest/test_ext_all_apis.html
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-c-contentScriptOptions.js
toolkit/components/extensions/ext-c-runtime.js
toolkit/components/extensions/ext-c-toolkit.js
toolkit/components/extensions/ext-contentScriptOptions.js
toolkit/components/extensions/ext-toolkit.js
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/content_script_options.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -510,16 +510,20 @@ this.tabs = class extends ExtensionAPI {
         },
 
         async removeCSS(tabId, details) {
           let tab = await promiseTabWhenReady(tabId);
 
           return tab.removeCSS(context, details);
         },
 
+        async setContentScriptOptions(contentScriptOptions) {
+          await context.extension.broadcastContentScriptOptions(contentScriptOptions);
+        },
+
         async move(tabIds, moveProperties) {
           let index = moveProperties.index;
           let tabsMoved = [];
           if (!Array.isArray(tabIds)) {
             tabIds = [tabIds];
           }
 
           let destinationWindow = null;
--- a/browser/components/extensions/test/mochitest/test_ext_all_apis.html
+++ b/browser/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -44,16 +44,17 @@ let expectedBackgroundApisTargetSpecific
   "tabs.onReplaced",
   "tabs.onUpdated",
   "tabs.onZoomChange",
   "tabs.query",
   "tabs.reload",
   "tabs.remove",
   "tabs.removeCSS",
   "tabs.sendMessage",
+  "tabs.setContentScriptOptions",
   "tabs.setZoom",
   "tabs.setZoomSettings",
   "tabs.update",
   "windows.CreateType",
   "windows.WINDOW_ID_CURRENT",
   "windows.WINDOW_ID_NONE",
   "windows.WindowState",
   "windows.WindowType",
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -888,16 +888,22 @@ this.Extension = class extends Extension
     let serial = this.serialize();
     data["Extension:Extensions"].push(serial);
 
     return this.broadcast("Extension:Startup", serial).then(() => {
       return Promise.all(promises);
     });
   }
 
+  broadcastContentScriptOptions(action, propertyName, propertyValue) {
+    return this.broadcast("Extension:SetContentScriptOptions", {
+      id: this.id, action, propertyName, propertyValue,
+    });
+  }
+
   callOnClose(obj) {
     this.onShutdown.add(obj);
   }
 
   forgetOnClose(obj) {
     this.onShutdown.delete(obj);
   }
 
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -100,16 +100,38 @@ var apiManager = new class extends Schem
       this.initialized = true;
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
         this.loadScript(value);
       }
     }
   }
 }();
 
+function setProcessContentScriptOptions(extensionId, contentScriptOptions) {
+  // Init the contentScriptOptions map in the initialProcessData object.
+  if (!Services.cpmm.initialProcessData["Extension:ContentScriptOptions"]) {
+    Services.cpmm.initialProcessData["Extension:ContentScriptOptions"] = {};
+  }
+
+  let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"];
+  optionsMap[extensionId] = contentScriptOptions;
+}
+
+function getProcessContentScriptOptions(extensionId) {
+  let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"] || {};
+  return optionsMap[extensionId];
+}
+
+function clearProcessContentScriptOptions(extensionId) {
+  let optionsMap = Services.cpmm.initialProcessData["Extension:ContentScriptOptions"];
+  if (optionsMap) {
+    delete optionsMap[extensionId];
+  }
+}
+
 const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
 const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
 
 const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
 
 const scriptCaches = new WeakSet();
 const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
 
@@ -766,16 +788,20 @@ DocumentManager = {
       }
     },
     "memory-pressure"(subject, topic, data) {
       let timeout = data === "heap-minimize" ? 0 : undefined;
 
       for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
         cache.clear(timeout);
       }
+
+      // TODO(rpl): clear the cached process contentScriptOptions object on memory-pressure?
+      // all the extension will get an undefined browser.runtime.contentScriptOptions
+      // object on the next load of the extensions content scripts.
     },
   },
 
   observe(subject, topic, data) {
     this.observers[topic].call(this, subject, topic, data);
   },
 
   handleEvent(event) {
@@ -889,16 +915,17 @@ DocumentManager = {
 
   startupExtension(extensionId) {
     if (this.extensionCount == 0) {
       this.init();
     }
     this.extensionCount++;
 
     let extension = ExtensionManager.get(extensionId);
+
     for (let global of ExtensionContent.globals.keys()) {
       // Note that we miss windows in the bfcache here. In theory we
       // could execute content scripts on a pageshow event for that
       // window, but that seems extreme.
       for (let window of this.enumerateWindows(global.docShell)) {
         for (let script of extension.scripts) {
           if (script.matches(window)) {
             let context = this.getContentScriptContext(extension, window);
@@ -1067,16 +1094,20 @@ class BrowserExtensionContent extends Ev
 
   hasPermission(perm) {
     let match = /^manifest:(.*)/.exec(perm);
     if (match) {
       return this.manifest[match[1]] != null;
     }
     return this.permissions.has(perm);
   }
+
+  get contentScriptOptions() {
+    return getProcessContentScriptOptions(this.id);
+  }
 }
 
 defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
   return new ScriptCache({hasReturnValue: false});
 });
 
 defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
   return new ScriptCache({hasReturnValue: true});
@@ -1096,32 +1127,34 @@ ExtensionManager = {
 
   init() {
     Schemas.init();
     ExtensionChild.initOnce();
 
     Services.cpmm.addMessageListener("Extension:Startup", this);
     Services.cpmm.addMessageListener("Extension:Shutdown", this);
     Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+    Services.cpmm.addMessageListener("Extension:SetContentScriptOptions", this);
 
     if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
       let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
       for (let data of extensions) {
         this.extensions.set(data.id, new BrowserExtensionContent(data));
         DocumentManager.startupExtension(data.id);
       }
     }
   },
 
   get(extensionId) {
     return this.extensions.get(extensionId);
   },
 
   receiveMessage({name, data}) {
     let extension;
+
     switch (name) {
       case "Extension:Startup": {
         extension = new BrowserExtensionContent(data);
 
         this.extensions.set(data.id, extension);
 
         DocumentManager.startupExtension(data.id);
 
@@ -1131,24 +1164,41 @@ ExtensionManager = {
 
       case "Extension:Shutdown": {
         extension = this.extensions.get(data.id);
         extension.shutdown();
 
         DocumentManager.shutdownExtension(data.id);
 
         this.extensions.delete(data.id);
+        clearProcessContentScriptOptions(data.id);
+
         break;
       }
 
       case "Extension:FlushJarCache": {
         flushJarCache(data.path);
         Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
         break;
       }
+
+      case "Extension:SetContentScriptOptions": {
+        const options = getProcessContentScriptOptions(data.id) || {};
+
+        if (data.action === "set") {
+          options[data.propertyName] = data.propertyValue;
+          setProcessContentScriptOptions(data.id, options);
+        } else if (data.action === "clear") {
+          clearProcessContentScriptOptions(data.id);
+        } else if (data.action === "unset") {
+          delete options[data.propertyName];
+        }
+
+        break;
+      }
     }
   },
 };
 
 class ExtensionGlobal {
   constructor(global) {
     this.global = global;
 
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -52,16 +52,17 @@ function readJSON(url) {
         // strip off for this to be valid JSON. As a hack, we just
         // look for the first '[' character, which signals the start
         // of the JSON content.
         let index = text.indexOf("[");
         text = text.slice(index);
 
         resolve(JSON.parse(text));
       } catch (e) {
+        Cu.reportError(new Error(`Error while loading '${url}' (${e.name})`));
         reject(e);
       }
     });
   });
 }
 
 /**
  * Defines a lazy getter for the given property on the given object. Any
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-contentScriptOptions.js
@@ -0,0 +1,34 @@
+"use strict";
+
+this.contentScriptOptions = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+
+    return {
+      contentScriptOptions: {
+        getProperty(propertyName) {
+          if (context.envType !== "content_child") {
+            throw new Error('This method is only supported in content scripts');
+          }
+
+          const options = extension.contentScriptOptions || {};
+          return Cu.cloneInto(options[propertyName], context.cloneScope);
+        },
+        getPropertyNames() {
+          if (context.envType !== "content_child") {
+            throw new Error('This method is only supported in content scripts');
+          }
+          const options = extension.contentScriptOptions || {};
+          return Cu.cloneInto(Object.keys(options), context.cloneScope);
+        },
+        getUsageInfo() {
+          if (context.envType !== "addon_child") {
+            throw new Error('This method is not support in content scripts and devtools pages');
+          }
+          return context.childManager
+                        .callParentAsyncFunction("contentScriptOptions.getUsageInfo", []);
+        }
+      },
+    };
+  }
+};
--- a/toolkit/components/extensions/ext-c-runtime.js
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -92,12 +92,16 @@ this.runtime = class extends ExtensionAP
           return Cu.cloneInto(extension.manifest, context.cloneScope);
         },
 
         id: extension.id,
 
         getURL: function(url) {
           return extension.baseURI.resolve(url);
         },
+
+        get contentScriptOptions() {
+          return Cu.cloneInto(extension.contentScriptOptions, context.cloneScope);
+        },
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-c-toolkit.js
+++ b/toolkit/components/extensions/ext-c-toolkit.js
@@ -27,16 +27,23 @@ extensions.registerModules({
     url: "chrome://extensions/content/ext-c-backgroundPage.js",
     scopes: ["addon_child"],
     manifest: ["background"],
     paths: [
       ["extension", "getBackgroundPage"],
       ["runtime", "getBackgroundPage"],
     ],
   },
+  contentScriptOptions: {
+    url: "chrome://extensions/content/ext-c-contentScriptOptions.js",
+    scopes: ["addon_child", "content_child"],
+    paths: [
+      ["contentScriptOptions"],
+    ],
+  },
   extension: {
     url: "chrome://extensions/content/ext-c-extension.js",
     scopes: ["addon_child", "content_child", "devtools_child", "proxy_script"],
     paths: [
       ["extension"],
     ],
   },
   i18n: {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-contentScriptOptions.js
@@ -0,0 +1,57 @@
+"use strict";
+
+// TODO(rpl): set reasonable limits on the maximum contentScriptOptions size per process,
+// and/or move these values into preferences.
+const MAX_CONTENT_SCRIPT_OPTIONS_LENGTH = 1024;
+const MAX_STRING_PROPERTY_LENGTH = 1024;
+
+// Track the usage of contentScriptOptions for the running extension.
+const contentScriptOptionsUsage = new WeakMap();
+
+this.contentScriptOptions = class extends ExtensionAPI {
+  getAPI(context) {
+    let {extension} = context;
+
+    let usageInfo;
+
+    if (!contentScriptOptionsUsage.has(extension)) {
+      usageInfo = {propertyNames: new Set()};
+      contentScriptOptionsUsage.set(extension, usageInfo);
+    } else {
+      usageInfo = contentScriptOptionsUsage.get(extension);
+    }
+
+    return {
+      contentScriptOptions: {
+        setProperty(propertyName, propertyValue) {
+          if (!usageInfo.propertyNames.has(propertyName) &&
+              usageInfo.propertyNames.size + 1 > MAX_CONTENT_SCRIPT_OPTIONS_LENGTH) {
+            throw new Error(`No more than ${MAX_CONTENT_SCRIPT_OPTIONS_LENGTH}` +
+                            " content script options are allowed to be set");
+          }
+
+          if (typeof propertyValue === "string" &&
+              propertyValue.length > MAX_STRING_PROPERTY_LENGTH) {
+            throw new Error(`No strings larger than ${MAX_STRING_PROPERTY_LENGTH}` +
+                            " are allowed as content script options property values");
+          }
+          usageInfo.propertyNames.add(propertyName);
+          extension.broadcastContentScriptOptions("set", propertyName, propertyValue);
+        },
+        unsetProperty(propertyName) {
+          usageInfo.propertyNames.delete(propertyName);
+          extension.broadcastContentScriptOptions("unset", propertyName);
+        },
+        clear() {
+          usageInfo.propertyNames.clear();
+          extension.broadcastContentScriptOptions("clear");
+        },
+        getUsageInfo() {
+          return Promise.resolve({
+            propertyNames: Array.from(usageInfo.propertyNames.values()),
+          });
+        }
+      },
+    };
+  }
+};
--- a/toolkit/components/extensions/ext-toolkit.js
+++ b/toolkit/components/extensions/ext-toolkit.js
@@ -69,16 +69,24 @@ extensions.registerModules({
       ["alarms"],
     ],
   },
   backgroundPage: {
     url: "chrome://extensions/content/ext-backgroundPage.js",
     scopes: ["addon_parent"],
     manifest: ["background"],
   },
+  contentScriptOptions: {
+    url: "chrome://extensions/content/ext-contentScriptOptions.js",
+    schema: "chrome://extensions/content/schemas/content_script_options.json",
+    scopes: ["addon_parent"],
+    paths: [
+      ["contentScriptOptions"],
+    ],
+  },
   contextualIdentities: {
     url: "chrome://extensions/content/ext-contextualIdentities.js",
     schema: "chrome://extensions/content/schemas/contextual_identities.json",
     scopes: ["addon_parent"],
     paths: [
       ["contextualIdentities"],
     ],
   },
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -2,16 +2,17 @@
 # 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/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/ext-alarms.js
     content/extensions/ext-backgroundPage.js
     content/extensions/ext-browser-content.js
+    content/extensions/ext-contentScriptOptions.js
     content/extensions/ext-contextualIdentities.js
     content/extensions/ext-cookies.js
     content/extensions/ext-downloads.js
     content/extensions/ext-extension.js
     content/extensions/ext-geolocation.js
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-management.js
@@ -25,16 +26,17 @@ toolkit.jar:
     content/extensions/ext-theme.js
     content/extensions/ext-toolkit.js
     content/extensions/ext-topSites.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
     # Below is a separate group using the naming convention ext-c-*.js that run
     # in the child process.
     content/extensions/ext-c-backgroundPage.js
+    content/extensions/ext-c-contentScriptOptions.js
     content/extensions/ext-c-extension.js
 #ifndef ANDROID
     content/extensions/ext-c-identity.js
 #endif
     content/extensions/ext-c-permissions.js
     content/extensions/ext-c-runtime.js
     content/extensions/ext-c-storage.js
     content/extensions/ext-c-test.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/content_script_options.json
@@ -0,0 +1,108 @@
+/* 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/. */
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "contentScriptOptions"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "contentScriptOptions",
+    "permissions": ["contentScriptOptions"],
+    "allowedContexts": ["content"],
+    "description": "Use the <code>browser.contentScriptOptions</code> API to set the properties on an object value synchronously available to the code running in the extension content scripts",
+    "functions": [
+      {
+        "name": "setProperty",
+        "type": "function",
+        "description": "Set a property of the contentScriptOptions",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "propertyName",
+            "description": "The name of the property to set",
+            "minLength": 1
+          },
+          {
+            "name": "propertyValue",
+            "description": "The value of the contentScriptOptions property.",
+            "choices": [
+              {"type": "string"},
+              {"type": "number"},
+              {"type": "boolean"}
+            ]
+          }
+        ]
+      },
+      {
+        "name": "unsetProperty",
+        "type": "function",
+        "async": true,
+        "description": "Clear all the contentScriptOptions properties",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "propertyName",
+            "description": "The name of the property to set",
+            "minLength": 1
+          }
+        ]
+      },
+      {
+        "name": "clear",
+        "type": "function",
+        "async": true,
+        "description": "Clear all the contentScriptOptions properties",
+        "parameters": []
+      },
+      {
+        "name": "getUsageInfo",
+        "type": "function",
+        "async": true,
+        "description": "Retrieve the contentScriptOptions usage info asynchronously",
+        "parameters": []
+      },
+      {
+        "name": "getProperty",
+        "allowedContexts": ["content"],
+        "type": "function",
+        "description": "Get a contentScriptOptions property",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "propertyName",
+            "description": "The name of the property to get",
+            "minLength": 1
+          }
+        ],
+        "returns": {
+          "type": "any",
+          "description": "The property value."
+        }
+      },
+      {
+        "name": "getPropertyNames",
+        "allowedContexts": ["content"],
+        "type": "function",
+        "description": "Get all the contentScriptOptions property names",
+        "parameters": [],
+        "returns": {
+          "type": "array",
+          "items": {"type": "string"}
+        }
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,15 +1,16 @@
 # 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/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/schemas/alarms.json
+    content/extensions/schemas/content_script_options.json
     content/extensions/schemas/contextual_identities.json
     content/extensions/schemas/cookies.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/events.json
     content/extensions/schemas/experiments.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/extension_protocol_handlers.json
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript.js
@@ -95,8 +95,95 @@ add_task(function* test_contentscript() 
   yield contentPage.close();
 
   equal(loadingCount, 1, "document_start script ran exactly once");
   equal(interactiveCount, 1, "document_end script ran exactly once");
   equal(completeCount, 1, "document_idle script ran exactly once");
 
   yield extension.unload();
 });
+
+add_task(function* test_contentscriptOptions() {
+  function background() {
+    browser.test.onMessage.addListener((event, testData) => {
+      if (event !== "content-script-options") {
+        return;
+      }
+
+      if (testData) {
+        for (const prop of Object.keys(testData)) {
+          browser.contentScriptOptions.setProperty(prop, testData[prop]);
+        }
+      } else {
+        browser.contentScriptOptions.clear();
+      }
+    });
+
+    browser.test.sendMessage("background-script-ready");
+  }
+
+  function contentScriptStart() {
+    let propertyNames = browser.contentScriptOptions.getPropertyNames();
+    let testData = {};
+    for (const prop of propertyNames) {
+      testData[prop] = browser.contentScriptOptions.getProperty(prop);
+    }
+    browser.test.sendMessage("content-script-loaded", {testData});
+  }
+
+  let extensionData = {
+    manifest: {
+      applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
+      permissions: ["contentScriptOptions"],
+      content_scripts: [
+        {
+          "matches": ["http://*/*/file_sample.html"],
+          "js": ["content_script_start.js"],
+          "run_at": "document_start",
+        },
+      ],
+    },
+    background,
+
+    files: {
+      "content_script_start.js": contentScriptStart,
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+  yield extension.startup();
+
+  const PAGE_URL = `${BASE_URL}/file_sample.html`;
+  const expectedInitialValue = {key: "content value"};
+  const expectedUpdatedValue = {newkey: "updated content value"};
+
+  const contentPage = yield ExtensionTestUtils.loadContentPage(PAGE_URL);
+
+  yield extension.awaitMessage("background-script-ready");
+
+  extension.sendMessage("content-script-options", expectedInitialValue);
+
+  let res = yield extension.awaitMessage("content-script-loaded");
+  deepEqual(res, {testData: {}}, "contentScriptOptions is undefined on the first load");
+
+  yield contentPage.loadURL(PAGE_URL);
+  res = yield extension.awaitMessage("content-script-loaded");
+  deepEqual(res, {testData: expectedInitialValue},
+            "contentScriptOptions has the expected value on the second load");
+
+  extension.sendMessage("content-script-options", expectedUpdatedValue);
+
+  yield contentPage.loadURL(PAGE_URL);
+  res = yield extension.awaitMessage("content-script-loaded");
+  deepEqual(res, {testData: Object.assign({}, expectedInitialValue, expectedUpdatedValue)},
+            "contentScriptOptions has the expected value on the third load");
+
+  extension.sendMessage("content-script-options", null);
+  yield contentPage.loadURL(PAGE_URL);
+  res = yield extension.awaitMessage("content-script-loaded");
+  deepEqual(res, {testData: {}},
+            "contentScriptOptions has the expected value on the last load");
+
+  yield contentPage.close();
+
+  yield extension.unload();
+});