Skip to content

Commit 5a73228

Browse files
committed
ENH: Add handler for JS Dialog.
ENH: Add message parser and dialog grabber. WIP: Tab control.
1 parent 875910b commit 5a73228

File tree

1 file changed

+137
-47
lines changed

1 file changed

+137
-47
lines changed

botcity/web/bot.py

Lines changed: 137 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,13 @@ def __init__(self, headless=False):
4343

4444
self._chrome_launcher = None
4545
self._devtools_service = None
46-
self._tab = None
47-
self._first_tab = True
4846

4947
self._page = None
5048
self._network = None
5149
self._input = None
5250
self._run = None
53-
self._tabs = []
51+
52+
self._dialog = None
5453

5554
self._last_clipboard = None
5655

@@ -135,6 +134,26 @@ def stop_browser(self):
135134
# Likely the connection as interrupted already or it timed-out
136135
pass
137136
self._chrome_launcher.shutdown()
137+
self._reset()
138+
139+
def _reset(self):
140+
self._chrome_launcher = None
141+
self._devtools_service = None
142+
143+
self._page = None
144+
self._network = None
145+
self._input = None
146+
self._run = None
147+
148+
def _parse_all_messages(self, messages):
149+
"""
150+
This method inspect all messages emitted by the browser to lookup for information.
151+
"""
152+
for m in messages:
153+
# Checking for dialogs that were opened
154+
if m.get('method') == 'Page.javascriptDialogOpening':
155+
self._dialog = m.get('params')
156+
return self._dialog
138157

139158
def set_download_folder(self, path=None):
140159
"""
@@ -148,12 +167,13 @@ def set_download_folder(self, path=None):
148167
self._download_folder_path = path
149168
if not self._devtools_service:
150169
return
151-
self._devtools_service.Browser.setDownloadBehavior(
170+
res, msgs = self._devtools_service.Browser.setDownloadBehavior(
152171
behavior="allow",
153172
browserContextId=None,
154173
downloadPath=self._download_folder_path,
155174
eventsEnabled=True
156175
)
176+
self._parse_all_messages(msgs)
157177

158178
def set_screen_resolution(self, width=None, height=None):
159179
"""
@@ -170,8 +190,10 @@ def set_screen_resolution(self, width=None, height=None):
170190
"left": 0, "top": 0, "width": width, "height": height
171191
}
172192
window_id = self.get_window_id()
173-
self._devtools_service.Browser.setWindowBounds(windowId=window_id, bounds=bounds)
174-
self._devtools_service.Emulation.setVisibleSize(width=width, height=height)
193+
res, msgs = self._devtools_service.Browser.setWindowBounds(windowId=window_id, bounds=bounds)
194+
self._parse_all_messages(msgs)
195+
res, msgs = self._devtools_service.Emulation.setVisibleSize(width=width, height=height)
196+
self._parse_all_messages(msgs)
175197

