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

1import math 

2import time 

3import urllib.parse 

4 

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 

11 

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""") 

21 

22 

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 

47 

48 

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""") 

69 

70 

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 

134 

135 

136click_schema = {"$ref": "#/definitions/common/locator"} 

137 

138 

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() 

149 

150 

151submit_schema = click_schema 

152 

153 

154def Base_submit(self, param): 

155 """ 

156 - name: submit element 

157 submit: 

158 id: elementid1 

159 """ 

160 self.findmany2one(param).submit() 

161 

162 

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}") 

217 

218 

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""") 

229 

230 

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}") 

249 

250 

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""") 

260 

261 

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}") 

291 

292 

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""") 

301 

302 

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) 

324 

325 

326setTextValue_schema = yaml.safe_load(""" 

327allOf: 

328 - "$ref": "#/definitions/common/locator" 

329 - "$ref": "#/definitions/common/textvalue" 

330""") 

331 

332 

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) 

348 

349 

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""") 

360 

361 

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 

408 

409 

410dragdrop_schema = yaml.safe_load(""" 

411type: object 

412properties: 

413 src: {"$ref": "#/definitions/common/locator"} 

414 dst: {"$ref": "#/definitions/common/locator"} 

415""") 

416 

417 

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() 

430 

431 

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""") 

443 

444 

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")) 

462 

463 

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") 

472 

473 

474deletecookie_schema = yaml.safe_load(""" 

475oneOf: 

476 - type: array 

477 items: {type: string} 

478 - type: string 

479""") 

480 

481 

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") 

498 

499 

500alertOK_schema = {"type": "boolean"} 

501 

502 

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() 

515 

516 

517auth_schema = yaml.safe_load(""" 

518type: object 

519properties: 

520 username: {type: string} 

521 password: {type: string} 

522""") 

523 

524 

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) 

535 

536 

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""") 

550 

551 

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) 

602 

603 

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""") 

622 

623 

624def scrollto(fn, x, y): 

625 return f"window.{fn}({x},{y})" 

626 

627 

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" 

647 

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) 

685 

686 

687def Base_shutdown(self, params): 

688 """ 

689 - name: shutdown webdriver 

690 shutdown: null 

691 """ 

692 self.shutdown_driver() 

693 

694 

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({})