utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import os
  2. import time
  3. from selenium.webdriver import ActionChains
  4. from selenium.webdriver.common.by import By
  5. from selenium.webdriver.common.keys import Keys
  6. from selenium.webdriver.support.ui import WebDriverWait
  7. from selenium.webdriver.support import expected_conditions as EC
  8. from selenium.webdriver.remote.webelement import WebElement
  9. from contextlib import contextmanager
  10. pjoin = os.path.join
  11. def wait_for_selector(browser, selector, timeout=10, visible=False, single=False):
  12. wait = WebDriverWait(browser, timeout)
  13. if single:
  14. if visible:
  15. conditional = EC.visibility_of_element_located
  16. else:
  17. conditional = EC.presence_of_element_located
  18. else:
  19. if visible:
  20. conditional = EC.visibility_of_all_elements_located
  21. else:
  22. conditional = EC.presence_of_all_elements_located
  23. return wait.until(conditional((By.CSS_SELECTOR, selector)))
  24. class CellTypeError(ValueError):
  25. def __init__(self, message=""):
  26. self.message = message
  27. class Notebook:
  28. def __init__(self, browser):
  29. self.browser = browser
  30. self.disable_autosave_and_onbeforeunload()
  31. def __len__(self):
  32. return len(self.cells)
  33. def __getitem__(self, key):
  34. return self.cells[key]
  35. def __setitem__(self, key, item):
  36. if isinstance(key, int):
  37. self.edit_cell(index=key, content=item, render=False)
  38. # TODO: re-add slicing support, handle general python slicing behaviour
  39. # includes: overwriting the entire self.cells object if you do
  40. # self[:] = []
  41. # elif isinstance(key, slice):
  42. # indices = (self.index(cell) for cell in self[key])
  43. # for k, v in zip(indices, item):
  44. # self.edit_cell(index=k, content=v, render=False)
  45. def __iter__(self):
  46. return (cell for cell in self.cells)
  47. @property
  48. def body(self):
  49. return self.browser.find_element_by_tag_name("body")
  50. @property
  51. def cells(self):
  52. """Gets all cells once they are visible.
  53. """
  54. return self.browser.find_elements_by_class_name("cell")
  55. @property
  56. def current_index(self):
  57. return self.index(self.current_cell)
  58. def index(self, cell):
  59. return self.cells.index(cell)
  60. def disable_autosave_and_onbeforeunload(self):
  61. """Disable request to save before closing window and autosave.
  62. This is most easily done by using js directly.
  63. """
  64. self.browser.execute_script("window.onbeforeunload = null;")
  65. self.browser.execute_script("Jupyter.notebook.set_autosave_interval(0)")
  66. def to_command_mode(self):
  67. """Changes us into command mode on currently focused cell
  68. """
  69. self.body.send_keys(Keys.ESCAPE)
  70. self.browser.execute_script("return Jupyter.notebook.handle_command_mode("
  71. "Jupyter.notebook.get_cell("
  72. "Jupyter.notebook.get_edit_index()))")
  73. def focus_cell(self, index=0):
  74. cell = self.cells[index]
  75. cell.click()
  76. self.to_command_mode()
  77. self.current_cell = cell
  78. def find_and_replace(self, index=0, find_txt='', replace_txt=''):
  79. self.focus_cell(index)
  80. self.to_command_mode()
  81. self.body.send_keys('f')
  82. wait_for_selector(self.browser, "#find-and-replace", single=True)
  83. self.browser.find_element_by_id("findreplace_allcells_btn").click()
  84. self.browser.find_element_by_id("findreplace_find_inp").send_keys(find_txt)
  85. self.browser.find_element_by_id("findreplace_replace_inp").send_keys(replace_txt)
  86. self.browser.find_element_by_id("findreplace_replaceall_btn").click()
  87. def convert_cell_type(self, index=0, cell_type="code"):
  88. # TODO add check to see if it is already present
  89. self.focus_cell(index)
  90. cell = self.cells[index]
  91. if cell_type == "markdown":
  92. self.current_cell.send_keys("m")
  93. elif cell_type == "raw":
  94. self.current_cell.send_keys("r")
  95. elif cell_type == "code":
  96. self.current_cell.send_keys("y")
  97. else:
  98. raise CellTypeError(("{} is not a valid cell type,"
  99. "use 'code', 'markdown', or 'raw'").format(cell_type))
  100. self.wait_for_stale_cell(cell)
  101. self.focus_cell(index)
  102. return self.current_cell
  103. def wait_for_stale_cell(self, cell):
  104. """ This is needed to switch a cell's mode and refocus it, or to render it.
  105. Warning: there is currently no way to do this when changing between
  106. markdown and raw cells.
  107. """
  108. wait = WebDriverWait(self.browser, 10)
  109. element = wait.until(EC.staleness_of(cell))
  110. def get_cells_contents(self):
  111. JS = 'return Jupyter.notebook.get_cells().map(function(c) {return c.get_text();})'
  112. return self.browser.execute_script(JS)
  113. def get_cell_contents(self, index=0, selector='div .CodeMirror-code'):
  114. return self.cells[index].find_element_by_css_selector(selector).text
  115. def set_cell_metadata(self, index, key, value):
  116. JS = 'Jupyter.notebook.get_cell({}).metadata.{} = {}'.format(index, key, value)
  117. return self.browser.execute_script(JS)
  118. def get_cell_type(self, index=0):
  119. JS = 'return Jupyter.notebook.get_cell({}).cell_type'.format(index)
  120. return self.browser.execute_script(JS)
  121. def set_cell_input_prompt(self, index, prmpt_val):
  122. JS = 'Jupyter.notebook.get_cell({}).set_input_prompt({})'.format(index, prmpt_val)
  123. self.browser.execute_script(JS)
  124. def edit_cell(self, cell=None, index=0, content="", render=False):
  125. """Set the contents of a cell to *content*, by cell object or by index
  126. """
  127. if cell is not None:
  128. index = self.index(cell)
  129. self.focus_cell(index)
  130. # Select & delete anything already in the cell
  131. self.current_cell.send_keys(Keys.ENTER)
  132. ctrl(self.browser, 'a')
  133. self.current_cell.send_keys(Keys.DELETE)
  134. for line_no, line in enumerate(content.splitlines()):
  135. if line_no != 0:
  136. self.current_cell.send_keys(Keys.ENTER, "\n")
  137. self.current_cell.send_keys(Keys.ENTER, line)
  138. if render:
  139. self.execute_cell(self.current_index)
  140. def execute_cell(self, cell_or_index=None):
  141. if isinstance(cell_or_index, int):
  142. index = cell_or_index
  143. elif isinstance(cell_or_index, WebElement):
  144. index = self.index(cell_or_index)
  145. else:
  146. raise TypeError("execute_cell only accepts a WebElement or an int")
  147. self.focus_cell(index)
  148. self.current_cell.send_keys(Keys.CONTROL, Keys.ENTER)
  149. def add_cell(self, index=-1, cell_type="code", content=""):
  150. self.focus_cell(index)
  151. self.current_cell.send_keys("b")
  152. new_index = index + 1 if index >= 0 else index
  153. if content:
  154. self.edit_cell(index=index, content=content)
  155. if cell_type != 'code':
  156. self.convert_cell_type(index=new_index, cell_type=cell_type)
  157. def delete_cell(self, index):
  158. self.focus_cell(index)
  159. self.to_command_mode()
  160. self.current_cell.send_keys('dd')
  161. def add_markdown_cell(self, index=-1, content="", render=True):
  162. self.add_cell(index, cell_type="markdown")
  163. self.edit_cell(index=index, content=content, render=render)
  164. def append(self, *values, cell_type="code"):
  165. for i, value in enumerate(values):
  166. if isinstance(value, str):
  167. self.add_cell(cell_type=cell_type,
  168. content=value)
  169. else:
  170. raise TypeError("Don't know how to add cell from %r" % value)
  171. def extend(self, values):
  172. self.append(*values)
  173. def run_all(self):
  174. for cell in self:
  175. self.execute_cell(cell)
  176. def trigger_keydown(self, keys):
  177. trigger_keystrokes(self.body, keys)
  178. @classmethod
  179. def new_notebook(cls, browser, kernel_name='kernel-python3'):
  180. with new_window(browser, selector=".cell"):
  181. select_kernel(browser, kernel_name=kernel_name)
  182. return cls(browser)
  183. def select_kernel(browser, kernel_name='kernel-python3'):
  184. """Clicks the "new" button and selects a kernel from the options.
  185. """
  186. wait = WebDriverWait(browser, 10)
  187. new_button = wait.until(EC.element_to_be_clickable((By.ID, "new-dropdown-button")))
  188. new_button.click()
  189. kernel_selector = '#{} a'.format(kernel_name)
  190. kernel = wait_for_selector(browser, kernel_selector, single=True)
  191. kernel.click()
  192. @contextmanager
  193. def new_window(browser, selector=None):
  194. """Contextmanager for switching to & waiting for a window created.
  195. This context manager gives you the ability to create a new window inside
  196. the created context and it will switch you to that new window.
  197. If you know a CSS selector that can be expected to appear on the window,
  198. then this utility can wait on that selector appearing on the page before
  199. releasing the context.
  200. Usage example:
  201. from notebook.tests.selenium.utils import new_window, Notebook
  202. ⋮ # something that creates a browser object
  203. with new_window(browser, selector=".cell"):
  204. select_kernel(browser, kernel_name=kernel_name)
  205. nb = Notebook(browser)
  206. """
  207. initial_window_handles = browser.window_handles
  208. yield
  209. new_window_handle = next(window for window in browser.window_handles
  210. if window not in initial_window_handles)
  211. browser.switch_to_window(new_window_handle)
  212. if selector is not None:
  213. wait_for_selector(browser, selector)
  214. def shift(browser, k):
  215. """Send key combination Shift+(k)"""
  216. trigger_keystrokes(browser, "shift-%s"%k)
  217. def ctrl(browser, k):
  218. """Send key combination Ctrl+(k)"""
  219. trigger_keystrokes(browser, "control-%s"%k)
  220. def trigger_keystrokes(browser, *keys):
  221. """ Send the keys in sequence to the browser.
  222. Handles following key combinations
  223. 1. with modifiers eg. 'control-alt-a', 'shift-c'
  224. 2. just modifiers eg. 'alt', 'esc'
  225. 3. non-modifiers eg. 'abc'
  226. Modifiers : http://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html
  227. """
  228. for each_key_combination in keys:
  229. keys = each_key_combination.split('-')
  230. if len(keys) > 1: # key has modifiers eg. control, alt, shift
  231. modifiers_keys = [getattr(Keys, x.upper()) for x in keys[:-1]]
  232. ac = ActionChains(browser)
  233. for i in modifiers_keys: ac = ac.key_down(i)
  234. ac.send_keys(keys[-1])
  235. for i in modifiers_keys[::-1]: ac = ac.key_up(i)
  236. ac.perform()
  237. else: # single key stroke. Check if modifier eg. "up"
  238. browser.send_keys(getattr(Keys, keys[0].upper(), keys[0]))