1
0

screen.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import os
  2. import threading
  3. import time
  4. from typing import List
  5. import pytest
  6. from selenium import webdriver
  7. from selenium.common.exceptions import ElementNotInteractableException, NoSuchElementException
  8. from selenium.webdriver import ActionChains
  9. from selenium.webdriver.common.by import By
  10. from selenium.webdriver.remote.webelement import WebElement
  11. from nicegui import globals, ui
  12. PORT = 3392
  13. IGNORED_CLASSES = ['row', 'column', 'q-card', 'q-field', 'q-field__label', 'q-input']
  14. class Screen:
  15. IMPLICIT_WAIT = 4
  16. SCREENSHOT_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'screenshots')
  17. UI_RUN_KWARGS = {'port': PORT, 'show': False, 'reload': False}
  18. def __init__(self, selenium: webdriver.Chrome, caplog: pytest.LogCaptureFixture) -> None:
  19. self.selenium = selenium
  20. self.caplog = caplog
  21. self.server_thread = None
  22. def start_server(self) -> None:
  23. '''Start the webserver in a separate thread. This is the equivalent of `ui.run()` in a normal script.'''
  24. self.server_thread = threading.Thread(target=ui.run, kwargs=self.UI_RUN_KWARGS)
  25. self.server_thread.start()
  26. @property
  27. def is_open(self) -> None:
  28. # https://stackoverflow.com/a/66150779/3419103
  29. try:
  30. self.selenium.current_url
  31. return True
  32. except Exception:
  33. return False
  34. def stop_server(self) -> None:
  35. '''Stop the webserver.'''
  36. self.close()
  37. self.caplog.clear()
  38. globals.server.should_exit = True
  39. self.server_thread.join()
  40. def open(self, path: str, timeout: float = 3.0) -> None:
  41. '''Try to open the page until the server is ready or we time out.
  42. If the server is not yet running, start it.
  43. '''
  44. if self.server_thread is None:
  45. self.start_server()
  46. deadline = time.time() + timeout
  47. while True:
  48. try:
  49. self.selenium.get(f'http://localhost:{PORT}{path}')
  50. self.selenium.find_element(By.XPATH, '//body') # ensure page and JS are loaded
  51. break
  52. except Exception as e:
  53. if time.time() > deadline:
  54. raise
  55. time.sleep(0.1)
  56. if not self.server_thread.is_alive():
  57. raise RuntimeError('The NiceGUI server has stopped running') from e
  58. def close(self) -> None:
  59. if self.is_open:
  60. self.selenium.close()
  61. def switch_to(self, tab_id: int) -> None:
  62. window_count = len(self.selenium.window_handles)
  63. if tab_id > window_count:
  64. raise IndexError(f'Could not go to or create tab {tab_id}, there are only {window_count} tabs')
  65. elif tab_id == window_count:
  66. self.selenium.switch_to.new_window('tab')
  67. else:
  68. self.selenium.switch_to.window(self.selenium.window_handles[tab_id])
  69. def should_contain(self, text: str) -> None:
  70. if self.selenium.title == text:
  71. return
  72. self.find(text)
  73. def wait_for(self, text: str) -> None:
  74. self.should_contain(text)
  75. def should_not_contain(self, text: str, wait: float = 0.5) -> None:
  76. assert self.selenium.title != text
  77. self.selenium.implicitly_wait(wait)
  78. with pytest.raises(AssertionError):
  79. self.find(text)
  80. self.selenium.implicitly_wait(self.IMPLICIT_WAIT)
  81. def should_contain_input(self, text: str) -> None:
  82. deadline = time.time() + self.IMPLICIT_WAIT
  83. while time.time() < deadline:
  84. for input in self.selenium.find_elements(By.TAG_NAME, 'input'):
  85. if input.get_attribute('value') == text:
  86. return
  87. self.wait(0.1)
  88. raise AssertionError(f'Could not find input with value "{text}"')
  89. def click(self, target_text: str) -> WebElement:
  90. element = self.find(target_text)
  91. try:
  92. element.click()
  93. except ElementNotInteractableException as e:
  94. raise AssertionError(f'Could not click on "{target_text}" on:\n{element.get_attribute("outerHTML")}') from e
  95. return element
  96. def click_at_position(self, element: WebElement, x: int, y: int) -> None:
  97. action = ActionChains(self.selenium)
  98. action.move_to_element_with_offset(element, x, y).click().perform()
  99. def type(self, text: str) -> None:
  100. self.selenium.execute_script("window.focus();")
  101. self.wait(0.2)
  102. self.selenium.switch_to.active_element.send_keys(text)
  103. def find(self, text: str) -> WebElement:
  104. try:
  105. query = f'//*[not(self::script) and not(self::style) and contains(text(), "{text}")]'
  106. element = self.selenium.find_element(By.XPATH, query)
  107. if not element.is_displayed():
  108. self.wait(0.1) # HACK: repeat check after a short delay to avoid timing issue on fast machines
  109. if not element.is_displayed():
  110. raise AssertionError(f'Found "{text}" but it is hidden')
  111. return element
  112. except NoSuchElementException as e:
  113. raise AssertionError(f'Could not find "{text}"') from e
  114. def find_by_tag(self, name: str) -> WebElement:
  115. return self.selenium.find_element(By.TAG_NAME, name)
  116. def find_all_by_tag(self, name: str) -> List[WebElement]:
  117. return self.selenium.find_elements(By.TAG_NAME, name)
  118. def render_js_logs(self) -> str:
  119. console = '\n'.join(l['message'] for l in self.selenium.get_log('browser'))
  120. return f'-- console logs ---\n{console}\n---------------------'
  121. def get_attributes(self, tag: str, attribute: str) -> List[str]:
  122. return [t.get_attribute(attribute) for t in self.find_all_by_tag(tag)]
  123. def wait(self, t: float) -> None:
  124. time.sleep(t)
  125. def shot(self, name: str) -> None:
  126. os.makedirs(self.SCREENSHOT_DIR, exist_ok=True)
  127. filename = f'{self.SCREENSHOT_DIR}/{name}.png'
  128. print(f'Storing screenshot to {filename}')
  129. self.selenium.get_screenshot_as_file(filename)
  130. def assert_py_logger(self, level: str, message: str) -> None:
  131. try:
  132. assert self.caplog.records, 'Expected a log message'
  133. record = self.caplog.records[0]
  134. print(record.levelname, record.message)
  135. assert record.levelname.strip() == level, f'Expected "{level}" but got "{record.levelname}"'
  136. assert record.message.strip() == message, f'Expected "{message}" but got "{record.message}"'
  137. finally:
  138. self.caplog.records.clear()