BrowserHandling.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. import os
  2. from selenium import webdriver
  3. from appium import webdriver as Appiumwebdriver
  4. from selenium.webdriver.support import expected_conditions as ec
  5. from selenium.webdriver.common.by import By
  6. from selenium.webdriver.support.ui import WebDriverWait
  7. from selenium.webdriver.chrome.options import Options as ChromeOptions
  8. from selenium.webdriver.firefox.options import Options as ffOptions
  9. from selenium.common.exceptions import *
  10. from selenium.webdriver.common import keys
  11. from baangt.base import GlobalConstants as GC
  12. from baangt.base.Timing.Timing import Timing
  13. from baangt.TestSteps import Exceptions
  14. import uuid
  15. import time
  16. import logging
  17. from pathlib import Path
  18. import json
  19. import sys
  20. import platform
  21. import ctypes
  22. from urllib.request import urlretrieve
  23. import tarfile
  24. import zipfile
  25. import requests
  26. import time
  27. logger = logging.getLogger("pyC")
  28. class BrowserDriver:
  29. """
  30. The main class for baangt-Elements to interact with a browser.
  31. Main Methods:
  32. - createNewBrowser: Create one instance of a Browser
  33. - findBy*-Methods: e.g. findByAndClick
  34. - URL-Methods: To navigate to an URL
  35. - handleIframe and handleWindow: To navigate between Windows (=tabs) and Iframes
  36. - javaScript: to pass JS directly to the browser
  37. - takeScreenshot: yes, that.
  38. """
  39. def __init__(self, timing=None, screenshotPath=None):
  40. self.driver = None
  41. self.iFrame = None
  42. self.element = None
  43. self.locatorType = None
  44. self.locator = None
  45. self.slowExecution = False
  46. self.slowExecutionTimeoutInSeconds = 1
  47. if timing:
  48. self.timing = timing
  49. else:
  50. self.timing = Timing()
  51. self.takeTime = self.timing.takeTime
  52. if screenshotPath:
  53. self.screenshotPath = screenshotPath
  54. Path(self.screenshotPath).mkdir(exist_ok=True)
  55. else:
  56. self.screenshotPath = os.getcwd()
  57. def createNewBrowser(self, mobileType=None, mobileApp = None, desired_app = None,mobile_app_setting = None, browserName=GC.BROWSER_FIREFOX, desiredCapabilities={}, **kwargs):
  58. """
  59. Will find the specified executables of the desired browser and start it with the given capabilities.
  60. @param browserName: one of GC_BROWSER_*-Browsernames, e.g. GC_BROWSER_FIREFOX
  61. @param desiredCapabilities: DICT of desiredCapabilities for this browser
  62. @param kwargs: Currently (Jan2020) not used
  63. """
  64. self.takeTime("Browser Start")
  65. browserNames = {
  66. GC.BROWSER_FIREFOX: webdriver.Firefox,
  67. GC.BROWSER_CHROME: webdriver.Chrome,
  68. GC.BROWSER_SAFARI: webdriver.Safari,
  69. GC.BROWSER_EDGE: webdriver.Edge,
  70. GC.BROWSER_REMOTE: webdriver.Remote}
  71. ChromeExecutable, GeckoExecutable = BrowserDriver.__getBrowserExecutableNames()
  72. lCurPath = Path(os.getcwd())
  73. lCurPath = lCurPath.joinpath("browserDrivers")
  74. if browserName in browserNames:
  75. browserProxy = kwargs.get('browserProxy')
  76. browserInstance = kwargs.get('browserInstance', 'unknown')
  77. if browserName == GC.BROWSER_FIREFOX:
  78. lCurPath = lCurPath.joinpath(GeckoExecutable)
  79. if mobileType == 'True':
  80. self.mobileConnectAppium(GeckoExecutable, browserName, desired_app, lCurPath, mobileApp,
  81. mobile_app_setting)
  82. else:
  83. if not(os.path.isfile(str(lCurPath))):
  84. self.downloadDriver(browserName)
  85. profile = None
  86. firefox_proxy = browserProxy.selenium_proxy() if browserProxy else None
  87. if firefox_proxy:
  88. profile = webdriver.FirefoxProfile()
  89. profile.set_proxy(firefox_proxy)
  90. self.driver = browserNames[browserName](options=self.__createBrowserOptions(browserName=browserName,
  91. desiredCapabilities=desiredCapabilities),
  92. executable_path=self.__findBrowserDriverPaths(GeckoExecutable),
  93. firefox_profile=profile)
  94. browserProxy.new_har("baangt-firefox-{}".format(browserInstance),
  95. options={'captureHeaders': True, 'captureContent': True}) \
  96. if firefox_proxy else None
  97. elif browserName == GC.BROWSER_CHROME:
  98. lCurPath = lCurPath.joinpath(ChromeExecutable)
  99. if mobileType == 'True':
  100. self.mobileConnectAppium(ChromeExecutable, browserName, desired_app, mobileApp,
  101. mobile_app_setting)
  102. else:
  103. if not (os.path.isfile(str(lCurPath))):
  104. self.downloadDriver(browserName)
  105. self.driver = browserNames[browserName](chrome_options=self.__createBrowserOptions(browserName=browserName,
  106. desiredCapabilities=desiredCapabilities,
  107. browserProxy=browserProxy),
  108. executable_path=self.__findBrowserDriverPaths(ChromeExecutable))
  109. browserProxy.new_har("baangt-chrome-{}".format(browserInstance),
  110. options={'captureHeaders': True, 'captureContent': True}) if browserProxy else None
  111. elif browserName == GC.BROWSER_EDGE:
  112. self.driver = browserNames[browserName](
  113. executable_path=self.__findBrowserDriverPaths("msedgedriver.exe"))
  114. elif browserName == GC.BROWSER_SAFARI:
  115. # SAFARI doesn't provide any options, but desired_capabilities.
  116. # Executable_path = the standard safaridriver path.
  117. if len(desiredCapabilities) == 0:
  118. desiredCapabilities = {}
  119. self.driver = browserNames[browserName](desired_capabilities=desiredCapabilities)
  120. elif browserName == GC.BROWSER_REMOTE:
  121. self.driver = browserNames[browserName](options=self.__createBrowserOptions(browserName=browserName,
  122. desiredCapabilities=desiredCapabilities),
  123. command_executor='http://localhost:4444/wd/hub',
  124. desired_capabilities=desiredCapabilities)
  125. elif browserName == GC.BROWSER_REMOTE_V4:
  126. desired_capabilities = eval(desiredCapabilities)
  127. if 'seleniumGridIp' in desired_capabilities.keys():
  128. seleniumGridIp = desired_capabilities['seleniumGridIp']
  129. del desired_capabilities['seleniumGridIp']
  130. else:
  131. seleniumGridIp = '127.0.0.1'
  132. if 'seleniumGridPort' in desired_capabilities.keys():
  133. seleniumGridPort = desired_capabilities['seleniumGridPort']
  134. del desired_capabilities['seleniumGridPort']
  135. else:
  136. seleniumGridPort = '4444'
  137. if not 'browserName' in desired_capabilities.keys():
  138. desired_capabilities['browserName'] = 'firefox'
  139. if desired_capabilities['browserName'] == 'firefox':
  140. lCurPath = lCurPath.joinpath(GeckoExecutable)
  141. if not (os.path.isfile(str(lCurPath))):
  142. self.downloadDriver(GC.BROWSER_FIREFOX)
  143. elif desired_capabilities['browserName'] == 'chrome':
  144. lCurPath = lCurPath.joinpath(ChromeExecutable)
  145. if not (os.path.isfile(str(lCurPath))):
  146. self.downloadDriver(GC.BROWSER_CHROME)
  147. serverUrl = 'http://' + seleniumGridIp + ':' + seleniumGridPort
  148. self.driver = webdriver.Remote(command_executor=serverUrl,
  149. desired_capabilities=desiredCapabilities)
  150. else:
  151. raise SystemExit("Browsername unknown")
  152. self.takeTime("Browser Start")
  153. @staticmethod
  154. def __getBrowserExecutableNames():
  155. GeckoExecutable = "geckodriver"
  156. ChromeExecutable = "chromedriver"
  157. if 'NT' in os.name.upper():
  158. GeckoExecutable = GeckoExecutable + ".exe"
  159. ChromeExecutable = ChromeExecutable + ".exe"
  160. return ChromeExecutable, GeckoExecutable
  161. def mobileConnectAppium(self, BrowserExecutable, browserName, desired_app, mobileApp, mobile_app_setting):
  162. if desired_app[GC.MOBILE_PLATFORM_NAME] == "Android":
  163. desired_cap = desired_app
  164. if mobileApp == 'True':
  165. desired_cap['app'] = mobile_app_setting[GC.MOBILE_APP_URL]
  166. desired_cap['appPackage'] = mobile_app_setting[GC.MOBILE_APP_PACKAGE]
  167. desired_cap['appActivity'] = mobile_app_setting[GC.MOBILE_APP_ACTIVITY]
  168. else:
  169. desired_cap['browserName'] = browserName
  170. desired_cap['chromedriverExecutable'] = mobile_app_setting[GC.MOBILE_APP_BROWSER_PATH]
  171. desired_cap['noReset'] = False
  172. self.driver = Appiumwebdriver.Remote("http://localhost:4723/wd/hub", desired_cap)
  173. elif desired_app[GC.MOBILE_PLATFORM_NAME] == "iOS":
  174. desired_cap = desired_app
  175. if mobileApp == 'True':
  176. desired_cap['automationName'] = 'XCUITest'
  177. desired_cap['app'] = mobile_app_setting[GC.MOBILE_APP_URL]
  178. else:
  179. desired_cap['browserName'] = 'safari'
  180. self.driver = Appiumwebdriver.Remote("http://localhost:4723/wd/hub", desired_cap)
  181. def __findBrowserDriverPaths(self, filename):
  182. lCurPath = Path(os.getcwd())
  183. lCurPath = lCurPath.joinpath("browserDrivers")
  184. lCurPath = lCurPath.joinpath(filename)
  185. logger.debug(f"Path for BrowserDrivers: {lCurPath}")
  186. return str(lCurPath)
  187. def slowExecutionToggle(self, newSlowExecutionWaitTimeInSeconds=None):
  188. """
  189. SlowExecution can be set in globals or by the teststep. It's intended use is debugging or showcasing a testcases
  190. functionality.
  191. @param newSlowExecutionWaitTimeInSeconds: Optional. If set, it will change the default value of WaitTime, when SlowExecution is active
  192. @return: Returns the state of sloeExecution toggle after toggling was done.
  193. """
  194. if self.slowExecution:
  195. self.slowExecution = False
  196. else:
  197. self.slowExecution = True
  198. if newSlowExecutionWaitTimeInSeconds:
  199. self.slowExecutionTimeoutInSeconds = newSlowExecutionWaitTimeInSeconds
  200. return self.slowExecution
  201. def __createBrowserOptions(self, browserName, desiredCapabilities, browserProxy=None):
  202. """
  203. Translates desired capabilities from the Testrun (or globals) into specific BrowserOptions for the
  204. currently active browser
  205. @param browserName: any of the GC.BROWSER*
  206. @param desiredCapabilities: Settings from TestRun or globals
  207. @param browserProxy: Proxy-Server IP+Port
  208. @return: the proper BrowserOptions for the currently active browser.
  209. """
  210. if browserName == GC.BROWSER_CHROME:
  211. lOptions = ChromeOptions()
  212. lOptions.add_argument('--proxy-server={0}'.format(browserProxy.proxy)) if browserProxy else None
  213. elif browserName == GC.BROWSER_FIREFOX:
  214. lOptions = ffOptions()
  215. else:
  216. return None
  217. if not desiredCapabilities and not browserProxy:
  218. return None
  219. # sometimes instead of DICT comes a string with DICT-Format
  220. if isinstance(desiredCapabilities, str) and "{" in desiredCapabilities and "}" in desiredCapabilities:
  221. desiredCapabilities = json.loads(desiredCapabilities.replace("'", '"'))
  222. if not isinstance(desiredCapabilities, dict) and not browserProxy:
  223. return None
  224. if isinstance(desiredCapabilities, dict) and desiredCapabilities.get(GC.BROWSER_MODE_HEADLESS):
  225. logger.debug("Starting in Headless mode")
  226. lOptions.headless = True
  227. lOptions.add_argument("--window-size=1920,1080")
  228. return lOptions
  229. def closeBrowser(self):
  230. self.driver.quit()
  231. def _log(self, logType, logText, **kwargs):
  232. """
  233. Interal wrapper of Browser-Class for Logging. Takes a screenshot on Error and Warning.
  234. @param logType: any of logging.ERROR, logging.WARN, INFO, etc.
  235. @param logText: Text to log
  236. @param kwargs: Additional Arguments to be logged
  237. """
  238. argsString = ""
  239. for key, value in kwargs.items():
  240. if value:
  241. argsString = argsString + f" {key}: {value}"
  242. if self.locator:
  243. argsString = argsString + f" Locator: {self.locatorType} = {self.locator}"
  244. if logType == logging.DEBUG:
  245. logger.debug(logText + argsString)
  246. elif logType == logging.ERROR:
  247. logger.error(logText + argsString)
  248. self.takeScreenshot()
  249. elif logType == logging.WARN:
  250. logger.warning(logText + argsString)
  251. self.takeScreenshot()
  252. elif logType == logging.INFO:
  253. logger.info(logText + argsString)
  254. elif logType == logging.CRITICAL:
  255. logger.critical(logText + argsString)
  256. self.takeScreenshot()
  257. else:
  258. print(f"Unknown call to Logger: {logType}")
  259. self._log(logging.CRITICAL, f"Unknown type in call to logger: {logType}")
  260. def takeScreenshot(self, screenShotPath=None):
  261. driver = self.driver
  262. # Filename must have ".png" inside
  263. lFile = str(uuid.uuid4()) + ".png"
  264. if screenShotPath:
  265. lFile = Path(screenShotPath).joinpath(lFile)
  266. else:
  267. lFile = Path(self.screenshotPath).joinpath(lFile)
  268. try:
  269. lFile = str(lFile)
  270. driver.save_screenshot(lFile)
  271. self._log(logging.DEBUG, f"Stored Screenshot: {lFile}")
  272. except Exception as e:
  273. self._log(logging.INFO, f"Screenshot not possible. Error: {e}")
  274. return lFile
  275. def handleIframe(self, iframe=None):
  276. """
  277. Give an IFRAME and it will try to go into.
  278. If you're inside an iframe it will go out of the iframe
  279. """
  280. if iframe:
  281. self._log(logging.DEBUG, "Going into Iframe: ", **{"iframe": iframe})
  282. # frame_to_be_availble_and_switch_to_it doesn't work.
  283. mustEnd = time.time() + 30
  284. while time.time() < mustEnd:
  285. try:
  286. self.driver.switch_to.default_content()
  287. self.iFrame = self.driver.switch_to.frame(iframe)
  288. break
  289. except WebDriverException as e:
  290. self._log(logging.DEBUG, f"IFrame {iframe} not there yet - waiting 1 second")
  291. time.sleep(1)
  292. if time.time() > mustEnd:
  293. raise TimeoutError
  294. elif self.iFrame:
  295. self._log(logging.DEBUG, f"Leaving Iframe: {self.iFrame}")
  296. self.driver.switch_to.default_content()
  297. self.iFrame = None
  298. def handleWindow(self, windowNumber=None, function=None):
  299. """
  300. Interations with Windows (=BrowserTabs).
  301. @param windowNumber: Number of the windowHandle inside this browser session (0 = startwindow(=Tab), 1=Next window
  302. @param function: "CLOSE", "CLOSEALL"
  303. """
  304. if function:
  305. if function.lower() == "close":
  306. self.driver.close()
  307. self.driver.switch_to.window(self.driver.window_handles[0])
  308. elif "closeall" in function.lower():
  309. exceptHandles = function.lower().replace("closeall", "")
  310. exceptHandles = exceptHandles.replace("-", "")
  311. # WindowHandles based on 0.. Value "let 2 windows open" means to close everything except 0 and 1:
  312. exceptHandles = int(exceptHandles.strip()) - 1
  313. totalWindows = len(self.driver.window_handles)
  314. for windowHandle in self.driver.window_handles[-1:exceptHandles:-1]:
  315. self.driver.switch_to.window(windowHandle)
  316. self.driver.close()
  317. self.driver.switch_to.window(self.driver.window_handles[exceptHandles])
  318. else:
  319. try:
  320. self.driver.switch_to.window(self.driver.window_handles[windowNumber])
  321. except Exception as e:
  322. logger.critical(f"Tried to switch to Window {windowNumber} but it's not there")
  323. raise Exceptions.baangtTestStepException(f"Window {windowNumber} doesn't exist")
  324. def findByAndWaitForValue(self, id=None, css=None, xpath=None, class_name=None, iframe=None, timeout=20,
  325. optional=False):
  326. """
  327. @param id: ID of the element
  328. @param css: CSS-Locator
  329. @param xpath: XPATH-Locator
  330. @param class_name: Class-Name
  331. @param iframe: Iframe to use (use only if changed. If you set an iframe before, you don't need to set it again!)
  332. @param timeout: Timeout in Seconds before raising an error or returning back (depending on "optional")
  333. @param optional: If set to "True" and the operation can not be executed, just a log entry is written but no error raised
  334. @return: the text of the element, if element was found
  335. """
  336. start = time.time()
  337. found = False
  338. duration = 0
  339. while not found and duration < timeout:
  340. self.element = None
  341. self.findBy(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout / 3,
  342. optional=optional)
  343. try:
  344. if len(self.element.text) > 0:
  345. return self.element.text
  346. elif self.element.tag_name == 'input':
  347. # element is of type <input />
  348. return self.element.get_property('value')
  349. except Exception as e:
  350. logger.debug(f"Exception during findByAndWaitForValue, but continuing {str(e)}, "
  351. f"Locator: {self.locatorType} = {self.locator}")
  352. pass
  353. time.sleep(0.5)
  354. duration = time.time() - start
  355. logger.info(f"Couldn't find value for element {self.locatorType}:{self.locator}")
  356. return None
  357. def findByAndSetText(self, id=None, css=None, xpath=None, class_name=None, value=None, iframe=None,
  358. timeout=60, optional=False):
  359. """
  360. Please see documentation in findBy and __doSomething
  361. """
  362. self.findBy(id=id,
  363. css=css,
  364. xpath=xpath,
  365. class_name=class_name,
  366. iframe=iframe,
  367. timeout=timeout)
  368. self.__doSomething(GC.CMD_SETTEXT, value=value, timeout=timeout, xpath=xpath, optional=optional)
  369. def findByAndSetTextIf(self, id=None, css=None, xpath=None, class_name=None, value=None, iframe=None,
  370. timeout=60):
  371. """
  372. Helper function to not have to write:
  373. If <condition>:
  374. findByAndSetText(locator)
  375. instead use:
  376. findByAndSetTextIf(locator, value).
  377. If value is evaluated into "True" the Text is set.
  378. """
  379. if not value:
  380. return True
  381. if len(value) == 0:
  382. return
  383. return self.findByAndSetText(id=id, css=css, xpath=xpath, class_name=class_name, value=value, iframe=iframe,
  384. timeout=timeout)
  385. def findByAndSetTextValidated(self, id=None,
  386. css=None,
  387. xpath=None,
  388. class_name=None,
  389. value=None,
  390. iframe=None,
  391. timeout=60,
  392. retries=5):
  393. """
  394. This is a method not recommended to be used regularly. Sometimes (especially with Angular Frontends) it gets
  395. pretty hard to set a value into a field. Chrome, but also FF will show the value, but the DOM will not have it.
  396. Ths Method should be your last ressort. Here we try <retries> time to set a value. Then we read the element again
  397. and compare value to what we'd expect. If value is different and we're less than <retries>-Times, we'll try again.
  398. """
  399. tries = 0
  400. self.findBy(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout)
  401. while self.element.text != value and self.element.get_property("value") != value and tries < retries:
  402. self._log(logging.DEBUG, f"Verified trying of SetText - iteration {tries} of {retries}")
  403. self.findByAndForceText(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe,
  404. value=value, timeout=timeout)
  405. self.findBy(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout)
  406. tries += 1
  407. def submit(self):
  408. """
  409. Used for forms to call the standard submit-function (similar to pressing "Enter" in Dialogue)
  410. @return:
  411. """
  412. self.element.submit()
  413. def findByAndClick(self, id=None, css=None, xpath=None, class_name=None, iframe=None, timeout=20, optional=False):
  414. """
  415. Execute a Click on an element identified by it's locator.
  416. @return wasSuccessful says, whether the element was found.
  417. """
  418. wasSuccessful = self.findBy(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout,
  419. optional=optional)
  420. if not wasSuccessful:
  421. logger.debug("findBy didn't work in findByAndClick")
  422. return wasSuccessful
  423. self.__doSomething(GC.CMD_CLICK, xpath=xpath, timeout=timeout, optional=optional)
  424. return wasSuccessful
  425. def findByAndClickIf(self, id=None, css=None, xpath=None, class_name=None, iframe=None, timeout=60,
  426. value=None, optional=False):
  427. """
  428. Convenience method to not have to write:
  429. if <condition>:
  430. findByAndClick(locator)
  431. instead write:
  432. findByAndClickIf(locator, value).
  433. If value is evaluated to "True", the click-event is executed.
  434. """
  435. if not value:
  436. return True
  437. if len(value) == 0:
  438. return True
  439. return self.findByAndClick(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout,
  440. optional=optional)
  441. def findByAndForceText(self, id=None, css=None, xpath=None, class_name=None, value=None,
  442. iframe=None, timeout=60, optional=False):
  443. """
  444. Convenience Method. Please see documentation in findBy and __doSomething.
  445. """
  446. self.findBy(id=id, css=css, xpath=xpath, class_name=class_name, iframe=iframe, timeout=timeout)
  447. self.__doSomething(GC.CMD_FORCETEXT, value=value, timeout=timeout, xpath=xpath, optional=optional)
  448. def findBy(self, id=None, css=None, xpath=None, class_name=None, iframe=None, timeout=60, loggingOn=True,
  449. optional=False):
  450. """
  451. chose any single locator (ID, CSS, XPATH, CLASS_NAME) to identify an element within the page. if slowExectuion
  452. is set, we'll pause for slowExecutionTimeoutInSeconds.
  453. @param id: ID of the element
  454. @param css: CSS-Locator
  455. @param xpath: XPATH
  456. @param class_name: Class-Name
  457. @param iframe: Name of an Iframe. Use only, if you didn't set the Iframe previously already!
  458. @param timeout: How many seconds shall we try/retry, default = 60 Seconds
  459. @param loggingOn: Shall this request be logged? Default = Yes
  460. @param optional: If set to true and within Timeout we can't find the element, we just return this info. If set to False (=default), an Exception is raised
  461. @return: True if element was located, False if element couldn't be found.
  462. """
  463. if self.slowExecution:
  464. time.sleep(self.slowExecutionTimeoutInSeconds)
  465. if self.slowExecution:
  466. time.sleep(self.slowExecutionTimeoutInSeconds)
  467. if iframe:
  468. self.handleIframe(iframe)
  469. # Set class variables for potential logging of problems.
  470. if xpath:
  471. self.locatorType = 'XPATH'
  472. self.locator = xpath
  473. elif css:
  474. self.locatorType = 'CSS'
  475. self.locator = css
  476. elif class_name:
  477. self.locatorType = 'ClassName'
  478. self.locator = class_name
  479. elif id:
  480. self.locatorType = 'ID'
  481. self.locator = id
  482. if loggingOn:
  483. self._log(logging.DEBUG, f"Locating Element {self.locatorType} = {self.locator}")
  484. successful = self.__tryAndRetry(id, css, xpath, class_name, timeout=timeout)
  485. if not successful and not optional:
  486. raise Exceptions.baangtTestStepException(f"Element {self.locatorType} = {self.locator} could not be found "
  487. f"within timeout of {timeout}")
  488. return successful
  489. def getURL(self):
  490. """
  491. @return: the current URL/URI of the current Tab of the current Browser
  492. """
  493. return self.driver.current_url
  494. def __tryAndRetry(self, id=None, css=None, xpath=None, class_name=None, timeout=20):
  495. """
  496. In: Locator
  497. Out: Boolean whether the element was found or not.
  498. Also sets the self.element for further use by other Methods (for instance to setText or read existing value)
  499. The method is resistant to common timing problems (can't work 100% of the time but will remove at least 80%
  500. of your pain compared to directly calling Selenium Methods).
  501. """
  502. wasSuccessful = False
  503. begin = time.time()
  504. elapsed = 0
  505. if timeout < 1.5:
  506. pollFrequency = timeout / 3
  507. else:
  508. pollFrequency = 0.5
  509. internalTimeout = timeout / 3
  510. lLoopCount = 0
  511. while not wasSuccessful and elapsed < timeout:
  512. lLoopCount += 1
  513. try:
  514. driverWait = WebDriverWait(self.driver, timeout=internalTimeout, poll_frequency=pollFrequency)
  515. if id:
  516. self.element = driverWait.until(ec.visibility_of_element_located((By.ID, id)))
  517. elif css:
  518. self.element = driverWait.until(ec.visibility_of_element_located((By.CSS_SELECTOR, css)))
  519. elif class_name:
  520. self.element = self.driver.find_element_by_class_name(class_name)
  521. elif xpath:
  522. # visibility of element sometimes not true, but still clickable. If we tried already
  523. # 2 times with visibility, let's give it one more try with Presence of element
  524. if lLoopCount > 1:
  525. self._log(logging.INFO, "Tried 2 times to find visible element, now trying presence "
  526. f"of element instead, XPATH = {xpath}")
  527. self.element = driverWait.until(ec.presence_of_element_located((By.XPATH, xpath)))
  528. else:
  529. self.element = driverWait.until(ec.visibility_of_element_located((By.XPATH, xpath)))
  530. wasSuccessful = True
  531. except StaleElementReferenceException as e:
  532. self._log(logging.DEBUG, "Stale Element Exception - retrying " + str(e))
  533. time.sleep(pollFrequency)
  534. except ElementClickInterceptedException as e:
  535. self._log(logging.DEBUG, "ElementClickIntercepted - retrying " + str(e))
  536. time.sleep(pollFrequency)
  537. except TimeoutException as e:
  538. self._log(logging.WARNING, "TimoutException - retrying " + str(e))
  539. time.sleep(pollFrequency)
  540. except NoSuchElementException as e:
  541. self._log(logging.WARNING, "Retrying Webdriver Exception: " + str(e))
  542. time.sleep(pollFrequency)
  543. except InvalidSessionIdException as e:
  544. self._log(logging.CRITICAL, "WebDriver Exception - terminating testrun: " + str(e))
  545. raise Exceptions.baangtTestStepException
  546. except NoSuchWindowException as e:
  547. self._log(logging.CRITICAL, "WebDriver Exception - terminating testrun: " + str(e))
  548. raise Exceptions.baangtTestStepException
  549. except ElementNotInteractableException as e:
  550. self._log(logging.DEBUG, "Most probably timeout exception - retrying: " + str(e))
  551. except WebDriverException as e:
  552. self._log(logging.ERROR, "Retrying WebDriver Exception: " + str(e))
  553. time.sleep(2)
  554. elapsed = time.time() - begin
  555. return wasSuccessful
  556. def findWaitNotVisible(self, css=None, xpath=None, id=None, timeout=90, optional=False):
  557. """
  558. You'd use this method when you wait for an element to disappear, for instance Angular Spinner or a popup
  559. to disapear before you continue with your script in the main screen.
  560. """
  561. self._log(logging.DEBUG, "Waiting for Element to disappear", **{"xpath": xpath, "timeout": timeout})
  562. time.sleep(0.5)
  563. stillHere = True
  564. elapsed = 0
  565. begin = time.time()
  566. while stillHere and elapsed < timeout:
  567. try:
  568. if xpath:
  569. self.element = self.driver.find_element_by_xpath(xpath)
  570. elif id:
  571. self.element = self.driver.find_element_by_id(id)
  572. elif css:
  573. self.element = self.driver.find_element_by_css_selector(css)
  574. time.sleep(0.1)
  575. elapsed = time.time() - begin
  576. except Exception as e:
  577. # Element gone - exit
  578. stillHere = False
  579. self._log(logging.DEBUG, f"Element was gone after {format(elapsed, '.2f')} seconds")
  580. return
  581. raise Exceptions.baangtTestStepException(
  582. f"Element still here after {timeout} seconds. Locator: xpath={xpath}, id={id}")
  583. @staticmethod
  584. def sleep(sleepTimeinSeconds):
  585. time.sleep(sleepTimeinSeconds)
  586. def __doSomething(self, command, value=None, timeout=20, xpath=None, optional=False):
  587. """
  588. Will interact in an element (that was found before by findBy-Method and stored in self.element) as defined by
  589. ``command``.
  590. Command can be "SETTEXT" (GC.CMD_SETTEXT), "CLICK" (GC.CMD_CLICK), "FORCETEXT" (GC.CMD_FORCETEXT).
  591. Similarly to __try_and_retry the method is pretty robust when it comes to error handling of timing issues.
  592. """
  593. didWork = False
  594. elapsed = 0
  595. begin = time.time()
  596. while not didWork and elapsed < timeout:
  597. try:
  598. self._log(logging.DEBUG, f"Do_something {command} with {value}")
  599. if command.upper() == GC.CMD_SETTEXT:
  600. self.element.send_keys(value)
  601. elif command.upper() == GC.CMD_CLICK:
  602. self.element.click()
  603. elif command.upper() == GC.CMD_FORCETEXT:
  604. self.element.clear()
  605. for i in range(0, 10):
  606. self.element.send_keys(keys.Keys.BACKSPACE)
  607. time.sleep(0.1)
  608. self.element.send_keys(value)
  609. didWork = True
  610. return
  611. except ElementClickInterceptedException as e:
  612. self._log(logging.DEBUG, "doSomething: Element intercepted - retry")
  613. time.sleep(0.2)
  614. except StaleElementReferenceException as e:
  615. self._log(logging.DEBUG, "doSomething: Element stale - retry")
  616. time.sleep(0.2)
  617. except NoSuchElementException as e:
  618. self._log(logging.DEBUG, "doSomething: Element not there yet - retry")
  619. time.sleep(0.5)
  620. except InvalidSessionIdException as e:
  621. self._log(logging.ERROR, f"Invalid Session ID Exception caught - aborting... {e} ")
  622. raise Exceptions.baangtTestStepException(e)
  623. except ElementNotInteractableException as e:
  624. self._log(logging.ERROR, f"Element not interactable {e}")
  625. raise Exceptions.baangtTestStepException(e)
  626. except NoSuchWindowException as e:
  627. raise Exceptions.baangtTestStepException(e)
  628. elapsed = time.time() - begin
  629. if optional:
  630. logger.debug(
  631. f"Action not possible after {timeout} s, Locator: {self.locatorType}: {self.locator}, but flag 'optional' is set")
  632. else:
  633. raise Exceptions.baangtTestStepException(
  634. f"Action not possible after {timeout} s, Locator: {self.locatorType}: {self.locator}")
  635. def goToUrl(self, url):
  636. self._log(logging.INFO, f'GoToUrl:{url}')
  637. try:
  638. self.driver.get(url)
  639. except WebDriverException as e:
  640. self._log(logging.ERROR, f"Webpage {url} not reached. Error was: {e}")
  641. raise Exceptions.baangtTestStepException
  642. pass
  643. def goBack(self):
  644. """
  645. Method to go 1 step back in current tab's browse history
  646. @return:
  647. """
  648. try:
  649. self.javaScript("window.history.go(-1)")
  650. except Exception as e:
  651. self._log(logging.WARNING, f"Tried to go back in history, didn't work with error {e}")
  652. def javaScript(self, jsText):
  653. """Execute a given JavaScript in the current Session"""
  654. self.driver.execute_script(jsText)
  655. def downloadDriver(self, browserName):
  656. path = Path(os.getcwd())
  657. logger.debug(f"Trying to download browserDriver for {browserName} into {path.joinpath('browserDrivers')}")
  658. path.joinpath("browserDrivers").mkdir(parents=True, exist_ok=True)
  659. path = path.joinpath("browserDrivers")
  660. tar_url = ''
  661. url = ''
  662. if str(browserName) == GC.BROWSER_FIREFOX:
  663. response = requests.get(GC.GECKO_URL)
  664. gecko = response.json()
  665. gecko = gecko['assets']
  666. gecko_length_results = len(gecko)
  667. drivers_url_dict = []
  668. for i in range(gecko_length_results):
  669. drivers_url_dict.append(gecko[i]['browser_download_url'])
  670. zipbObj = zip(GC.OS_list, drivers_url_dict)
  671. geckoDriversDict = dict(zipbObj)
  672. if platform.system().lower() == GC.WIN_PLATFORM:
  673. if ctypes.sizeof(ctypes.c_voidp) == GC.BIT_64:
  674. url = geckoDriversDict[GC.OS_list[4]]
  675. else:
  676. url = geckoDriversDict[GC.OS_list[3]]
  677. elif platform.system().lower() == GC.LINUX_PLATFORM:
  678. if ctypes.sizeof(ctypes.c_voidp) == GC.BIT_64:
  679. tar_url = geckoDriversDict[GC.OS_list[1]]
  680. else:
  681. tar_url = geckoDriversDict[GC.OS_list[0]]
  682. else:
  683. tar_url = geckoDriversDict[GC.OS_list[2]]
  684. if tar_url != '':
  685. path_zip = path.joinpath(GC.GECKO_DRIVER.replace('exe', 'tar.gz'))
  686. filename, headers = urlretrieve(tar_url, path_zip)
  687. logger.debug(f"Tarfile with browser expected here: {filename} ")
  688. tar = tarfile.open(filename, "r:gz")
  689. tar.extractall(path)
  690. tar.close()
  691. else:
  692. file = requests.get(url)
  693. path_zip = path.joinpath(GC.GECKO_DRIVER.replace('exe', 'zip'))
  694. logger.debug(f"Zipfile with browser expected here: {path_zip} ")
  695. open(path_zip, 'wb').write(file.content)
  696. with zipfile.ZipFile(path_zip, 'r') as zip_ref:
  697. zip_ref.extractall(path)
  698. elif browserName == GC.BROWSER_CHROME:
  699. response = requests.get(GC.CHROME_URL)
  700. chromeversion = response.text
  701. chromedriver_url_dict = []
  702. for i in range(len(GC.OS_list_chrome)):
  703. OS = GC.OS_list_chrome[i]
  704. chrome = 'http://chromedriver.storage.googleapis.com/{ver}/chromedriver_{os}.zip'.format(
  705. ver=chromeversion,
  706. os=OS)
  707. chromedriver_url_dict.append(chrome)
  708. zipbObjChrome = zip(GC.OS_list, chromedriver_url_dict)
  709. chromeDriversDict = dict(zipbObjChrome)
  710. if platform.system().lower() == GC.WIN_PLATFORM:
  711. url = chromeDriversDict[GC.OS_list[3]]
  712. elif platform.system().lower() == GC.LINUX_PLATFORM:
  713. url = chromeDriversDict[GC.OS_list[1]]
  714. else:
  715. url = chromeDriversDict[GC.OS_list[2]]
  716. file = requests.get(url)
  717. path_zip = path.joinpath(GC.CHROME_DRIVER.replace('exe', 'zip'))
  718. logger.debug(f"Writing Chrome file into {path_zip}")
  719. open(path_zip, 'wb').write(file.content)
  720. with zipfile.ZipFile(path_zip, 'r') as zip_ref:
  721. zip_ref.extractall(path)
  722. logger.debug(f"Extracting Chrome driver into {path}")
  723. # permissions
  724. if platform.system().lower() != GC.WIN_PLATFORM:
  725. file_path = path.joinpath(GC.CHROME_DRIVER.replace('.exe', ''))
  726. os.chmod(file_path, 0o777)
  727. os.remove(path_zip)
  728. else:
  729. logging.critical(f"Please download driver for {browserName} manually into folder /browserDrivers")