summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support')
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.jsm83
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.jsm26
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/background.js118
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/manifest.json41
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-api.js229
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-schema.json264
-rw-r--r--mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-support.js48
7 files changed, 809 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.jsm b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.jsm
new file mode 100644
index 0000000000..58c60a4e6c
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.jsm
@@ -0,0 +1,83 @@
+/* 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/. */
+
+const { GeckoViewActorChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewActorChild.sys.mjs"
+);
+
+const EXPORTED_SYMBOLS = ["TestSupportChild"];
+
+class TestSupportChild extends GeckoViewActorChild {
+ receiveMessage(aMsg) {
+ debug`receiveMessage: ${aMsg.name}`;
+
+ switch (aMsg.name) {
+ case "FlushApzRepaints":
+ return new Promise(resolve => {
+ const repaintDone = () => {
+ debug`APZ flush done`;
+ Services.obs.removeObserver(repaintDone, "apz-repaints-flushed");
+ resolve();
+ };
+ Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
+ if (this.contentWindow.windowUtils.flushApzRepaints()) {
+ debug`Flushed APZ repaints, waiting for callback...`;
+ } else {
+ debug`Flushing APZ repaints was a no-op, triggering callback directly...`;
+ repaintDone();
+ }
+ });
+ case "PromiseAllPaintsDone":
+ return new Promise(resolve => {
+ const window = this.contentWindow;
+ const utils = window.windowUtils;
+
+ function waitForPaints() {
+ // Wait until paint suppression has ended
+ if (utils.paintingSuppressed) {
+ dump`waiting for paint suppression to end...`;
+ window.setTimeout(waitForPaints, 0);
+ return;
+ }
+
+ if (utils.isMozAfterPaintPending) {
+ dump`waiting for paint...`;
+ window.addEventListener("MozAfterPaint", waitForPaints, {
+ once: true,
+ });
+ return;
+ }
+ resolve();
+ }
+ waitForPaints();
+ });
+ case "GetLinkColor": {
+ const { selector } = aMsg.data;
+ const element = this.document.querySelector(selector);
+ if (!element) {
+ throw new Error("No element for " + selector);
+ }
+ const color =
+ this.contentWindow.windowUtils.getVisitedDependentComputedStyle(
+ element,
+ "",
+ "color"
+ );
+ return color;
+ }
+ case "SetResolutionAndScaleTo": {
+ return new Promise(resolve => {
+ const window = this.contentWindow;
+ const { resolution } = aMsg.data;
+ window.visualViewport.addEventListener("resize", () => resolve(), {
+ once: true,
+ });
+ window.windowUtils.setResolutionAndScaleTo(resolution);
+ });
+ }
+ }
+ return null;
+ }
+}
+const { debug } = TestSupportChild.initLogging("GeckoViewTestSupport");
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.jsm b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.jsm
new file mode 100644
index 0000000000..e0622d7743
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.jsm
@@ -0,0 +1,26 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["TestSupportProcessChild"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+);
+
+class TestSupportProcessChild extends JSProcessActorChild {
+ receiveMessage(aMsg) {
+ debug`receiveMessage: ${aMsg.name}`;
+
+ switch (aMsg.name) {
+ case "KillContentProcess":
+ ProcessTools.kill(Services.appinfo.processID);
+ }
+ }
+}
+
+const { debug } = GeckoViewUtils.initLogging("TestSupportProcess[C]");
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/background.js b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/background.js
new file mode 100644
index 0000000000..c818719d3b
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/background.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+const port = browser.runtime.connectNative("browser");
+
+const APIS = {
+ AddHistogram({ id, value }) {
+ browser.test.addHistogram(id, value);
+ },
+ Eval({ code }) {
+ // eslint-disable-next-line no-eval
+ return eval(`(async () => {
+ ${code}
+ })()`);
+ },
+ SetScalar({ id, value }) {
+ browser.test.setScalar(id, value);
+ },
+ GetRequestedLocales() {
+ return browser.test.getRequestedLocales();
+ },
+ GetLinkColor({ tab, selector }) {
+ return browser.test.getLinkColor(tab.id, selector);
+ },
+ GetPidForTab({ tab }) {
+ return browser.test.getPidForTab(tab.id);
+ },
+ GetProfilePath() {
+ return browser.test.getProfilePath();
+ },
+ GetAllBrowserPids() {
+ return browser.test.getAllBrowserPids();
+ },
+ KillContentProcess({ pid }) {
+ return browser.test.killContentProcess(pid);
+ },
+ GetPrefs({ prefs }) {
+ return browser.test.getPrefs(prefs);
+ },
+ GetActive({ tab }) {
+ return browser.test.getActive(tab.id);
+ },
+ RemoveAllCertOverrides() {
+ browser.test.removeAllCertOverrides();
+ },
+ RestorePrefs({ oldPrefs }) {
+ return browser.test.restorePrefs(oldPrefs);
+ },
+ SetPrefs({ oldPrefs, newPrefs }) {
+ return browser.test.setPrefs(oldPrefs, newPrefs);
+ },
+ SetResolutionAndScaleTo({ tab, resolution }) {
+ return browser.test.setResolutionAndScaleTo(tab.id, resolution);
+ },
+ FlushApzRepaints({ tab }) {
+ return browser.test.flushApzRepaints(tab.id);
+ },
+ PromiseAllPaintsDone({ tab }) {
+ return browser.test.promiseAllPaintsDone(tab.id);
+ },
+ UsingGpuProcess() {
+ return browser.test.usingGpuProcess();
+ },
+ KillGpuProcess() {
+ return browser.test.killGpuProcess();
+ },
+ CrashGpuProcess() {
+ return browser.test.crashGpuProcess();
+ },
+ ClearHSTSState() {
+ return browser.test.clearHSTSState();
+ },
+ TriggerCookieBannerDetected({ tab }) {
+ return browser.test.triggerCookieBannerDetected(tab.id);
+ },
+ TriggerCookieBannerHandled({ tab }) {
+ return browser.test.triggerCookieBannerHandled(tab.id);
+ },
+};
+
+port.onMessage.addListener(async message => {
+ const impl = APIS[message.type];
+ apiCall(message, impl);
+});
+
+browser.runtime.onConnect.addListener(contentPort => {
+ contentPort.onMessage.addListener(message => {
+ message.args.tab = contentPort.sender.tab;
+
+ const impl = APIS[message.type];
+ apiCall(message, impl);
+ });
+});
+
+function apiCall(message, impl) {
+ const { id, args } = message;
+ try {
+ sendResponse(id, impl(args));
+ } catch (error) {
+ sendResponse(id, Promise.reject(error));
+ }
+}
+
+function sendResponse(id, response) {
+ Promise.resolve(response).then(
+ value => sendSyncResponse(id, value),
+ reason => sendSyncResponse(id, null, reason)
+ );
+}
+
+function sendSyncResponse(id, response, exception) {
+ port.postMessage({
+ id,
+ response: JSON.stringify(response),
+ exception: exception && exception.toString(),
+ });
+}
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/manifest.json b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/manifest.json
new file mode 100644
index 0000000000..133eed8985
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/manifest.json
@@ -0,0 +1,41 @@
+{
+ "manifest_version": 2,
+ "name": "Test support",
+ "version": "1.0",
+ "description": "Helper script for GeckoView tests",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "test-support@tests.mozilla.org"
+ }
+ },
+ "content_scripts": [
+ {
+ "matches": ["<all_urls>"],
+ "js": ["test-support.js"],
+ "run_at": "document_start"
+ }
+ ],
+ "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';",
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "experiment_apis": {
+ "test": {
+ "schema": "test-schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "script": "test-api.js",
+ "events": ["startup"],
+ "paths": [["test"]]
+ }
+ }
+ },
+ "options_ui": {
+ "page": "dummy.html"
+ },
+ "permissions": [
+ "geckoViewAddons",
+ "nativeMessaging",
+ "nativeMessagingFromContent"
+ ]
+}
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-api.js b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-api.js
new file mode 100644
index 0000000000..4bddaedfea
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-api.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+"use strict";
+
+/* globals Services */
+
+const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["PathUtils"]);
+
+this.test = class extends ExtensionAPI {
+ onStartup() {
+ ChromeUtils.registerWindowActor("TestSupport", {
+ child: {
+ moduleURI:
+ "resource://android/assets/web_extensions/test-support/TestSupportChild.jsm",
+ },
+ allFrames: true,
+ });
+ ChromeUtils.registerProcessActor("TestSupportProcess", {
+ child: {
+ moduleURI:
+ "resource://android/assets/web_extensions/test-support/TestSupportProcessChild.jsm",
+ },
+ });
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return;
+ }
+ ChromeUtils.unregisterWindowActor("TestSupport");
+ ChromeUtils.unregisterProcessActor("TestSupportProcess");
+ }
+
+ getAPI(context) {
+ /**
+ * Helper function for getting window or process actors.
+ *
+ * @param tabId - id of the tab; required
+ * @param actorName - a string; the name of the actor
+ * Default: "TestSupport" which is our test framework actor
+ * (you can still pass the second parameter when getting the TestSupport actor, for readability)
+ *
+ * @returns actor
+ */
+ function getActorForTab(tabId, actorName = "TestSupport") {
+ const tab = context.extension.tabManager.get(tabId);
+ const { browsingContext } = tab.browser;
+ return browsingContext.currentWindowGlobal.getActor(actorName);
+ }
+
+ return {
+ test: {
+ /* Set prefs and returns set of saved prefs */
+ async setPrefs(oldPrefs, newPrefs) {
+ // Save old prefs
+ Object.assign(
+ oldPrefs,
+ ...Object.keys(newPrefs)
+ .filter(key => !(key in oldPrefs))
+ .map(key => ({ [key]: Preferences.get(key, null) }))
+ );
+
+ // Set new prefs
+ Preferences.set(newPrefs);
+ return oldPrefs;
+ },
+
+ /* Restore prefs to old value. */
+ async restorePrefs(oldPrefs) {
+ for (const [name, value] of Object.entries(oldPrefs)) {
+ if (value === null) {
+ Preferences.reset(name);
+ } else {
+ Preferences.set(name, value);
+ }
+ }
+ },
+
+ /* Get pref values. */
+ async getPrefs(prefs) {
+ return Preferences.get(prefs);
+ },
+
+ /* Gets link color for a given selector. */
+ async getLinkColor(tabId, selector) {
+ return getActorForTab(tabId, "TestSupport").sendQuery(
+ "GetLinkColor",
+ { selector }
+ );
+ },
+
+ async getRequestedLocales() {
+ return Services.locale.requestedLocales;
+ },
+
+ async getPidForTab(tabId) {
+ const tab = context.extension.tabManager.get(tabId);
+ const pids = E10SUtils.getBrowserPids(tab.browser);
+ return pids[0];
+ },
+
+ async getAllBrowserPids() {
+ const pids = [];
+ const processes = ChromeUtils.getAllDOMProcesses();
+ for (const process of processes) {
+ if (process.remoteType && process.remoteType.startsWith("web")) {
+ pids.push(process.osPid);
+ }
+ }
+ return pids;
+ },
+
+ async killContentProcess(pid) {
+ const procs = ChromeUtils.getAllDOMProcesses();
+ for (const proc of procs) {
+ if (pid === proc.osPid) {
+ proc
+ .getActor("TestSupportProcess")
+ .sendAsyncMessage("KillContentProcess");
+ }
+ }
+ },
+
+ async addHistogram(id, value) {
+ return Services.telemetry.getHistogramById(id).add(value);
+ },
+
+ removeAllCertOverrides() {
+ const overrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+ ].getService(Ci.nsICertOverrideService);
+ overrideService.clearAllOverrides();
+ },
+
+ async setScalar(id, value) {
+ return Services.telemetry.scalarSet(id, value);
+ },
+
+ async setResolutionAndScaleTo(tabId, resolution) {
+ return getActorForTab(tabId, "TestSupport").sendQuery(
+ "SetResolutionAndScaleTo",
+ {
+ resolution,
+ }
+ );
+ },
+
+ async getActive(tabId) {
+ const tab = context.extension.tabManager.get(tabId);
+ return tab.browser.docShellIsActive;
+ },
+
+ async getProfilePath() {
+ return PathUtils.profileDir;
+ },
+
+ async flushApzRepaints(tabId) {
+ // TODO: Note that `waitUntilApzStable` in apz_test_utils.js does
+ // flush APZ repaints in the parent process (i.e. calling
+ // nsIDOMWindowUtils.flushApzRepaints for the parent process) before
+ // flushApzRepaints is called for the target content document, if we
+ // still meet intermittent failures, we might want to do it here as
+ // well.
+ await getActorForTab(tabId, "TestSupport").sendQuery(
+ "FlushApzRepaints"
+ );
+ },
+
+ async promiseAllPaintsDone(tabId) {
+ await getActorForTab(tabId, "TestSupport").sendQuery(
+ "PromiseAllPaintsDone"
+ );
+ },
+
+ async usingGpuProcess() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ return gfxInfo.usingGPUProcess;
+ },
+
+ async killGpuProcess() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ return gfxInfo.killGPUProcessForTests();
+ },
+
+ async crashGpuProcess() {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ return gfxInfo.crashGPUProcessForTests();
+ },
+
+ async clearHSTSState() {
+ const sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+ return sss.clearAll();
+ },
+
+ async triggerCookieBannerDetected(tabId) {
+ const actor = getActorForTab(tabId, "CookieBanner");
+ return actor.receiveMessage({
+ name: "CookieBanner::DetectedBanner",
+ });
+ },
+
+ async triggerCookieBannerHandled(tabId) {
+ const actor = getActorForTab(tabId, "CookieBanner");
+ return actor.receiveMessage({
+ name: "CookieBanner::HandledBanner",
+ });
+ },
+ },
+ };
+ }
+};
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-schema.json b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-schema.json
new file mode 100644
index 0000000000..d07e74cc10
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-schema.json
@@ -0,0 +1,264 @@
+[
+ {
+ "namespace": "test",
+ "description": "Additional APIs for test support in GeckoView.",
+ "functions": [
+ {
+ "name": "setPrefs",
+ "type": "function",
+ "async": true,
+ "description": "Set prefs and return a set of saved prefs",
+ "parameters": [
+ {
+ "name": "oldPrefs",
+ "type": "object",
+ "properties": {},
+ "additionalProperties": { "type": "any" }
+ },
+ {
+ "name": "newPrefs",
+ "type": "object",
+ "properties": {},
+ "additionalProperties": { "type": "any" }
+ }
+ ]
+ },
+ {
+ "name": "restorePrefs",
+ "type": "function",
+ "async": true,
+ "description": "Restore prefs to old value",
+ "parameters": [
+ {
+ "type": "any",
+ "name": "oldPrefs"
+ }
+ ]
+ },
+ {
+ "name": "getPrefs",
+ "type": "function",
+ "async": true,
+ "description": "Get pref values.",
+ "parameters": [
+ {
+ "name": "prefs",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getLinkColor",
+ "type": "function",
+ "async": true,
+ "description": "Get resolved color for the link resolved by a given selector.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ },
+ {
+ "type": "string",
+ "name": "selector"
+ }
+ ]
+ },
+ {
+ "name": "getRequestedLocales",
+ "type": "function",
+ "async": true,
+ "description": "Gets the requested locales.",
+ "parameters": []
+ },
+ {
+ "name": "addHistogram",
+ "type": "function",
+ "async": true,
+ "description": "Add a sample with the given value to the histogram with the given id.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "id"
+ },
+ {
+ "type": "any",
+ "name": "value"
+ }
+ ]
+ },
+ {
+ "name": "removeAllCertOverrides",
+ "type": "function",
+ "async": true,
+ "description": "Revokes SSL certificate overrides.",
+ "parameters": []
+ },
+ {
+ "name": "setScalar",
+ "type": "function",
+ "async": true,
+ "description": "Set the given value to the scalar with the given id.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "id"
+ },
+ {
+ "type": "any",
+ "name": "value"
+ }
+ ]
+ },
+ {
+ "name": "setResolutionAndScaleTo",
+ "type": "function",
+ "async": true,
+ "description": "Invokes nsIDOMWindowUtils.setResolutionAndScaleTo.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ },
+ {
+ "type": "number",
+ "name": "resolution"
+ }
+ ]
+ },
+ {
+ "name": "getActive",
+ "type": "function",
+ "async": true,
+ "description": "Returns true if the docShell is active for given tab.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ },
+ {
+ "name": "getPidForTab",
+ "type": "function",
+ "async": true,
+ "description": "Gets the top-level pid belonging to tabId.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ },
+ {
+ "name": "getAllBrowserPids",
+ "type": "function",
+ "async": true,
+ "description": "Gets the list of pids of the running browser processes",
+ "parameters": []
+ },
+ {
+ "name": "getProfilePath",
+ "type": "function",
+ "async": true,
+ "description": "Gets the path of the current profile",
+ "parameters": []
+ },
+ {
+ "name": "killContentProcess",
+ "type": "function",
+ "async": true,
+ "description": "Crash all content processes",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "pid"
+ }
+ ]
+ },
+ {
+ "name": "flushApzRepaints",
+ "type": "function",
+ "async": true,
+ "description": "Invokes nsIDOMWindowUtils.flushApzRepaints for the document of the tabId.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ },
+ {
+ "name": "promiseAllPaintsDone",
+ "type": "function",
+ "async": true,
+ "description": "A simplified version of promiseAllPaintsDone in paint_listeners.js.",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ },
+ {
+ "name": "usingGpuProcess",
+ "type": "function",
+ "async": true,
+ "description": "Returns true if Gecko is using a GPU process.",
+ "parameters": []
+ },
+
+ {
+ "name": "killGpuProcess",
+ "type": "function",
+ "async": true,
+ "description": "Kills the GPU process cleanly without generating a crash report.",
+ "parameters": []
+ },
+
+ {
+ "name": "crashGpuProcess",
+ "type": "function",
+ "async": true,
+ "description": "Causes the GPU process to crash.",
+ "parameters": []
+ },
+
+ {
+ "name": "clearHSTSState",
+ "type": "function",
+ "async": true,
+ "description": "Clears the sites on the HSTS list.",
+ "parameters": []
+ },
+
+ {
+ "name": "triggerCookieBannerDetected",
+ "type": "function",
+ "async": true,
+ "description": "Simulates a cookie banner detection",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ },
+
+ {
+ "name": "triggerCookieBannerHandled",
+ "type": "function",
+ "async": true,
+ "description": "Simulates a cookie banner handling",
+ "parameters": [
+ {
+ "type": "number",
+ "name": "tabId"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-support.js b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-support.js
new file mode 100644
index 0000000000..c0f073e81d
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-support.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+let backgroundPort = null;
+let nativePort = null;
+
+window.addEventListener("pageshow", () => {
+ backgroundPort = browser.runtime.connect();
+ nativePort = browser.runtime.connectNative("browser");
+
+ nativePort.onMessage.addListener(message => {
+ if (message.type) {
+ // This is a session-specific webExtensionApiCall.
+ // Forward to the background script.
+ backgroundPort.postMessage(message);
+ } else if (message.eval) {
+ try {
+ // Using eval here is the whole point of this WebExtension so we can
+ // safely ignore the eslint warning.
+ const response = window.eval(message.eval); // eslint-disable-line no-eval
+ sendResponse(message.id, response);
+ } catch (ex) {
+ sendSyncResponse(message.id, null, ex);
+ }
+ }
+ });
+
+ function sendResponse(id, response, exception) {
+ Promise.resolve(response).then(
+ value => sendSyncResponse(id, value),
+ reason => sendSyncResponse(id, null, reason)
+ );
+ }
+
+ function sendSyncResponse(id, response, exception) {
+ nativePort.postMessage({
+ id,
+ response: JSON.stringify(response),
+ exception: exception && exception.toString(),
+ });
+ }
+});
+
+window.addEventListener("pagehide", () => {
+ backgroundPort.disconnect();
+ nativePort.disconnect();
+});