176198
##########
177199
# Display
@@ -190,18 +212,11 @@ def get_screen_image(self, region=None):
190212
"""
191213
if not region:
192214
region = (0, 0, 0, 0)
193-
layout_metrics = self._page.getLayoutMetrics()[0]
194-
if layout_metrics:
195-
content_size = layout_metrics['result']['contentSize']
196-
x = region[0] or 0
197-
y = region[1] or 0
198-
width = region[2] or content_size['width']
199-
height = region[3] or content_size['height']
200-
else:
201-
x = 0
202-
y = 0
203-
width = self._dimensions[0]
204-
height = self._dimensions[1]
215+
216+
x = region[0] or 0
217+
y = region[1] or 0
218+
width = region[2] or self._dimensions[0]
219+
height = region[3] or self._dimensions[1]
205220
viewport = dict(x=x, y=y, width=width, height=height, scale=1)
206221
data = self._page.captureScreenshot(format="png", quality=100, clip=viewport,
207222
fromSurface=True, captureBeyondViewport=False)
@@ -279,7 +294,7 @@ def find_multiple(self, labels, x=None, y=None, width=None, height=None, *,
279294
def _to_dict(lbs, elems):
280295
return {k: v for k, v in zip(lbs, elems)}
281296

282-
screen_w, screen_h = self.get_viewport_size()
297+
screen_w, screen_h = self._dimensions
283298
x = x or 0
284299
y = y or 0
285300
w = width or screen_w
@@ -307,7 +322,7 @@ def _to_dict(lbs, elems):
307322
return _to_dict(labels, results)
308323

309324
haystack = self.screenshot()
310-
helper = functools.partial(self.__find_multiple_helper, haystack, region, matching, grayscale)
325+
helper = functools.partial(self._find_multiple_helper, haystack, region, matching, grayscale)
311326

312327
with multiprocessing.Pool(processes=n_cpus) as pool:
313328
results = pool.map(helper, paths)
@@ -318,7 +333,7 @@ def _to_dict(lbs, elems):
318333
else:
319334
return _to_dict(labels, results)
320335

321-
def __find_multiple_helper(self, haystack, region, confidence, grayscale, needle):
336+
def _find_multiple_helper(self, haystack, region, confidence, grayscale, needle):
322337
ele = cv2find.locate_all_opencv(
323338
needle, haystack, region=region, confidence=confidence, grayscale=grayscale
324339
)
@@ -378,7 +393,7 @@ def find_until(self, label, x=None, y=None, width=None, height=None, *,
378393
element (NamedTuple): The element coordinates. None if not found.
379394
"""
380395
self.state.element = None
381-
screen_w, screen_h = self.get_viewport_size()
396+
screen_w, screen_h = self._dimensions
382397
x = x or 0
383398
y = y or 0
384399
w = width or screen_w
@@ -455,8 +470,7 @@ def display_size(self):
455470
Returns:
456471
size (Tuple): The screen dimension (width and height) in pixels.
457472
"""
458-
screen_size = self.get_viewport_size()
459-
return screen_size.width, screen_size.height
473+
return self._dimensions
460474

461475
def screenshot(self, filepath=None, region=None):
462476
"""
@@ -500,11 +514,11 @@ def screen_cut(self, x, y, width=None, height=None):
500514
Returns:
501515
Image: The screenshot Image object
502516
"""
503-
screen_size = self.get_viewport_size()
517+
screen_size = self._dimensions
504518
x = x or 0
505519
y = y or 0
506-
width = width or screen_size.width
507-
height = height or screen_size.height
520+
width = width or screen_size[0]
521+
height = height or screen_size[1]
508522
img = self.screenshot(region=(x, y, width, height))
509523
return img
510524

@@ -537,11 +551,11 @@ def get_element_coords(self, label, x=None, y=None, width=None, height=None, mat
537551
coords (Tuple): A tuple containing the x and y coordinates for the element.
538552
"""
539553
self.state.element = None
540-
screen_size = self.get_viewport_size()
554+
screen_size = self._dimensions
541555
x = x or 0
542556
y = y or 0
543-
width = width or screen_size.width
544-
height = height or screen_size.height
557+
width = width or screen_size[0]
558+
height = height or screen_size[1]
545559
region = (x, y, width, height)
546560

