Coverage for selenible/modules/browser.py: 43%
293 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:24 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-05 14:24 +0000
1import math
2import time
3import urllib.parse
5import yaml
6from selenium.webdriver.common.action_chains import ActionChains
7from selenium.webdriver.common.alert import Alert
8from selenium.webdriver.support import expected_conditions
9from selenium.webdriver.support.select import Select
10from selenium.webdriver.support.ui import WebDriverWait
12open_schema = yaml.safe_load("""
13oneOf:
14 - type: string
15 - type: object
16 properties:
17 url: {type: string}
18 query: {type: object}
19 required: [url]
20""")
23def Base_open(self, param):
24 """
25 - name: open google
26 open: https://www.google.com
27 - name: open google
28 open:
29 url: https://www.google.com/search
30 query:
31 q: keyword1
32 """
33 self.log.debug("open %s", param)
34 if isinstance(param, str):
35 self.driver.get(param)
36 elif isinstance(param, dict): 36 ↛ 46line 36 didn't jump to line 46 because the condition on line 36 was always true
37 url = param.get("url", None)
38 if url is None:
39 raise Exception(f"cannot find open.url: {param}")
40 query = param.get("query", {})
41 qstr = urllib.parse.urlencode(query)
42 if qstr != "": 42 ↛ 45line 42 didn't jump to line 45 because the condition on line 42 was always true
43 url += "?"
44 url += qstr
45 self.driver.get(url)
46 return self.driver.current_url
49screenshot_schema = yaml.safe_load("""
50oneOf:
51 - type: string
52 - allOf:
53 - type: object
54 properties:
55 output: {type: string}
56 optimize: {type: boolean}
57 archive: {type: string}
58 crop:
59 oneOf:
60 - type: string
61 enum: [auto]
62 - type: array
63 items: {type: integer}
64 resize:
65 type: array
66 items: {type: integer}
67 - "$ref": "#/definitions/common/locator"
68""")
71def Base_screenshot(self, param):
72 """
73 - name: take screenshot 1
74 screenshot: shot1.png
75 - name: take screenshot 2
76 screenshot:
77 output: shot2.png
78 optimize: true
79 archive: images.tar
80 crop: auto
81 resize: [800, 600]
82 """
83 self.log.debug("screenshot %s", param)
84 if isinstance(param, str):
85 self.saveshot(param)
86 return param
87 elif isinstance(param, dict): 87 ↛ exitline 87 didn't return from function 'Base_screenshot' because the condition on line 87 was always true
88 output = param.get("output")
89 if output is None:
90 # generate filename
91 ts = time.time()
92 msec = math.modf(ts)[0] * 1000
93 output = param.get("prefix", "")
94 output += time.strftime("%Y%m%d_%H%M%S", time.localtime(ts))
95 output += f"_{int(msec):03d}.png"
96 self.log.debug("filename generated %s", output)
97 self.saveshot(output)
98 elem = self.findmany2one(param)
99 if elem is not None: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 x1 = elem.location["x"]
101 y1 = elem.location["y"]
102 x2 = x1 + elem.size["width"]
103 y2 = y1 + elem.size["height"]
104 nparam = {
105 "input": output,
106 "size": [x1, y1, x2, y2],
107 }
108 self.do_image_crop(nparam)
109 if param.get("crop", None) is not None:
110 nparam = {
111 "input": output,
112 "size": param.get("crop"),
113 }
114 self.do_image_crop(nparam)
115 if param.get("resize", None) is not None:
116 nparam = {
117 "input": output,
118 "size": param.get("resize"),
119 }
120 self.do_image_resize(nparam)
121 if param.get("optimize", False):
122 nparam = {
123 "input": output,
124 }
125 self.do_image_optimize(nparam)
126 if param.get("archive", False):
127 nparam = {
128 "output": param.get("archive"),
129 "input": output,
130 "delete": True,
131 }
132 self.do_image_archive(nparam)
133 return output
136click_schema = {"$ref": "#/definitions/common/locator"}
139def Base_click(self, param):
140 """
141 - name: click1
142 click:
143 id: elementid1
144 - name: click2
145 click:
146 xpath: //div[1]
147 """
148 self.findmany2one(param).click()
151submit_schema = click_schema
154def Base_submit(self, param):
155 """
156 - name: submit element
157 submit:
158 id: elementid1
159 """
160 self.findmany2one(param).submit()
163def Base_waitfor(self, param):
164 waiter = WebDriverWait(self.driver, param.get("timeout", 10))
165 simple_fn = [
166 "title_is",
167 "title_contains",
168 "url_changes",
169 "url_contains",
170 "url_matches",
171 "url_to_be",
172 "number_of_windows_to_be",
173 ]
174 if "alert_is_present" in param:
175 return waiter.until(expected_conditions.alert_is_present())
176 for f in simple_fn:
177 if f in param:
178 return waiter.until(getattr(expected_conditions, f)(param.get(f)))
179 locator_fn = [
180 "element_located_to_be_selected",
181 "element_to_be_clickable",
182 "frame_to_be_available_and_switch_to_it",
183 "invisibility_of_element_located",
184 "presence_of_all_elements_located",
185 "presence_of_element_located",
186 "visibility_of_all_elements_located",
187 "visibility_of_any_elements_located",
188 "visibility_of_element_located",
189 ]
190 for f in locator_fn:
191 if f in param:
192 loc = self.getlocator(param)
193 if len(loc) != 2 or loc[0] is None:
194 raise Exception(f"locator not set: {param}")
195 return waiter.until(getattr(expected_conditions, f)(loc))
196 locator_and_fn = {
197 "text_to_be_present_in_element": None,
198 "text_to_be_present_in_element_value": None,
199 "element_located_selection_state_to_be": "selected",
200 }
201 for f, v in locator_and_fn.items():
202 if f in param:
203 if v is None:
204 arg = self.getvalue(param)
205 else:
206 arg = param.get(v)
207 if arg is None:
208 raise Exception(f"missing argument {v}: param={param}")
209 loc = self.getlocator(param)
210 if len(loc) != 2 or loc[0] is None:
211 raise Exception(f"locator not set: {param}")
212 return waiter.until(getattr(expected_conditions, f)(loc, arg))
213 # other conditions:
214 # element_selection_state_to_be, element_to_be_selected,
215 # new_window_is_opened, staleness_of, visibility_of
216 raise Exception(f"not implemented: param={param}")
219script_schema = yaml.safe_load("""
220oneOf:
221 - type: string
222 - type: array
223 items: {type: string}
224 - type: object
225 properties:
226 file: {type: string}
227 required: [file]
228""")
231def Base_script(self, param):
232 """
233 - name: execute js
234 script: 'alert("hello")'
235 """
236 if isinstance(param, (list, tuple)):
237 for s in param:
238 self.driver.execute_script(s)
239 elif isinstance(param, str):
240 self.driver.execute_script(param)
241 elif isinstance(param, dict):
242 fname = param.get("file", None)
243 if fname is None:
244 raise Exception("no file")
245 with open(fname) as f:
246 self.driver.execute_script(f.read())
247 else:
248 raise Exception(f"parameter error: {param}")
251history_schema = yaml.safe_load("""
252oneOf:
253 - type: string
254 enum: [forward, fwd, f, backward, back, b]
255 - type: array
256 items:
257 type: string
258 enum: [forward, fwd, f, backward, back, b]
259""")
262def Base_history(self, param):
263 """
264 - name: back
265 history: [back, fwd, back, refresh]
266 """
267 fwd = ("forward", "fwd", "f")
268 back = ("backward", "back", "b")
269 refresh = ("refresh", "reload", "r")
270 if isinstance(param, (tuple, list)):
271 for d in param:
272 if d in fwd:
273 self.driver.forward()
274 elif d in back:
275 self.driver.back()
276 elif d in refresh: 276 ↛ 279line 276 didn't jump to line 279 because the condition on line 276 was always true
277 self.driver.refresh()
278 else:
279 raise Exception(f"no such direction: {d}")
280 elif isinstance(param, str): 280 ↛ 290line 280 didn't jump to line 290 because the condition on line 280 was always true
281 if param in fwd:
282 self.driver.forward()
283 elif param in back:
284 self.driver.back()
285 elif param in refresh: 285 ↛ 288line 285 didn't jump to line 288 because the condition on line 285 was always true
286 self.driver.refresh()
287 else:
288 raise Exception(f"no such direction: {param}")
289 else:
290 raise Exception(f"history: not supported direction: {param}")
293sendKeys_schema = yaml.safe_load("""
294allOf:
295 - "$ref": "#/definitions/common/locator"
296 - "$ref": "#/definitions/common/textvalue"
297 - type: object
298 properties:
299 clear: {type: boolean}
300""")
303def Base_sendKeys(self, param):
304 """
305 - name: input username
306 sendKeys:
307 text: user1
308 id: elementid1
309 - name: input password
310 sendKeys:
311 password: site/password
312 # get text from $(pass site/password)
313 id: elementid2
314 """
315 clear = param.get("clear", False)
316 txt = self.getvalue(param)
317 if txt is None:
318 raise Exception(f"text not set: param={param}")
319 elem = self.findmany2one(param)
320 if clear:
321 elem.clear()
322 elem.send_keys(txt)
323 return self.return_element(param, elem)
326setTextValue_schema = yaml.safe_load("""
327allOf:
328 - "$ref": "#/definitions/common/locator"
329 - "$ref": "#/definitions/common/textvalue"
330""")
333def Base_setTextValue(self, param):
334 """
335 - name: input username
336 setTextValue:
337 text: |
338 multi line text1
339 multi line text2
340 id: elementid1
341 """
342 txt = self.getvalue(param)
343 if txt is None:
344 raise Exception(f"text not set: param={param}")
345 elem = self.findmany2one(param)
346 self.driver.execute_script("arguments[0].value = arguments[1];", elem, txt)
347 return self.return_element(param, elem)
350save_schema = yaml.safe_load("""
351allOf:
352 - type: object
353 properties:
354 mode:
355 type: string
356 enum: ["source", "source_outer", "text", "title"]
357 output: {type: string}
358 - "$ref": "#/definitions/common/locator"
359""")
362def Base_save(self, param):
363 """
364 - name: save page title
365 save:
366 mode: title
367 output: title.txt
368 - name: save page content
369 save:
370 mode: source
371 id: element1
372 - name: copy page content to variable
373 save:
374 mode: text
375 id: element2
376 register: title1
377 """
378 mode = param.get("mode", "source")
379 locator = self.getlocator(param)
380 if mode == "source":
381 if locator[0] is None:
382 txt = [self.driver.page_source]
383 else:
384 txt = []
385 for p in self.findmany(param):
386 txt.append(p.get_attribute("innerHTML"))
387 elif mode == "source_outer":
388 if locator[0] is None:
389 txt = [self.driver.page_source]
390 else:
391 txt = []
392 for p in self.findmany(param):
393 txt.append(p.get_attribute("outerHTML"))
394 elif mode == "title":
395 txt = [self.driver.title]
396 elif mode == "text":
397 if locator[0] is None:
398 txt = [self.driver.find_element_by_xpath("/html").text]
399 else:
400 txt = []
401 for p in self.findmany(param):
402 txt.append(p.text)
403 output = param.get("output", None)
404 if output is not None:
405 with open(output, "w") as f:
406 f.write("\n".join(txt))
407 return txt
410dragdrop_schema = yaml.safe_load("""
411type: object
412properties:
413 src: {"$ref": "#/definitions/common/locator"}
414 dst: {"$ref": "#/definitions/common/locator"}
415""")
418def Base_dragdrop(self, param):
419 """
420 - name: drag and drop
421 dragdrop:
422 src:
423 xpath: //div[1]
424 dst:
425 select: "$.x.y.z"
426 """
427 src = self.findmany2one(param.get("src"))
428 dst = self.findmany2one(param.get("dst"))
429 ActionChains(self.driver).drag_and_drop(src, dst).perform()
432switch_schema = yaml.safe_load("""
433oneOf:
434 - type: string
435 enum: [default]
436 - type: boolean
437 - type: "null"
438 - type: object
439 properties:
440 window: {type: string}
441 frame: {type: string}
442""")
445def Base_switch(self, param):
446 """
447 - name: switch window
448 switch:
449 window: win1
450 - name: switch frame
451 switch:
452 frame: frm1
453 - name: switch to default window
454 switch: default
455 """
456 if param in ("default", None, {}, True):
457 self.driver.switch_to_default_content()
458 elif "window" in param:
459 self.driver.switch_to_window(param.get("window"))
460 elif "frame" in param: 460 ↛ exitline 460 didn't return from function 'Base_switch' because the condition on line 460 was always true
461 self.driver.switch_to_frame(param.get("frame"))
464def Base_dropfile(self, param):
465 """
466 - name: drop file
467 dropfile:
468 filename: /path/to/file
469 id: element1
470 """
471 raise Exception("not implemented yet")
474deletecookie_schema = yaml.safe_load("""
475oneOf:
476 - type: array
477 items: {type: string}
478 - type: string
479""")
482def Base_deletecookie(self, param):
483 """
484 - name: delete all cookie
485 deletecookie: all
486 - name: clear cookie a, b, c
487 deletecookie: [a, b, c]
488 """
489 if param == "all":
490 self.driver.delete_all_cookies()
491 elif isinstance(param, (tuple, list)):
492 for c in param:
493 self.driver.delete_cookie(c)
494 elif isinstance(param, str):
495 self.driver.delete_cookie(param)
496 else:
497 raise Exception("invalid argument")
500alertOK_schema = {"type": "boolean"}
503def Base_alertOK(self, param):
504 """
505 - name: accept alert
506 alertOK: true
507 - name: cancel alert
508 alertOK: false
509 """
510 if isinstance(param, bool):
511 if param:
512 Alert(self.driver).accept()
513 else:
514 Alert(self.driver).dismiss()
517auth_schema = yaml.safe_load("""
518type: object
519properties:
520 username: {type: string}
521 password: {type: string}
522""")
525def Base_auth(self, param):
526 """
527 - name: basic/digest auth
528 auth:
529 username: user1
530 password: password1
531 """
532 user = param.get("username", "")
533 passwd = param.get("password", "")
534 Alert(self.driver).authenticate(user, passwd)
537select_schema = yaml.safe_load("""
538allOf:
539 - "$ref": "#/definitions/common/locator"
540 - type: object
541 properties:
542 by_index: {type: integer}
543 by_value: {type: string}
544 by_text: {type: string}
545 all: {type: boolean}
546 return:
547 type: string
548 enum: [selected, first, all]
549""")
552def Base_select(self, param):
553 """
554 - name: select 1st
555 select:
556 id: element1
557 by_index: 1
558 - name: select by value
559 select:
560 id: element1
561 by_value: value1
562 - name: select by visible text
563 select:
564 id: element1
565 by_text: "text 1"
566 """
567 elem = self.findone(param)
568 if elem is None:
569 raise Exception(f"element not found: {param}")
570 flag = param.get("deselect", False)
571 sel = Select(elem)
572 if "by_index" in param:
573 if flag:
574 sel.deselect_by_index(param.get("by_index"))
575 else:
576 sel.select_by_index(param.get("by_index"))
577 elif "by_value" in param:
578 if flag:
579 sel.deselect_by_value(param.get("by_value"))
580 else:
581 sel.select_by_value(param.get("by_value"))
582 elif "by_text" in param:
583 if flag:
584 sel.deselect_by_visible_text(param.get("by_text"))
585 else:
586 sel.select_by_visible_text(param.get("by_text"))
587 elif param.get("all", False):
588 if flag:
589 sel.deselect_all()
590 else:
591 sel.select_all()
592 retp = param.get("return", "selected")
593 if retp == "selected":
594 res = sel.all_selected_options
595 elif retp == "first":
596 res = sel.first_selected_option
597 elif retp == "all":
598 res = sel.options
599 else:
600 return
601 return self.return_element(param, res)
604scroll_schema = yaml.safe_load("""
605anyOf:
606 - "$ref": "#/definitions/common/locator"
607 - type: object
608 properties:
609 relative:
610 type: array
611 items: {type: integer}
612 absolute:
613 type: array
614 items: {type: integer}
615 percent:
616 type: array
617 items: {type: number}
618 position:
619 type: string
620 enum: [top, bottom, right, left, topright, topleft, bottomright, bottomleft]
621""")
624def scrollto(fn, x, y):
625 return f"window.{fn}({x},{y})"
628def Base_scroll(self, param):
629 """
630 - name: scroll down 100 pixel
631 scroll:
632 relative: [0, 100]
633 - name: scroll to pixel
634 scroll:
635 absolute: [0, 100]
636 - name: scroll to percent
637 scroll:
638 percent: [0, 50]
639 - name: scroll to position
640 scroll:
641 position: bottom
642 - name: scroll to element
643 scroll:
644 id: element1
645 """
646 xmax, ymax = "document.body.scrollWidth", "document.body.scrollHeight"
648 relative = param.get("relative")
649 if (
650 relative is not None
651 and isinstance(relative, (tuple, list))
652 and len(relative) == 2
653 ):
654 self.driver.execute_script(scrollto("scrollBy", relative[0], relative[1]))
655 absolute = param.get("absolute")
656 if (
657 absolute is not None
658 and isinstance(absolute, (tuple, list))
659 and len(absolute) == 2
660 ):
661 self.driver.execute_script(scrollto("scrollTo", absolute[0], absolute[1]))
662 percent = param.get("percent")
663 if percent is not None and isinstance(percent, (tuple, list)) and len(percent) == 2:
664 self.driver.execute_script(
665 scrollto(
666 "scrollTo",
667 f"{xmax}*{percent[0] / 100.0:f}",
668 f"{ymax}*{percent[1] / 100.0:f}",
669 )
670 )
671 pos = param.get("position")
672 if pos in ("bottom", "bottomleft"):
673 self.driver.execute_script(scrollto("scrollTo", 0, ymax))
674 elif pos in ("right", "topright"):
675 self.driver.execute_script(scrollto("scrollTo", xmax, 0))
676 elif pos in ("bottomright",):
677 self.driver.execute_script(scrollto("scrollTo", xmax, ymax))
678 elif pos in ("top", "topleft", "left"):
679 self.driver.execute_script(scrollto("scrollTo", 0, 0))
680 locator = self.getlocator(param)
681 if locator[0] is not None:
682 elem = self.findmany2one(param)
683 if elem is not None:
684 self.driver.execute_script("arguments[0].scrollIntoView();", elem)
687def Base_shutdown(self, params):
688 """
689 - name: shutdown webdriver
690 shutdown: null
691 """
692 self.shutdown_driver()
695def Base_browser_setting(self, params):
696 restart = params.pop("restart", False)
697 copt = params.pop("options", {})
698 self.browser_args.update(params)
699 if len(copt) != 0:
700 opt = self.get_options()
701 for k, v in copt.items():
702 if hasattr(opt, k) and callable(getattr(opt, k)):
703 getattr(opt, k)(v)
704 else:
705 self.log.error("no such option: %s(%s): %s", k, v, dir(opt))
706 raise Exception(f"no such option: {k}")
707 self.browser_args["options"] = opt
708 if restart:
709 self.do_shutdown({})