screen.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  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. def remove_prefix(text: str, prefix: str) -> str:
  15. if prefix and text.startswith(prefix):
  16. return text[len(prefix):]
  17. return text
  18. class Screen:
  19. SCREENSHOT_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'screenshots')
  20. UI_RUN_KWARGS = {'port': PORT, 'show': False, 'reload': False}
  21. def __init__(self, selenium: webdriver.Chrome, caplog: pytest.LogCaptureFixture) -> None:
  22. self.selenium = selenium
  23. self.caplog = caplog
  24. self.server_thread = None
  25. def start_server(self) -> None:
  26. '''Start the webserver in a separate thread. This is the equivalent of `ui.run()` in a normal script.'''
  27. self.server_thread = threading.Thread(target=ui.run, kwargs=self.UI_RUN_KWARGS)
  28. self.server_thread.start()
  29. @property
  30. def is_open(self) -> None:
  31. # https://stackoverflow.com/a/66150779/3419103
  32. try:
  33. self.selenium.current_url
  34. return True
  35. except:
  36. return False
  37. def stop_server(self) -> None:
  38. '''Stop the webserver.'''
  39. self.close()
  40. globals.server.should_exit = True
  41. self.server_thread.join()
  42. def open(self, path: str) -> None:
  43. if self.server_thread is None:
  44. self.start_server()
  45. start = time.time()
  46. while True:
  47. try:
  48. self.selenium.get(f'http://localhost:{PORT}{path}')
  49. break
  50. except Exception:
  51. if time.time() - start > 3:
  52. raise
  53. time.sleep(0.1)
  54. if not self.server_thread.is_alive():
  55. raise RuntimeError('The NiceGUI server has stopped running')
  56. def close(self) -> None:
  57. if self.is_open:
  58. self.selenium.close()
  59. def should_contain(self, text: str) -> None:
  60. assert self.selenium.title == text or self.find(text), f'could not find "{text}"'
  61. def should_not_contain(self, text: str) -> None:
  62. assert self.selenium.title != text
  63. with pytest.raises(AssertionError):
  64. self.find(text)
  65. def click(self, target_text: str) -> WebElement:
  66. element = self.find(target_text)
  67. try:
  68. element.click()
  69. except ElementNotInteractableException:
  70. raise AssertionError(f'Could not click on "{target_text}" on:\n{element.get_attribute("outerHTML")}')
  71. return element
  72. def click_at_position(self, element: WebElement, x: int, y: int) -> None:
  73. action = ActionChains(self.selenium)
  74. action.move_to_element_with_offset(element, x, y).click().perform()
  75. def find(self, text: str) -> WebElement:
  76. try:
  77. query = f'//*[not(self::script) and not(self::style) and contains(text(), "{text}")]'
  78. element = self.selenium.find_element(By.XPATH, query)
  79. if not element.is_displayed():
  80. self.wait(0.1) # HACK: repeat check after a short delay to avoid timing issue on fast machines
  81. if not element.is_displayed():
  82. raise AssertionError(f'Found "{text}" but it is hidden')
  83. return element
  84. except NoSuchElementException:
  85. raise AssertionError(f'Could not find "{text}"')
  86. def render_js_logs(self) -> str:
  87. console = '\n'.join(l['message'] for l in self.selenium.get_log('browser'))
  88. return f'-- console logs ---\n{console}\n---------------------'
  89. def get_tags(self, name: str) -> List[WebElement]:
  90. return self.selenium.find_elements(By.TAG_NAME, name)
  91. def get_attributes(self, tag: str, attribute: str) -> List[str]:
  92. return [t.get_attribute(attribute) for t in self.get_tags(tag)]
  93. def wait(self, t: float) -> None:
  94. time.sleep(t)
  95. def wait_for(self, text: str, *, timeout: float = 1.0) -> None:
  96. deadline = time.time() + timeout
  97. while time.time() < deadline:
  98. try:
  99. self.find(text)
  100. return
  101. except:
  102. self.wait(0.1)
  103. raise TimeoutError()
  104. def shot(self, name: str) -> None:
  105. os.makedirs(self.SCREENSHOT_DIR, exist_ok=True)
  106. filename = f'{self.SCREENSHOT_DIR}/{name}.png'
  107. print(f'Storing screenshot to {filename}')
  108. self.selenium.get_screenshot_as_file(filename)
  109. def assert_py_logger(self, level: str, message: str) -> None:
  110. try:
  111. assert self.caplog.records, f'Expected a log message'
  112. record = self.caplog.records[0]
  113. print(record.levelname, record.message)
  114. assert record.levelname.strip() == level, f'Expected "{level}" but got "{record.levelname}"'
  115. assert record.message.strip() == message, f'Expected "{message}" but got "{record.message}"'
  116. finally:
  117. self.caplog.records.clear()