commit 73fb1081cd3c3cc70ea382ed36930e58260fba1a
parent a99dfbcddf35ceb514d059180e6ce3b180659c90
Author: Julian Descottes <jdescottes@mozilla.com>
Date: Thu, 13 Nov 2025 14:51:59 +0000
Bug 1855045 - [bidi] Add support for input.fileDialogOpened event r=Sasha
Differential Revision: https://phabricator.services.mozilla.com/D271791
Diffstat:
7 files changed, 280 insertions(+), 62 deletions(-)
diff --git a/remote/jar.mn b/remote/jar.mn
@@ -52,6 +52,7 @@ remote.jar:
content/shared/listeners/ContextualIdentityListener.sys.mjs (shared/listeners/ContextualIdentityListener.sys.mjs)
content/shared/listeners/DataChannelListener.sys.mjs (shared/listeners/DataChannelListener.sys.mjs)
content/shared/listeners/DownloadListener.sys.mjs (shared/listeners/DownloadListener.sys.mjs)
+ content/shared/listeners/FilePickerListener.sys.mjs (shared/listeners/FilePickerListener.sys.mjs)
content/shared/listeners/LoadListener.sys.mjs (shared/listeners/LoadListener.sys.mjs)
content/shared/listeners/NavigationListener.sys.mjs (shared/listeners/NavigationListener.sys.mjs)
content/shared/listeners/NetworkEventRecord.sys.mjs (shared/listeners/NetworkEventRecord.sys.mjs)
diff --git a/remote/shared/listeners/FilePickerListener.sys.mjs b/remote/shared/listeners/FilePickerListener.sys.mjs
@@ -0,0 +1,78 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
+});
+
+const OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING = "file-input-picker-opening";
+
+/**
+ * The FilePickerListener can be used to listen for file picker dialog openings
+ * triggered by input type=file elements.
+ *
+ * Note that the actual file picker might not open if it is automatically
+ * dismissed as part of the defined user prompt behavior.
+ *
+ * Example:
+ * ```
+ * const listener = new FilePickerListener();
+ * listener.on("file-picker-opening", onFilePickerOpened);
+ * listener.startListening();
+ *
+ * const onFilePickerOpened = (eventName, data) => {
+ * const { element } = data;
+ * console.log("File picker opened:", element.multiple);
+ * };
+ * ```
+ *
+ * @fires FilePickerListener#"file-picker-opening"
+ * The FilePickerListener emits the following event:
+ * - "file-picker-opening" when a file picker is requested to be opened,
+ * with the following object as payload:
+ * - {Element} element
+ * The DOM element which triggered the file picker to open.
+ */
+export class FilePickerListener {
+ #listening;
+
+ constructor() {
+ lazy.EventEmitter.decorate(this);
+
+ this.#listening = false;
+ }
+
+ destroy() {
+ this.stopListening();
+ }
+
+ observe(subject, topic) {
+ switch (topic) {
+ case OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING: {
+ this.emit("file-picker-opening", {
+ element: subject,
+ });
+ break;
+ }
+ }
+ }
+
+ startListening() {
+ if (this.#listening) {
+ return;
+ }
+ Services.obs.addObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING);
+ this.#listening = true;
+ }
+
+ stopListening() {
+ if (!this.#listening) {
+ return;
+ }
+ Services.obs.removeObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING);
+ this.#listening = false;
+ }
+}
diff --git a/remote/webdriver-bidi/jar.mn b/remote/webdriver-bidi/jar.mn
@@ -41,6 +41,7 @@ remote.jar:
# WebDriver BiDi windowglobal-in-root modules
content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs (modules/windowglobal-in-root/browsingContext.sys.mjs)
+ content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs (modules/windowglobal-in-root/input.sys.mjs)
content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs (modules/windowglobal-in-root/log.sys.mjs)
content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs (modules/windowglobal-in-root/network.sys.mjs)
content/webdriver-bidi/modules/windowglobal-in-root/script.sys.mjs (modules/windowglobal-in-root/script.sys.mjs)
diff --git a/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs
@@ -41,6 +41,8 @@ ChromeUtils.defineESModuleGetters(modules.root, {
ChromeUtils.defineESModuleGetters(modules["windowglobal-in-root"], {
browsingContext:
"chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs",
+ input:
+ "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs",
log: "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs",
network:
"chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs",
diff --git a/remote/webdriver-bidi/modules/root/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs
@@ -395,7 +395,7 @@ class InputModule extends RootBiDiModule {
}
static get supportedEvents() {
- return [];
+ return ["input.fileDialogOpened"];
}
}
diff --git a/remote/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs
@@ -0,0 +1,34 @@
+/* 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 { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+class InputModule extends Module {
+ destroy() {}
+
+ interceptEvent(name, payload) {
+ if (name == "input.fileDialogOpened") {
+ const browsingContext = payload.context;
+ if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) {
+ // Discard events for invalid browsing contexts.
+ return null;
+ }
+
+ // Resolve browsing context to a Navigable id.
+ payload.context =
+ lazy.NavigableManager.getIdForBrowsingContext(browsingContext);
+ }
+
+ return payload;
+ }
+}
+
+export const input = InputModule;
diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs
@@ -15,14 +15,97 @@ ChromeUtils.defineESModuleGetters(lazy, {
dom: "chrome://remote/content/shared/DOM.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
+ FilePickerListener:
+ "chrome://remote/content/shared/listeners/FilePickerListener.sys.mjs",
+ OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
+ setDefaultSerializationOptions:
+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
});
class InputModule extends WindowGlobalBiDiModule {
+ #filePickerListener;
+ #subscribedEvents;
+
constructor(messageHandler) {
super(messageHandler);
+
+ this.#filePickerListener = new lazy.FilePickerListener();
+ this.#filePickerListener.on(
+ "file-picker-opening",
+ this.#onFilePickerOpening
+ );
+
+ // Set of event names which have active subscriptions.
+ this.#subscribedEvents = new Set();
+ }
+
+ destroy() {
+ this.#filePickerListener.off(
+ "file-picker-opening",
+ this.#onFilePickerOpening
+ );
+ this.#subscribedEvents = null;
}
- destroy() {}
+ async setFiles(options) {
+ const { element: sharedReference, files } = options;
+
+ const element =
+ await this.#deserializeElementSharedReference(sharedReference);
+
+ if (
+ !HTMLInputElement.isInstance(element) ||
+ element.type !== "file" ||
+ element.disabled
+ ) {
+ throw new lazy.error.UnableToSetFileInputError(
+ `Element needs to be an <input> element with type "file" and not disabled`
+ );
+ }
+
+ if (files.length > 1 && !element.hasAttribute("multiple")) {
+ throw new lazy.error.UnableToSetFileInputError(
+ `Element should have an attribute "multiple" set when trying to set more than 1 file`
+ );
+ }
+
+ const fileObjects = [];
+ for (const file of files) {
+ try {
+ fileObjects.push(await File.createFromFileName(file));
+ } catch (e) {
+ throw new lazy.error.UnsupportedOperationError(
+ `Failed to add file ${file} (${e})`
+ );
+ }
+ }
+
+ const selectedFiles = Array.from(element.files);
+
+ const intersection = fileObjects.filter(fileObject =>
+ selectedFiles.some(
+ selectedFile =>
+ // Compare file fields to identify if the files are equal.
+ // TODO: Bug 1883856. Add check for full path or use a different way
+ // to compare files when it's available.
+ selectedFile.name === fileObject.name &&
+ selectedFile.size === fileObject.size &&
+ selectedFile.type === fileObject.type
+ )
+ );
+
+ if (
+ intersection.length === selectedFiles.length &&
+ selectedFiles.length === fileObjects.length
+ ) {
+ lazy.event.cancel(element);
+ } else {
+ element.mozSetFileArray(fileObjects);
+
+ lazy.event.input(element);
+ lazy.event.change(element);
+ }
+ }
async #deserializeElementSharedReference(sharedReference) {
if (typeof sharedReference?.sharedId !== "string") {
@@ -43,6 +126,85 @@ class InputModule extends WindowGlobalBiDiModule {
return element;
}
+ #onFilePickerOpening = (eventName, data) => {
+ const { element } = data;
+ if (element.ownerGlobal.browsingContext != this.messageHandler.context) {
+ return;
+ }
+
+ const realm = this.messageHandler.getRealm();
+
+ const serializedNode = this.serialize(
+ element,
+ lazy.setDefaultSerializationOptions(),
+ lazy.OwnershipModel.None,
+ realm
+ );
+
+ this.emitEvent("input.fileDialogOpened", {
+ context: this.messageHandler.context,
+ element: serializedNode,
+ multiple: element.multiple,
+ });
+ };
+
+ #startListingOnFilePickerOpened() {
+ if (!this.#subscribedEvents.has("script.FilePickerOpened")) {
+ this.#filePickerListener.startListening();
+ }
+ }
+
+ #stopListingOnFilePickerOpened() {
+ if (this.#subscribedEvents.has("script.FilePickerOpened")) {
+ this.#filePickerListener.stopListening();
+ }
+ }
+
+ #subscribeEvent(event) {
+ switch (event) {
+ case "input.fileDialogOpened": {
+ this.#startListingOnFilePickerOpened();
+ this.#subscribedEvents.add(event);
+ break;
+ }
+ }
+ }
+
+ #unsubscribeEvent(event) {
+ switch (event) {
+ case "input.fileDialogOpened": {
+ this.#stopListingOnFilePickerOpened();
+ this.#subscribedEvents.delete(event);
+ break;
+ }
+ }
+ }
+
+ _applySessionData(params) {
+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
+ // class.
+ const { category } = params;
+ if (category === "event") {
+ const filteredSessionData = params.sessionData.filter(item =>
+ this.messageHandler.matchesContext(item.contextDescriptor)
+ );
+ for (const event of this.#subscribedEvents.values()) {
+ const hasSessionItem = filteredSessionData.some(
+ item => item.value === event
+ );
+ // If there are no session items for this context, we should unsubscribe from the event.
+ if (!hasSessionItem) {
+ this.#unsubscribeEvent(event);
+ }
+ }
+
+ // Subscribe to all events, which have an item in SessionData.
+ for (const { value } of filteredSessionData) {
+ this.#subscribeEvent(value);
+ }
+ }
+ }
+
_assertInViewPort(options) {
const { target } = options;
@@ -169,66 +331,6 @@ class InputModule extends WindowGlobalBiDiModule {
return [val.x / dpr, val.y / dpr];
}
-
- async setFiles(options) {
- const { element: sharedReference, files } = options;
-
- const element =
- await this.#deserializeElementSharedReference(sharedReference);
-
- if (
- !HTMLInputElement.isInstance(element) ||
- element.type !== "file" ||
- element.disabled
- ) {
- throw new lazy.error.UnableToSetFileInputError(
- `Element needs to be an <input> element with type "file" and not disabled`
- );
- }
-
- if (files.length > 1 && !element.hasAttribute("multiple")) {
- throw new lazy.error.UnableToSetFileInputError(
- `Element should have an attribute "multiple" set when trying to set more than 1 file`
- );
- }
-
- const fileObjects = [];
- for (const file of files) {
- try {
- fileObjects.push(await File.createFromFileName(file));
- } catch (e) {
- throw new lazy.error.UnsupportedOperationError(
- `Failed to add file ${file} (${e})`
- );
- }
- }
-
- const selectedFiles = Array.from(element.files);
-
- const intersection = fileObjects.filter(fileObject =>
- selectedFiles.some(
- selectedFile =>
- // Compare file fields to identify if the files are equal.
- // TODO: Bug 1883856. Add check for full path or use a different way
- // to compare files when it's available.
- selectedFile.name === fileObject.name &&
- selectedFile.size === fileObject.size &&
- selectedFile.type === fileObject.type
- )
- );
-
- if (
- intersection.length === selectedFiles.length &&
- selectedFiles.length === fileObjects.length
- ) {
- lazy.event.cancel(element);
- } else {
- element.mozSetFileArray(fileObjects);
-
- lazy.event.input(element);
- lazy.event.change(element);
- }
- }
}
export const input = InputModule;