1818 EnvState ,
1919)
2020from playwright .sync_api import sync_playwright
21+ from typing import Literal
22+
23+ # Define a mapping from the user-friendly key names to Playwright's expected key names.
24+ # Playwright is generally good with case-insensitivity for these, but it's best to be canonical.
25+ # See: https://playwright.dev/docs/api/class-keyboard#keyboard-press
26+ # Keys like 'a', 'b', '1', '$' are passed directly.
27+ PLAYWRIGHT_KEY_MAP = {
28+ "backspace" : "Backspace" ,
29+ "tab" : "Tab" ,
30+ "return" : "Enter" , # Playwright uses 'Enter'
31+ "enter" : "Enter" ,
32+ "shift" : "Shift" ,
33+ "control" : "Control" , # Or 'ControlOrMeta' for cross-platform Ctrl/Cmd
34+ "alt" : "Alt" ,
35+ "escape" : "Escape" ,
36+ "space" : "Space" , # Can also just be " "
37+ "pageup" : "PageUp" ,
38+ "pagedown" : "PageDown" ,
39+ "end" : "End" ,
40+ "home" : "Home" ,
41+ "left" : "ArrowLeft" ,
42+ "up" : "ArrowUp" ,
43+ "right" : "ArrowRight" ,
44+ "down" : "ArrowDown" ,
45+ "insert" : "Insert" ,
46+ "delete" : "Delete" ,
47+ "semicolon" : ";" , # For actual character ';'
48+ "equals" : "=" , # For actual character '='
49+ "multiply" : "Multiply" , # NumpadMultiply
50+ "add" : "Add" , # NumpadAdd
51+ "separator" : "Separator" , # Numpad specific
52+ "subtract" : "Subtract" , # NumpadSubtract, or just '-' for character
53+ "decimal" : "Decimal" , # NumpadDecimal, or just '.' for character
54+ "divide" : "Divide" , # NumpadDivide, or just '/' for character
55+ "f1" : "F1" ,
56+ "f2" : "F2" ,
57+ "f3" : "F3" ,
58+ "f4" : "F4" ,
59+ "f5" : "F5" ,
60+ "f6" : "F6" ,
61+ "f7" : "F7" ,
62+ "f8" : "F8" ,
63+ "f9" : "F9" ,
64+ "f10" : "F10" ,
65+ "f11" : "F11" ,
66+ "f12" : "F12" ,
67+ "command" : "Meta" , # 'Meta' is Command on macOS, Windows key on Windows
68+ }
2169
2270
2371class PlaywrightComputer (Computer ):
@@ -91,24 +139,86 @@ def hover_at(self, x: int, y: int):
91139 self ._page .wait_for_load_state ()
92140 return self .current_state ()
93141
94- def type_text_at (self , x : int , y : int , text : str ) -> EnvState :
142+ def type_text_at (
143+ self ,
144+ x : int ,
145+ y : int ,
146+ text : str ,
147+ press_enter : bool = True ,
148+ clear_before_typing : bool = True ,
149+ ) -> EnvState :
95150 self .highlight_mouse (x , y )
96151 self ._page .mouse .click (x , y )
97152 self ._page .wait_for_load_state ()
153+
154+ if clear_before_typing :
155+ self .key_combination (["Control" , "A" ])
156+ self .key_combination (["Delete" ])
157+
98158 self ._page .keyboard .type (text )
99159 self ._page .wait_for_load_state ()
100- self .key_combination (["Enter" ])
160+
161+ if press_enter :
162+ self .key_combination (["Enter" ])
101163 self ._page .wait_for_load_state ()
102164 return self .current_state ()
103165
104- def scroll_document (self , direction : str ) -> EnvState :
105- if direction .lower () == "down" :
166+ def _horizontal_document_scroll (
167+ self , direction : Literal ["left" , "right" ]
168+ ) -> EnvState :
169+ # Scroll by 50% of the viewport size.
170+ horizontal_scroll_amount = self .screen_size ()[0 ] // 2
171+ if direction == "left" :
172+ sign = "-"
173+ else :
174+ sign = ""
175+ scroll_argument = f"{ sign } { horizontal_scroll_amount } "
176+ # Scroll using JS.
177+ self ._page .evaluate (f"window.scrollBy({ scroll_argument } , 0); " )
178+ self ._page .wait_for_load_state ()
179+ return self .current_state ()
180+
181+ def scroll_document (
182+ self , direction : Literal ["up" , "down" , "left" , "right" ]
183+ ) -> EnvState :
184+ if direction == "down" :
106185 return self .key_combination (["PageDown" ])
107- elif direction . lower () == "up" :
186+ elif direction == "up" :
108187 return self .key_combination (["PageUp" ])
188+ elif direction in ("left" , "right" ):
189+ return self ._horizontal_document_scroll (direction )
190+ else :
191+ raise ValueError ("Unsupported direction: " , direction )
192+
193+ def scroll_at (
194+ self ,
195+ x : int ,
196+ y : int ,
197+ direction : Literal ["up" , "down" , "left" , "right" ],
198+ magnitude : int ,
199+ ) -> EnvState :
200+ self .highlight_mouse (x , y )
201+
202+ self ._page .mouse .move (x , y )
203+ self ._page .wait_for_load_state ()
204+
205+ dx = 0
206+ dy = 0
207+ if direction == "up" :
208+ dy = - magnitude
209+ elif direction == "down" :
210+ dy = magnitude
211+ elif direction == "left" :
212+ dx = - magnitude
213+ elif direction == "right" :
214+ dx = magnitude
109215 else :
110216 raise ValueError ("Unsupported direction: " , direction )
111217
218+ self ._page .mouse .wheel (dx , dy )
219+ self ._page .wait_for_load_state ()
220+ return self .current_state ()
221+
112222 def wait_5_seconds (self ) -> EnvState :
113223 time .sleep (5 )
114224 return self .current_state ()
@@ -132,6 +242,9 @@ def navigate(self, url: str) -> EnvState:
132242 return self .current_state ()
133243
134244 def key_combination (self , keys : list [str ]) -> EnvState :
245+ # Normalize all keys to the Playwright compatible version.
246+ keys = [PLAYWRIGHT_KEY_MAP .get (k .lower (), k ) for k in keys ]
247+
135248 for key in keys [:- 1 ]:
136249 self ._page .keyboard .down (key )
137250
@@ -142,6 +255,21 @@ def key_combination(self, keys: list[str]) -> EnvState:
142255
143256 return self .current_state ()
144257
258+ def drag_and_drop (
259+ self , x : int , y : int , destination_x : int , destination_y : int
260+ ) -> EnvState :
261+ self .highlight_mouse (x , y )
262+ self ._page .mouse .move (x , y )
263+ self ._page .wait_for_load_state ()
264+ self ._page .mouse .down ()
265+ self ._page .wait_for_load_state ()
266+
267+ self .highlight_mouse (destination_x , destination_y )
268+ self ._page .mouse .move (destination_x , destination_y )
269+ self ._page .wait_for_load_state ()
270+ self ._page .mouse .up ()
271+ return self .current_state ()
272+
145273 def current_state (self ) -> EnvState :
146274 self ._page .wait_for_load_state ()
147275 # Even if Playwright reports the page as loaded, it may not be so.
@@ -167,7 +295,7 @@ def highlight_mouse(self, x: int, y: int):
167295 div.style.borderRadius = '50%';
168296 div.style.width = '20px';
169297 div.style.height = '20px';
170- div.style.position = 'absolute ';
298+ div.style.position = 'fixed ';
171299 div.style.zIndex = '9999';
172300 document.body.appendChild(div);
173301
0 commit comments