diff options
Diffstat (limited to '')
7 files changed, 898 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.sys.mjs b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.sys.mjs new file mode 100644 index 0000000000..6dd3e8eed4 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportChild.sys.mjs @@ -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/. */ + +import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs"; + +export 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); + }); + } + case "WaitForContentTransformsReceived": { + return this.contentWindow.docShell.browserChild.contentTransformsReceived(); + } + } + return null; + } +} + +const { debug } = TestSupportChild.initLogging("GeckoViewTestSupport"); diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.sys.mjs b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.sys.mjs new file mode 100644 index 0000000000..0684ef0967 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/TestSupportProcessChild.sys.mjs @@ -0,0 +1,22 @@ +/* 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/. */ + +import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; + +const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( + Ci.nsIProcessToolsService +); + +export 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..181764859a --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/background.js @@ -0,0 +1,127 @@ +/* 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); + }, + WaitForContentTransformsReceived({ tab }) { + return browser.test.waitForContentTransformsReceived(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); + }, + TriggerTranslationsOffer({ tab }) { + return browser.test.triggerTranslationsOffer(tab.id); + }, + TriggerLanguageStateChange({ tab, languageState }) { + return browser.test.triggerLanguageStateChange(tab.id, languageState); + }, +}; + +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..fea5add0de --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/manifest.json @@ -0,0 +1,42 @@ +{ + "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>"], + "match_about_blank": true, + "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..1868d25c84 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-api.js @@ -0,0 +1,256 @@ +/* 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: { + esModuleURI: + "resource://android/assets/web_extensions/test-support/TestSupportChild.sys.mjs", + }, + allFrames: true, + }); + ChromeUtils.registerProcessActor("TestSupportProcess", { + child: { + esModuleURI: + "resource://android/assets/web_extensions/test-support/TestSupportProcessChild.sys.mjs", + }, + }); + } + + 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 waitForContentTransformsReceived(tabId) { + return getActorForTab(tabId).sendQuery( + "WaitForContentTransformsReceived" + ); + }, + + 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", + }); + }, + + async triggerTranslationsOffer(tabId) { + const browser = context.extension.tabManager.get(tabId).browser; + const { CustomEvent } = browser.ownerGlobal; + return browser.dispatchEvent( + new CustomEvent("TranslationsParent:OfferTranslation", { + bubbles: true, + }) + ); + }, + + async triggerLanguageStateChange(tabId, languageState) { + const browser = context.extension.tabManager.get(tabId).browser; + const { CustomEvent } = browser.ownerGlobal; + return browser.dispatchEvent( + new CustomEvent("TranslationsParent:LanguageState", { + bubbles: true, + detail: languageState, + }) + ); + }, + }, + }; + } +}; 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..94e4b3bd9b --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-schema.json @@ -0,0 +1,308 @@ +[ + { + "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": "waitForContentTransformsReceived", + "type": "function", + "async": true, + "description": "If we want to test screen coordinates, we need to wait for the updated data which is what this function allows us to do", + "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" + } + ] + }, + + { + "name": "triggerTranslationsOffer", + "type": "function", + "async": true, + "description": "Simulates offering a translation.", + "parameters": [ + { + "type": "number", + "name": "tabId" + } + ] + }, + + { + "name": "triggerLanguageStateChange", + "type": "function", + "async": true, + "description": "Simulates expecting a translation.", + "parameters": [ + { + "type": "number", + "name": "tabId" + }, + { + "name": "languageState", + "type": "object", + "properties": {}, + "additionalProperties": { "type": "any" } + } + ] + } + ] + } +] 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..18e047ca1a --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/test-support/test-support.js @@ -0,0 +1,60 @@ +/* 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; + +function connectNativePort() { + if (nativePort) { + return; + } + + 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(), + }); + } +} + +function disconnectNativePort() { + backgroundPort?.disconnect(); + nativePort?.disconnect(); + backgroundPort = null; + nativePort = null; +} + +window.addEventListener("pageshow", connectNativePort); +window.addEventListener("pagehide", disconnectNativePort); + +// If loading error page, pageshow mightn't fired. +connectNativePort(); |