@@ -15,14 +15,97 @@ ChromeUtils.defineESModuleGetters(lazy, {
1515 dom : "chrome://remote/content/shared/DOM.sys.mjs" ,
1616 error : "chrome://remote/content/shared/webdriver/Errors.sys.mjs" ,
1717 event : "chrome://remote/content/shared/webdriver/Event.sys.mjs" ,
18+ FilePickerListener :
19+ "chrome://remote/content/shared/listeners/FilePickerListener.sys.mjs" ,
20+ OwnershipModel : "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs" ,
21+ setDefaultSerializationOptions :
22+ "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs" ,
1823} ) ;
1924
2025class InputModule extends WindowGlobalBiDiModule {
26+ #filePickerListener;
27+ #subscribedEvents;
28+
2129 constructor ( messageHandler ) {
2230 super ( messageHandler ) ;
31+
32+ this . #filePickerListener = new lazy . FilePickerListener ( ) ;
33+ this . #filePickerListener. on (
34+ "file-picker-opening" ,
35+ this . #onFilePickerOpening
36+ ) ;
37+
38+ // Set of event names which have active subscriptions.
39+ this . #subscribedEvents = new Set ( ) ;
40+ }
41+
42+ destroy ( ) {
43+ this . #filePickerListener. off (
44+ "file-picker-opening" ,
45+ this . #onFilePickerOpening
46+ ) ;
47+ this . #subscribedEvents = null ;
2348 }
2449
25- destroy ( ) { }
50+ async setFiles ( options ) {
51+ const { element : sharedReference , files } = options ;
52+
53+ const element =
54+ await this . #deserializeElementSharedReference( sharedReference ) ;
55+
56+ if (
57+ ! HTMLInputElement . isInstance ( element ) ||
58+ element . type !== "file" ||
59+ element . disabled
60+ ) {
61+ throw new lazy . error . UnableToSetFileInputError (
62+ `Element needs to be an <input> element with type "file" and not disabled`
63+ ) ;
64+ }
65+
66+ if ( files . length > 1 && ! element . hasAttribute ( "multiple" ) ) {
67+ throw new lazy . error . UnableToSetFileInputError (
68+ `Element should have an attribute "multiple" set when trying to set more than 1 file`
69+ ) ;
70+ }
71+
72+ const fileObjects = [ ] ;
73+ for ( const file of files ) {
74+ try {
75+ fileObjects . push ( await File . createFromFileName ( file ) ) ;
76+ } catch ( e ) {
77+ throw new lazy . error . UnsupportedOperationError (
78+ `Failed to add file ${ file } (${ e } )`
79+ ) ;
80+ }
81+ }
82+
83+ const selectedFiles = Array . from ( element . files ) ;
84+
85+ const intersection = fileObjects . filter ( fileObject =>
86+ selectedFiles . some (
87+ selectedFile =>
88+ // Compare file fields to identify if the files are equal.
89+ // TODO: Bug 1883856. Add check for full path or use a different way
90+ // to compare files when it's available.
91+ selectedFile . name === fileObject . name &&
92+ selectedFile . size === fileObject . size &&
93+ selectedFile . type === fileObject . type
94+ )
95+ ) ;
96+
97+ if (
98+ intersection . length === selectedFiles . length &&
99+ selectedFiles . length === fileObjects . length
100+ ) {
101+ lazy . event . cancel ( element ) ;
102+ } else {
103+ element . mozSetFileArray ( fileObjects ) ;
104+
105+ lazy . event . input ( element ) ;
106+ lazy . event . change ( element ) ;
107+ }
108+ }
26109
27110 async #deserializeElementSharedReference( sharedReference ) {
28111 if ( typeof sharedReference ?. sharedId !== "string" ) {
@@ -43,6 +126,85 @@ class InputModule extends WindowGlobalBiDiModule {
43126 return element ;
44127 }
45128
129+ #onFilePickerOpening = ( eventName , data ) => {
130+ const { element } = data ;
131+ if ( element . ownerGlobal . browsingContext != this . messageHandler . context ) {
132+ return ;
133+ }
134+
135+ const realm = this . messageHandler . getRealm ( ) ;
136+
137+ const serializedNode = this . serialize (
138+ element ,
139+ lazy . setDefaultSerializationOptions ( ) ,
140+ lazy . OwnershipModel . None ,
141+ realm
142+ ) ;
143+
144+ this . emitEvent ( "input.fileDialogOpened" , {
145+ context : this . messageHandler . context ,
146+ element : serializedNode ,
147+ multiple : element . multiple ,
148+ } ) ;
149+ } ;
150+
151+ #startListingOnFilePickerOpened( ) {
152+ if ( ! this . #subscribedEvents. has ( "script.FilePickerOpened" ) ) {
153+ this . #filePickerListener. startListening ( ) ;
154+ }
155+ }
156+
157+ #stopListingOnFilePickerOpened( ) {
158+ if ( this . #subscribedEvents. has ( "script.FilePickerOpened" ) ) {
159+ this . #filePickerListener. stopListening ( ) ;
160+ }
161+ }
162+
163+ #subscribeEvent( event ) {
164+ switch ( event ) {
165+ case "input.fileDialogOpened" : {
166+ this . #startListingOnFilePickerOpened( ) ;
167+ this . #subscribedEvents. add ( event ) ;
168+ break ;
169+ }
170+ }
171+ }
172+
173+ #unsubscribeEvent( event ) {
174+ switch ( event ) {
175+ case "input.fileDialogOpened" : {
176+ this . #stopListingOnFilePickerOpened( ) ;
177+ this . #subscribedEvents. delete ( event ) ;
178+ break ;
179+ }
180+ }
181+ }
182+
183+ _applySessionData ( params ) {
184+ // TODO: Bug 1775231. Move this logic to a shared module or an abstract
185+ // class.
186+ const { category } = params ;
187+ if ( category === "event" ) {
188+ const filteredSessionData = params . sessionData . filter ( item =>
189+ this . messageHandler . matchesContext ( item . contextDescriptor )
190+ ) ;
191+ for ( const event of this . #subscribedEvents. values ( ) ) {
192+ const hasSessionItem = filteredSessionData . some (
193+ item => item . value === event
194+ ) ;
195+ // If there are no session items for this context, we should unsubscribe from the event.
196+ if ( ! hasSessionItem ) {
197+ this . #unsubscribeEvent( event ) ;
198+ }
199+ }
200+
201+ // Subscribe to all events, which have an item in SessionData.
202+ for ( const { value } of filteredSessionData ) {
203+ this . #subscribeEvent( value ) ;
204+ }
205+ }
206+ }
207+
46208 _assertInViewPort ( options ) {
47209 const { target } = options ;
48210
@@ -169,66 +331,6 @@ class InputModule extends WindowGlobalBiDiModule {
169331
170332 return [ val . x / dpr , val . y / dpr ] ;
171333 }
172-
173- async setFiles ( options ) {
174- const { element : sharedReference , files } = options ;
175-
176- const element =
177- await this . #deserializeElementSharedReference( sharedReference ) ;
178-
179- if (
180- ! HTMLInputElement . isInstance ( element ) ||
181- element . type !== "file" ||
182- element . disabled
183- ) {
184- throw new lazy . error . UnableToSetFileInputError (
185- `Element needs to be an <input> element with type "file" and not disabled`
186- ) ;
187- }
188-
189- if ( files . length > 1 && ! element . hasAttribute ( "multiple" ) ) {
190- throw new lazy . error . UnableToSetFileInputError (
191- `Element should have an attribute "multiple" set when trying to set more than 1 file`
192- ) ;
193- }
194-
195- const fileObjects = [ ] ;
196- for ( const file of files ) {
197- try {
198- fileObjects . push ( await File . createFromFileName ( file ) ) ;
199- } catch ( e ) {
200- throw new lazy . error . UnsupportedOperationError (
201- `Failed to add file ${ file } (${ e } )`
202- ) ;
203- }
204- }
205-
206- const selectedFiles = Array . from ( element . files ) ;
207-
208- const intersection = fileObjects . filter ( fileObject =>
209- selectedFiles . some (
210- selectedFile =>
211- // Compare file fields to identify if the files are equal.
212- // TODO: Bug 1883856. Add check for full path or use a different way
213- // to compare files when it's available.
214- selectedFile . name === fileObject . name &&
215- selectedFile . size === fileObject . size &&
216- selectedFile . type === fileObject . type
217- )
218- ) ;
219-
220- if (
221- intersection . length === selectedFiles . length &&
222- selectedFiles . length === fileObjects . length
223- ) {
224- lazy . event . cancel ( element ) ;
225- } else {
226- element . mozSetFileArray ( fileObjects ) ;
227-
228- lazy . event . input ( element ) ;
229- lazy . event . change ( element ) ;
230- }
231- }
232334}
233335
234336export const input = InputModule ;
0 commit comments