547561
if not best:
@@ -639,6 +653,63 @@ def get_window_id(self):
639653
"""
640654
return self._devtools_service.Browser.getWindowForTarget()[0]['result']['windowId']
641655

656+
def handle_js_dialog(self, accept=True, prompt_text=None):
657+
"""
658+
Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).
659+
This also cleans the dialog information in the local buffer.
660+
661+
Args:
662+
accept (bool): Whether to accept or dismiss the dialog.
663+
prompt_text (str): The text to enter into the dialog prompt before accepting.
664+
Used only if this is a prompt dialog.
665+
"""
666+
kwargs = {'accept': accept}
667+
if prompt_text is not None:
668+
kwargs['promptText'] = prompt_text
669+
self._devtools_service.Page.handleJavaScriptDialog(**kwargs)
670+
self._dialog = None
671+
672+
def get_js_dialog(self):
673+
"""
674+
Return the last found dialog. Invoke first the `find_js_dialog` method to look up.
675+
676+
Returns:
677+
dialog (dict): The dialog information or None if not available.
678+
See https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-javascriptDialogOpening
679+
"""
680+
if not self._dialog:
681+
return self._find_js_dialog()
682+
return self._dialog
683+
684+
def _find_js_dialog(self):
685+
"""
686+
Find the JavaScript dialog that is currently open and return its information.
687+
688+
Returns:
689+
dialog (dict): The dialog information.
690+
See https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-javascriptDialogOpening
691+
"""
692+
messages = self._devtools_service.pop_messages()
693+
print('find_js_dialog -> messages: ', messages)
694+
for m in messages:
695+
if m.get('method') == 'Page.javascriptDialogOpening':
696+
self._dialog = m.get('params')
697+
return self._dialog
698+
699+
def get_tabs(self):
700+
...
701+
702+
def create_tab(self, url):
703+
...
704+
705+
def close_tab(self, tab=None):
706+
# If tab is None, close current tab
707+
...
708+
709+
def activate_tab(self, tab):
710+
...
711+
712+
642713
#######
643714
# Mouse
644715
#######
@@ -684,7 +755,8 @@ def mouse_move(self, x, y):
684755
"""
685756
self._x = x
686757
self._y = y
687-
self._input.dispatchMouseEvent(type="mouseMoved", x=x, y=y)
758+
res, msgs = self._input.dispatchMouseEvent(type="mouseMoved", x=x, y=y)
759+
self._parse_all_messages(msgs)
688760

689761
def click_at(self, x, y, *, clicks=1, interval_between_clicks=0, button='left'):
690762
"""
@@ -702,12 +774,14 @@ def click_at(self, x, y, *, clicks=1, interval_between_clicks=0, button='left'):
702774
self._x = x
703775
self._y = y
704776
for i in range(clicks):
705-
self._input.dispatchMouseEvent(
777+
res, msgs = self._input.dispatchMouseEvent(
706778
type="mousePressed", x=x, y=y, button=button, buttons=idx, clickCount=1
707779
)
708-
self._input.dispatchMouseEvent(
780+
self._parse_all_messages(msgs)
781+
res, msgs = self._input.dispatchMouseEvent(
709782
type="mouseReleased", x=x, y=y, button=button, buttons=idx, clickCount=1
710783
)
784+
self._parse_all_messages(msgs)
711785
self.sleep(interval_between_clicks)
712786

713787
@only_if_element
@@ -805,9 +879,11 @@ def scroll_down(self, clicks):
805879
clicks (int): Number of times to scroll down.
806880
"""
807881
for i in range(clicks):
808-
self._input.dispatchKeyEvent(type="keyDown", commands=["ScrollLineDown"])
882+
res, msgs = self._input.dispatchKeyEvent(type="keyDown", commands=["ScrollLineDown"])
883+
self._parse_all_messages(msgs)
809884
self.sleep(200)
810-
self._input.dispatchKeyEvent(type="keyUp", commands=["ScrollLineDown"])
885+
res, msgs = self._input.dispatchKeyEvent(type="keyUp", commands=["ScrollLineDown"])
886+
self._parse_all_messages(msgs)
811887

812888
def scroll_up(self, clicks):
813889
"""
@@ -817,9 +893,11 @@ def scroll_up(self, clicks):
817893
clicks (int): Number of times to scroll up.
818894
"""
819895
for i in range(clicks):
820-
self._input.dispatchKeyEvent(type="keyDown", commands=["ScrollLineUp"])
896+
res, msgs = self._input.dispatchKeyEvent(type="keyDown", commands=["ScrollLineUp"])
897+
self._parse_all_messages(msgs)
821898
self.sleep(200)
822-
self._input.dispatchKeyEvent(type="keyUp", commands=["ScrollLineUp"])
899+
res, msgs = self._input.dispatchKeyEvent(type="keyUp", commands=["ScrollLineUp"])
900+
self._parse_all_messages(msgs)
823901

824902
def move_to(self, x, y):
825903
"""
@@ -831,7 +909,8 @@ def move_to(self, x, y):
831909
"""
832910
self._x = x
833911
self._y = y
834-
self._input.dispatchMouseEvent(type="mouseMoved", x=x, y=y)
912+
res, msgs = self._input.dispatchMouseEvent(type="mouseMoved", x=x, y=y)
913+
self._parse_all_messages(msgs)
835914

