screen.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import threading
  2. import time
  3. import pytest
  4. from bs4 import BeautifulSoup
  5. from nicegui import globals, ui
  6. from selenium import webdriver
  7. from selenium.common.exceptions import NoSuchElementException
  8. from selenium.webdriver.remote.webelement import WebElement
  9. PORT = 3392
  10. IGNORED_CLASSES = ['row', 'column', 'q-card', 'q-field', 'q-field__label', 'q-input']
  11. class Screen():
  12. def __init__(self, selenium: webdriver.Chrome) -> None:
  13. self.selenium = selenium
  14. self.server_thread = None
  15. def start_server(self) -> None:
  16. '''Start the webserver in a separate thread. This is the equivalent of `ui.run()` in a normal script.'''
  17. self.server_thread = threading.Thread(target=ui.run, kwargs={'port': PORT, 'show': False, 'reload': False})
  18. self.server_thread.start()
  19. def stop_server(self) -> None:
  20. '''Stop the webserver.'''
  21. self.selenium.close()
  22. globals.server.should_exit = True
  23. self.server_thread.join()
  24. def open(self, path: str) -> None:
  25. if self.server_thread is None:
  26. self.start_server()
  27. start = time.time()
  28. while True:
  29. try:
  30. self.selenium.get(f'http://localhost:{PORT}{path}')
  31. break
  32. except Exception:
  33. if time.time() - start > 3:
  34. raise
  35. time.sleep(0.1)
  36. if not self.server_thread.is_alive():
  37. raise RuntimeError('The NiceGUI server has stopped running')
  38. def should_contain(self, text: str) -> None:
  39. assert self.selenium.title == text or self.find(text), \
  40. f'could not find "{text}" on:\n{self.render_content()}'
  41. def should_not_contain(self, text: str) -> None:
  42. assert self.selenium.title != text
  43. with pytest.raises(AssertionError):
  44. element = self.find(text)
  45. print(element.get_attribute('outerHTML'))
  46. def click(self, target_text: str) -> None:
  47. self.find(target_text).click()
  48. def find(self, text: str) -> WebElement:
  49. try:
  50. return self.selenium.find_element_by_xpath(f'//*[contains(text(),"{text}")]')
  51. except NoSuchElementException:
  52. raise AssertionError(f'Could not find "{text}" on:\n{self.render_content()}')
  53. def render_content(self, with_extras: bool = False) -> str:
  54. body = self.selenium.find_element_by_tag_name('body').get_attribute('innerHTML')
  55. soup = BeautifulSoup(body, 'html.parser')
  56. self.simplify_input_tags(soup)
  57. content = ''
  58. for child in soup.find_all():
  59. is_element = False
  60. if child is None or child.name == 'script':
  61. continue
  62. depth = (len(list(child.parents)) - 3) * ' '
  63. if not child.find_all() and child.text:
  64. content += depth + child.getText()
  65. is_element = True
  66. classes = child.get('class', '')
  67. if classes:
  68. if classes[0] in ['row', 'column', 'q-card']:
  69. content += depth + classes[0].removeprefix('q-')
  70. is_element = True
  71. if classes[0] == 'q-field':
  72. pass
  73. [classes.remove(c) for c in IGNORED_CLASSES if c in classes]
  74. for i, c in enumerate(classes):
  75. classes[i] = c.removeprefix('q-field--')
  76. if is_element and with_extras:
  77. content += f' [class: {" ".join(classes)}]'
  78. if is_element:
  79. content += '\n'
  80. return f'Title: {self.selenium.title}\n\n{content}'
  81. @staticmethod
  82. def simplify_input_tags(soup: BeautifulSoup) -> None:
  83. for element in soup.find_all(class_="q-field"):
  84. print(element.prettify())
  85. new = soup.new_tag('simple_input')
  86. name = element.find(class_='q-field__label').text
  87. placeholder = element.find(class_='q-field__native').get('placeholder')
  88. value = element.find(class_='q-field__native').get('value')
  89. new.string = (f'{name}: ' if name else '') + (value or placeholder or '')
  90. new['class'] = element['class']
  91. element.replace_with(new)
  92. def get_tags(self, name: str) -> list[WebElement]:
  93. return self.selenium.find_elements_by_tag_name(name)
  94. def get_attributes(self, tag: str, attribute: str) -> list[str]:
  95. return [t.get_attribute(attribute) for t in self.get_tags(tag)]
  96. def wait(self, t: float) -> None:
  97. time.sleep(t)