Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/common/bidi/input.py
1864 views
1
# Licensed to the Software Freedom Conservancy (SFC) under one
2
# or more contributor license agreements. See the NOTICE file
3
# distributed with this work for additional information
4
# regarding copyright ownership. The SFC licenses this file
5
# to you under the Apache License, Version 2.0 (the
6
# "License"); you may not use this file except in compliance
7
# with the License. You may obtain a copy of the License at
8
#
9
# http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing,
12
# software distributed under the License is distributed on an
13
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
# KIND, either express or implied. See the License for the
15
# specific language governing permissions and limitations
16
# under the License.
17
18
import math
19
from dataclasses import dataclass, field
20
from typing import Any, Optional, Union
21
22
from selenium.webdriver.common.bidi.common import command_builder
23
from selenium.webdriver.common.bidi.session import Session
24
25
26
class PointerType:
27
"""Represents the possible pointer types."""
28
29
MOUSE = "mouse"
30
PEN = "pen"
31
TOUCH = "touch"
32
33
VALID_TYPES = {MOUSE, PEN, TOUCH}
34
35
36
class Origin:
37
"""Represents the possible origin types."""
38
39
VIEWPORT = "viewport"
40
POINTER = "pointer"
41
42
43
@dataclass
44
class ElementOrigin:
45
"""Represents an element origin for input actions."""
46
47
type: str
48
element: dict
49
50
def __init__(self, element_reference: dict):
51
self.type = "element"
52
self.element = element_reference
53
54
def to_dict(self) -> dict:
55
"""Convert the ElementOrigin to a dictionary."""
56
return {"type": self.type, "element": self.element}
57
58
59
@dataclass
60
class PointerParameters:
61
"""Represents pointer parameters for pointer actions."""
62
63
pointer_type: str = PointerType.MOUSE
64
65
def __post_init__(self):
66
if self.pointer_type not in PointerType.VALID_TYPES:
67
raise ValueError(f"Invalid pointer type: {self.pointer_type}. Must be one of {PointerType.VALID_TYPES}")
68
69
def to_dict(self) -> dict:
70
"""Convert the PointerParameters to a dictionary."""
71
return {"pointerType": self.pointer_type}
72
73
74
@dataclass
75
class PointerCommonProperties:
76
"""Common properties for pointer actions."""
77
78
width: int = 1
79
height: int = 1
80
pressure: float = 0.0
81
tangential_pressure: float = 0.0
82
twist: int = 0
83
altitude_angle: float = 0.0
84
azimuth_angle: float = 0.0
85
86
def __post_init__(self):
87
if self.width < 1:
88
raise ValueError("width must be at least 1")
89
if self.height < 1:
90
raise ValueError("height must be at least 1")
91
if not (0.0 <= self.pressure <= 1.0):
92
raise ValueError("pressure must be between 0.0 and 1.0")
93
if not (0.0 <= self.tangential_pressure <= 1.0):
94
raise ValueError("tangential_pressure must be between 0.0 and 1.0")
95
if not (0 <= self.twist <= 359):
96
raise ValueError("twist must be between 0 and 359")
97
if not (0.0 <= self.altitude_angle <= math.pi / 2):
98
raise ValueError("altitude_angle must be between 0.0 and π/2")
99
if not (0.0 <= self.azimuth_angle <= 2 * math.pi):
100
raise ValueError("azimuth_angle must be between 0.0 and 2π")
101
102
def to_dict(self) -> dict:
103
"""Convert the PointerCommonProperties to a dictionary."""
104
result: dict[str, Any] = {}
105
if self.width != 1:
106
result["width"] = self.width
107
if self.height != 1:
108
result["height"] = self.height
109
if self.pressure != 0.0:
110
result["pressure"] = self.pressure
111
if self.tangential_pressure != 0.0:
112
result["tangentialPressure"] = self.tangential_pressure
113
if self.twist != 0:
114
result["twist"] = self.twist
115
if self.altitude_angle != 0.0:
116
result["altitudeAngle"] = self.altitude_angle
117
if self.azimuth_angle != 0.0:
118
result["azimuthAngle"] = self.azimuth_angle
119
return result
120
121
122
# Action classes
123
@dataclass
124
class PauseAction:
125
"""Represents a pause action."""
126
127
duration: Optional[int] = None
128
129
@property
130
def type(self) -> str:
131
return "pause"
132
133
def to_dict(self) -> dict:
134
"""Convert the PauseAction to a dictionary."""
135
result: dict[str, Any] = {"type": self.type}
136
if self.duration is not None:
137
result["duration"] = self.duration
138
return result
139
140
141
@dataclass
142
class KeyDownAction:
143
"""Represents a key down action."""
144
145
value: str = ""
146
147
@property
148
def type(self) -> str:
149
return "keyDown"
150
151
def to_dict(self) -> dict:
152
"""Convert the KeyDownAction to a dictionary."""
153
return {"type": self.type, "value": self.value}
154
155
156
@dataclass
157
class KeyUpAction:
158
"""Represents a key up action."""
159
160
value: str = ""
161
162
@property
163
def type(self) -> str:
164
return "keyUp"
165
166
def to_dict(self) -> dict:
167
"""Convert the KeyUpAction to a dictionary."""
168
return {"type": self.type, "value": self.value}
169
170
171
@dataclass
172
class PointerDownAction:
173
"""Represents a pointer down action."""
174
175
button: int = 0
176
properties: Optional[PointerCommonProperties] = None
177
178
@property
179
def type(self) -> str:
180
return "pointerDown"
181
182
def to_dict(self) -> dict:
183
"""Convert the PointerDownAction to a dictionary."""
184
result: dict[str, Any] = {"type": self.type, "button": self.button}
185
if self.properties:
186
result.update(self.properties.to_dict())
187
return result
188
189
190
@dataclass
191
class PointerUpAction:
192
"""Represents a pointer up action."""
193
194
button: int = 0
195
196
@property
197
def type(self) -> str:
198
return "pointerUp"
199
200
def to_dict(self) -> dict:
201
"""Convert the PointerUpAction to a dictionary."""
202
return {"type": self.type, "button": self.button}
203
204
205
@dataclass
206
class PointerMoveAction:
207
"""Represents a pointer move action."""
208
209
x: float = 0
210
y: float = 0
211
duration: Optional[int] = None
212
origin: Optional[Union[str, ElementOrigin]] = None
213
properties: Optional[PointerCommonProperties] = None
214
215
@property
216
def type(self) -> str:
217
return "pointerMove"
218
219
def to_dict(self) -> dict:
220
"""Convert the PointerMoveAction to a dictionary."""
221
result: dict[str, Any] = {"type": self.type, "x": self.x, "y": self.y}
222
if self.duration is not None:
223
result["duration"] = self.duration
224
if self.origin is not None:
225
if isinstance(self.origin, ElementOrigin):
226
result["origin"] = self.origin.to_dict()
227
else:
228
result["origin"] = self.origin
229
if self.properties:
230
result.update(self.properties.to_dict())
231
return result
232
233
234
@dataclass
235
class WheelScrollAction:
236
"""Represents a wheel scroll action."""
237
238
x: int = 0
239
y: int = 0
240
delta_x: int = 0
241
delta_y: int = 0
242
duration: Optional[int] = None
243
origin: Optional[Union[str, ElementOrigin]] = Origin.VIEWPORT
244
245
@property
246
def type(self) -> str:
247
return "scroll"
248
249
def to_dict(self) -> dict:
250
"""Convert the WheelScrollAction to a dictionary."""
251
result: dict[str, Any] = {
252
"type": self.type,
253
"x": self.x,
254
"y": self.y,
255
"deltaX": self.delta_x,
256
"deltaY": self.delta_y,
257
}
258
if self.duration is not None:
259
result["duration"] = self.duration
260
if self.origin is not None:
261
if isinstance(self.origin, ElementOrigin):
262
result["origin"] = self.origin.to_dict()
263
else:
264
result["origin"] = self.origin
265
return result
266
267
268
# Source Actions
269
@dataclass
270
class NoneSourceActions:
271
"""Represents a sequence of none actions."""
272
273
id: str = ""
274
actions: list[PauseAction] = field(default_factory=list)
275
276
@property
277
def type(self) -> str:
278
return "none"
279
280
def to_dict(self) -> dict:
281
"""Convert the NoneSourceActions to a dictionary."""
282
return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}
283
284
285
@dataclass
286
class KeySourceActions:
287
"""Represents a sequence of key actions."""
288
289
id: str = ""
290
actions: list[Union[PauseAction, KeyDownAction, KeyUpAction]] = field(default_factory=list)
291
292
@property
293
def type(self) -> str:
294
return "key"
295
296
def to_dict(self) -> dict:
297
"""Convert the KeySourceActions to a dictionary."""
298
return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}
299
300
301
@dataclass
302
class PointerSourceActions:
303
"""Represents a sequence of pointer actions."""
304
305
id: str = ""
306
parameters: Optional[PointerParameters] = None
307
actions: list[Union[PauseAction, PointerDownAction, PointerUpAction, PointerMoveAction]] = field(
308
default_factory=list
309
)
310
311
def __post_init__(self):
312
if self.parameters is None:
313
self.parameters = PointerParameters()
314
315
@property
316
def type(self) -> str:
317
return "pointer"
318
319
def to_dict(self) -> dict:
320
"""Convert the PointerSourceActions to a dictionary."""
321
result: dict[str, Any] = {
322
"type": self.type,
323
"id": self.id,
324
"actions": [action.to_dict() for action in self.actions],
325
}
326
if self.parameters:
327
result["parameters"] = self.parameters.to_dict()
328
return result
329
330
331
@dataclass
332
class WheelSourceActions:
333
"""Represents a sequence of wheel actions."""
334
335
id: str = ""
336
actions: list[Union[PauseAction, WheelScrollAction]] = field(default_factory=list)
337
338
@property
339
def type(self) -> str:
340
return "wheel"
341
342
def to_dict(self) -> dict:
343
"""Convert the WheelSourceActions to a dictionary."""
344
return {"type": self.type, "id": self.id, "actions": [action.to_dict() for action in self.actions]}
345
346
347
@dataclass
348
class FileDialogInfo:
349
"""Represents file dialog information from input.fileDialogOpened event."""
350
351
context: str
352
multiple: bool
353
element: Optional[dict] = None
354
355
@classmethod
356
def from_dict(cls, data: dict) -> "FileDialogInfo":
357
"""Creates a FileDialogInfo instance from a dictionary.
358
359
Parameters:
360
-----------
361
data: A dictionary containing the file dialog information.
362
363
Returns:
364
-------
365
FileDialogInfo: A new instance of FileDialogInfo.
366
"""
367
return cls(context=data["context"], multiple=data["multiple"], element=data.get("element"))
368
369
370
# Event Class
371
class FileDialogOpened:
372
"""Event class for input.fileDialogOpened event."""
373
374
event_class = "input.fileDialogOpened"
375
376
@classmethod
377
def from_json(cls, json):
378
"""Create FileDialogInfo from JSON data."""
379
return FileDialogInfo.from_dict(json)
380
381
382
class Input:
383
"""
384
BiDi implementation of the input module.
385
"""
386
387
def __init__(self, conn):
388
self.conn = conn
389
self.subscriptions = {}
390
self.callbacks = {}
391
392
def perform_actions(
393
self,
394
context: str,
395
actions: list[Union[NoneSourceActions, KeySourceActions, PointerSourceActions, WheelSourceActions]],
396
) -> None:
397
"""Performs a sequence of user input actions.
398
399
Parameters:
400
-----------
401
context: The browsing context ID where actions should be performed.
402
actions: A list of source actions to perform.
403
"""
404
params = {"context": context, "actions": [action.to_dict() for action in actions]}
405
self.conn.execute(command_builder("input.performActions", params))
406
407
def release_actions(self, context: str) -> None:
408
"""Releases all input state for the given context.
409
410
Parameters:
411
-----------
412
context: The browsing context ID to release actions for.
413
"""
414
params = {"context": context}
415
self.conn.execute(command_builder("input.releaseActions", params))
416
417
def set_files(self, context: str, element: dict, files: list[str]) -> None:
418
"""Sets files for a file input element.
419
420
Parameters:
421
-----------
422
context: The browsing context ID.
423
element: The element reference (script.SharedReference).
424
files: A list of file paths to set.
425
"""
426
params = {"context": context, "element": element, "files": files}
427
self.conn.execute(command_builder("input.setFiles", params))
428
429
def add_file_dialog_handler(self, handler):
430
"""Add a handler for file dialog opened events.
431
432
Parameters:
433
-----------
434
handler: Callback function that takes a FileDialogInfo object.
435
436
Returns:
437
--------
438
int: Callback ID for removing the handler later.
439
"""
440
# Subscribe to the event if not already subscribed
441
if FileDialogOpened.event_class not in self.subscriptions:
442
session = Session(self.conn)
443
self.conn.execute(session.subscribe(FileDialogOpened.event_class))
444
self.subscriptions[FileDialogOpened.event_class] = []
445
446
# Add callback - the callback receives the parsed FileDialogInfo directly
447
callback_id = self.conn.add_callback(FileDialogOpened, handler)
448
449
self.subscriptions[FileDialogOpened.event_class].append(callback_id)
450
self.callbacks[callback_id] = handler
451
452
return callback_id
453
454
def remove_file_dialog_handler(self, callback_id: int) -> None:
455
"""Remove a file dialog handler.
456
457
Parameters:
458
-----------
459
callback_id: The callback ID returned by add_file_dialog_handler.
460
"""
461
if callback_id in self.callbacks:
462
del self.callbacks[callback_id]
463
464
if FileDialogOpened.event_class in self.subscriptions:
465
if callback_id in self.subscriptions[FileDialogOpened.event_class]:
466
self.subscriptions[FileDialogOpened.event_class].remove(callback_id)
467
468
# If no more callbacks for this event, unsubscribe
469
if not self.subscriptions[FileDialogOpened.event_class]:
470
session = Session(self.conn)
471
self.conn.execute(session.unsubscribe(FileDialogOpened.event_class))
472
del self.subscriptions[FileDialogOpened.event_class]
473
474
self.conn.remove_callback(FileDialogOpened, callback_id)
475
476