836915
@only_if_element
837916
def move(self):
@@ -917,16 +996,20 @@ def _dispatch_key_event(self, *, event_type="keyDown", text=None, key=None, virt
917996
kwargs = {
918997
"type": event_type
919998
}
999+
if self._shift_hold:
1000+
kwargs.update({"modifiers": 8})
9201001
if text:
9211002
kwargs.update({"text": text})
9221003
if virtual_kc is not None:
9231004
kwargs.update({"windowsVirtualKeyCode": virtual_kc, "nativeVirtualKeyCode": virtual_kc})
9241005
if key is not None:
9251006
kwargs.update({"key": key})
9261007

927-
self._input.dispatchKeyEvent(**kwargs)
1008+
res, msgs = self._input.dispatchKeyEvent(**kwargs)
1009+
self._parse_all_messages(msgs)
9281010
if execute_up:
929-
self._input.dispatchKeyEvent(type="keyUp")
1011+
res, msgs = self._input.dispatchKeyEvent(type="keyUp")
1012+
self._parse_all_messages(msgs)
9301013

9311014
def kb_type(self, text, interval=0):
9321015
"""
@@ -1095,7 +1178,8 @@ def maximize_window(self):
10951178
"""
10961179

10971180
bounds = dict(left=0, top=0, width=0, height=0, windowState="maximized")
1098-
self._devtools_service.Browser.setWindowBounds(windowId=self.get_window_id(), bounds=bounds)
1181+
res, msgs = self._devtools_service.Browser.setWindowBounds(windowId=self.get_window_id(), bounds=bounds)
1182+
self._parse_all_messages(msgs)
10991183
self.sleep(1000)
11001184

11011185
def type_keys_with_interval(self, interval, keys):
@@ -1154,8 +1238,10 @@ def control_c(self, wait=0):
11541238
"document.getElementById('clipboardTransferText').value = text;"
11551239
)
11561240
self.execute_javascript(cmd)
1157-
self._input.dispatchKeyEvent(type="keyDown", commands=["Copy"])
1158-
self._input.dispatchKeyEvent(type="keyUp", commands=["Copy"])
1241+
res, msgs = self._input.dispatchKeyEvent(type="keyDown", commands=["Copy"])
1242+
self._parse_all_messages(msgs)
1243+
res, msgs = self._input.dispatchKeyEvent(type="keyUp", commands=["Copy"])
1244+
self._parse_all_messages(msgs)
11591245
delay = max(0, wait or config.DEFAULT_SLEEP_AFTER_ACTION)
11601246
self.sleep(delay)
11611247

@@ -1167,8 +1253,10 @@ def control_v(self, wait=0):
11671253
wait (int, optional): Wait interval (ms) after task
11681254
11691255
"""
1170-
self._input.dispatchKeyEvent(type="keyDown", commands=["Paste"])
1171-
self._input.dispatchKeyEvent(type="keyUp", commands=["Paste"])
1256+
res, msgs = self._input.dispatchKeyEvent(type="keyDown", commands=["Paste"])
1257+
self._parse_all_messages(msgs)
1258+
res, msgs = self._input.dispatchKeyEvent(type="keyUp", commands=["Paste"])
1259+
self._parse_all_messages(msgs)
11721260
delay = max(0, wait or config.DEFAULT_SLEEP_AFTER_ACTION)
11731261
self.sleep(delay)
11741262

@@ -1180,8 +1268,10 @@ def control_a(self, wait=0):
11801268
wait (int, optional): Wait interval (ms) after task
11811269
11821270
"""
1183-
self._input.dispatchKeyEvent(type="keyDown", commands=["SelectAll"])
1184-
self._input.dispatchKeyEvent(type="keyUp", commands=["SelectAll"])
1271+
res, msgs = self._input.dispatchKeyEvent(type="keyDown", commands=["SelectAll"])
1272+
self._parse_all_messages(msgs)
1273+
res, msgs = self._input.dispatchKeyEvent(type="keyUp", commands=["SelectAll"])
1274+
self._parse_all_messages(msgs)
11851275
delay = max(0, wait or config.DEFAULT_SLEEP_AFTER_ACTION)
11861276
self.sleep(delay)
11871277

0 commit comments

Comments
 (0)