screen.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import logging
  2. import os
  3. import threading
  4. import time
  5. from datetime import datetime
  6. from typing import List
  7. import pytest
  8. from bs4 import BeautifulSoup
  9. from nicegui import globals, ui
  10. from selenium import webdriver
  11. from selenium.common.exceptions import NoSuchElementException
  12. from selenium.webdriver import ActionChains
  13. from selenium.webdriver.common.by import By
  14. from selenium.webdriver.remote.webelement import WebElement
  15. from .helper import remove_prefix
  16. PORT = 3392
  17. IGNORED_CLASSES = ['row', 'column', 'q-card', 'q-field', 'q-field__label', 'q-input']
  18. class Screen():
  19. SCREENSHOT_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'screenshots')
  20. def __init__(self, selenium: webdriver.Chrome) -> None:
  21. self.selenium = selenium
  22. self.server_thread = None
  23. def start_server(self) -> None:
  24. '''Start the webserver in a separate thread. This is the equivalent of `ui.run()` in a normal script.'''
  25. self.server_thread = threading.Thread(target=ui.run, kwargs={'port': PORT, 'show': False, 'reload': False})
  26. self.server_thread.start()
  27. def stop_server(self) -> None:
  28. '''Stop the webserver.'''
  29. self.selenium.close()
  30. globals.server.should_exit = True
  31. self.server_thread.join()
  32. def open(self, path: str) -> None:
  33. if self.server_thread is None:
  34. self.start_server()
  35. start = time.time()
  36. while True:
  37. try:
  38. self.selenium.get(f'http://localhost:{PORT}{path}')
  39. break
  40. except Exception:
  41. if time.time() - start > 3:
  42. raise
  43. time.sleep(0.1)
  44. if not self.server_thread.is_alive():
  45. raise RuntimeError('The NiceGUI server has stopped running')
  46. def should_contain(self, text: str) -> None:
  47. assert self.selenium.title == text or self.find(text), \
  48. f'could not find "{text}" on:\n{self.render_content()}'
  49. def should_not_contain(self, text: str) -> None:
  50. assert self.selenium.title != text
  51. with pytest.raises(AssertionError):
  52. element = self.find(text)
  53. def click(self, target_text: str) -> WebElement:
  54. element = self.find(target_text)
  55. element.click()
  56. return element
  57. def click_at_position(self, element: WebElement, x: int = 5, y: int = 5) -> None:
  58. action = ActionChains(self.selenium)
  59. action.move_to_element_with_offset(element, x, y).click().perform()
  60. def find(self, text: str) -> WebElement:
  61. try:
  62. return self.selenium.find_element(By.XPATH, f'//*[not(self::script) and contains(text(), "{text}")]')
  63. except NoSuchElementException:
  64. raise AssertionError(f'Could not find "{text}" on:\n{self.render_content()}')
  65. def render_content(self, with_extras: bool = False) -> str:
  66. body = self.selenium.find_element(By.TAG_NAME, 'body').get_attribute('innerHTML')
  67. soup = BeautifulSoup(body, 'html.parser')
  68. self.simplify_input_tags(soup)
  69. content = ''
  70. for child in soup.find_all():
  71. is_element = False
  72. if child is None or child.name == 'script':
  73. continue
  74. depth = (len(list(child.parents)) - 3) * ' '
  75. if not child.find_all() and child.text:
  76. content += depth + child.getText()
  77. is_element = True
  78. classes = child.get('class', '')
  79. if classes:
  80. if classes[0] in ['row', 'column', 'q-card']:
  81. content += depth + remove_prefix(classes[0], 'q-')
  82. is_element = True
  83. if classes[0] == 'q-field':
  84. pass
  85. [classes.remove(c) for c in IGNORED_CLASSES if c in classes]
  86. for i, c in enumerate(classes):
  87. classes[i] = remove_prefix(c, 'q-field--')
  88. if is_element and with_extras:
  89. content += f' [class: {" ".join(classes)}]'
  90. if is_element:
  91. content += '\n'
  92. return f'Title: {self.selenium.title}\n\n{content}'
  93. def render_html(self) -> str:
  94. body = self.selenium.page_source
  95. soup = BeautifulSoup(body, 'html.parser')
  96. for element in soup.find_all():
  97. if element.name in ['script', 'style'] and len(element.text) > 10:
  98. element.string = '... removed lengthly content ...'
  99. return soup.prettify()
  100. def render_logs(self) -> str:
  101. console = '\n'.join([l['message'] for l in self.selenium.get_log('browser')])
  102. return f'-- console logs ---\n{console}\n---------------------'
  103. @staticmethod
  104. def simplify_input_tags(soup: BeautifulSoup) -> None:
  105. for element in soup.find_all(class_="q-field"):
  106. new = soup.new_tag('simple_input')
  107. name = element.find(class_='q-field__label').text
  108. placeholder = element.find(class_='q-field__native').get('placeholder')
  109. messages = element.find(class_='q-field__messages')
  110. value = element.find(class_='q-field__native').get('value')
  111. new.string = (f'{name}: ' if name else '') + (value or placeholder or '') + \
  112. (f' \u002A{messages.text}' if messages else '')
  113. new['class'] = element['class']
  114. element.replace_with(new)
  115. def get_tags(self, name: str) -> List[WebElement]:
  116. return self.selenium.find_elements(By.TAG_NAME, name)
  117. def get_attributes(self, tag: str, attribute: str) -> List[str]:
  118. return [t.get_attribute(attribute) for t in self.get_tags(tag)]
  119. def wait(self, t: float) -> None:
  120. time.sleep(t)
  121. def shot(self, name: str) -> None:
  122. os.makedirs(self.SCREENSHOT_DIR, exist_ok=True)
  123. filename = f'{self.SCREENSHOT_DIR}/{name}.png'
  124. print(f'Storing Screenshot to {filename}')
  125. self.selenium.get_screenshot_as_file(filename)