diff --git a/notebook/tests/selenium/conftest.py b/notebook/tests/selenium/conftest.py index d7227b12b..232b5bc4a 100644 --- a/notebook/tests/selenium/conftest.py +++ b/notebook/tests/selenium/conftest.py @@ -12,6 +12,7 @@ from selenium.webdriver import Firefox, Remote, Chrome pjoin = os.path.join + def _wait_for_server(proc, info_file_path): """Wait 30 seconds for the notebook server to start""" for i in range(300): @@ -28,6 +29,7 @@ def _wait_for_server(proc, info_file_path): time.sleep(0.1) raise RuntimeError("Didn't find %s in 30 seconds", info_file_path) + @pytest.fixture(scope='session') def notebook_server(): info = {} @@ -50,10 +52,11 @@ def notebook_server(): # run with a base URL that would be escaped, # to test that we don't double-escape URLs '--NotebookApp.base_url=/a@b/', - ] + ] print("command=", command) proc = info['popen'] = Popen(command, cwd=nbdir, env=env) - info_file_path = pjoin(td, 'jupyter_runtime', 'nbserver-%i.json' % proc.pid) + info_file_path = pjoin(td, 'jupyter_runtime', + 'nbserver-%i.json' % proc.pid) info.update(_wait_for_server(proc, info_file_path)) print("Notebook server info:", info) @@ -63,26 +66,38 @@ def notebook_server(): requests.post(urljoin(info['url'], 'api/shutdown'), headers={'Authorization': 'token '+info['token']}) + +def make_sauce_driver(): + """This function helps travis create a driver on Sauce Labs. + + This function will err if used without specifying the variables expected + in that context. + """ + + username = os.environ["SAUCE_USERNAME"] + access_key = os.environ["SAUCE_ACCESS_KEY"] + capabilities = { + "tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"], + "build": os.environ["TRAVIS_BUILD_NUMBER"], + "tags": [os.environ['TRAVIS_PYTHON_VERSION'], 'CI'], + "platform": "Windows 10", + "browserName": os.environ['JUPYTER_TEST_BROWSER'], + "version": "latest", + } + if capabilities['browserName'] == 'firefox': + # Attempt to work around issue where browser loses authentication + capabilities['version'] = '57.0' + hub_url = "%s:%s@localhost:4445" % (username, access_key) + print("Connecting remote driver on Sauce Labs") + driver = Remote(desired_capabilities=capabilities, + command_executor="http://%s/wd/hub" % hub_url) + return driver + + @pytest.fixture(scope='session') def selenium_driver(): if os.environ.get('SAUCE_USERNAME'): - username = os.environ["SAUCE_USERNAME"] - access_key = os.environ["SAUCE_ACCESS_KEY"] - capabilities = { - "tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"], - "build": os.environ["TRAVIS_BUILD_NUMBER"], - "tags": [os.environ['TRAVIS_PYTHON_VERSION'], 'CI'], - "platform": "Windows 10", - "browserName": os.environ['JUPYTER_TEST_BROWSER'], - "version": "latest", - } - if capabilities['browserName'] == 'firefox': - # Attempt to work around issue where browser loses authentication - capabilities['version'] = '57.0' - hub_url = "%s:%s@localhost:4445" % (username, access_key) - print("Connecting remote driver on Sauce Labs") - driver = Remote(desired_capabilities=capabilities, - command_executor="http://%s/wd/hub" % hub_url) + driver = make_sauce_driver() elif os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': driver = Chrome() else: @@ -93,7 +108,8 @@ def selenium_driver(): # Teardown driver.quit() -@pytest.fixture + +@pytest.fixture(scope='module') def authenticated_browser(selenium_driver, notebook_server): selenium_driver.jupyter_server_info = notebook_server selenium_driver.get("{url}?token={token}".format(**notebook_server)) diff --git a/notebook/tests/selenium/quick_selenium.py b/notebook/tests/selenium/quick_selenium.py new file mode 100644 index 000000000..10e46e1f3 --- /dev/null +++ b/notebook/tests/selenium/quick_selenium.py @@ -0,0 +1,53 @@ +"""Utilities for driving Selenium interactively to develop tests. + +These are not used in the tests themselves - rather, the developer writing tests +can use them to experiment with Selenium. +""" +from selenium.webdriver import Firefox + +from notebook.tests.selenium.utils import Notebook +from notebook.notebookapp import list_running_servers + +class NoServerError(Exception): + + def __init__(self, message): + self.message = message + +def quick_driver(lab=False): + """Quickly create a selenium driver pointing at an active noteboook server. + + Usage example: + + from notebook.tests.selenium.quick_selenium import quick_driver + driver = quick_driver + + Note: you need to manually close the driver that opens with driver.quit() + """ + try: + server = list(list_running_servers())[0] + except IndexError as e: + raise NoServerError('You need a server running before you can run ' + 'this command') + driver = Firefox() + auth_url = '{url}?token={token}'.format(**server) + driver.get(auth_url) + + # If this redirects us to a lab page and we don't want that; + # then we need to redirect ourselves to the classic notebook view + if driver.current_url.endswith('/lab') and not lab: + driver.get(driver.current_url.rstrip('lab')+'tree') + return driver + + +def quick_notebook(): + """Quickly create a new classic notebook in a selenium driver + + + Usage example: + + from notebook.tests.selenium.quick_selenium import quick_notebook + nb = quick_notebook() + + Note: you need to manually close the driver that opens with nb.browser.quit() + """ + return Notebook.new_notebook(quick_driver()) diff --git a/notebook/tests/selenium/test_dashboard_nav.py b/notebook/tests/selenium/test_dashboard_nav.py index 9b588c75e..8e0997902 100644 --- a/notebook/tests/selenium/test_dashboard_nav.py +++ b/notebook/tests/selenium/test_dashboard_nav.py @@ -5,6 +5,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from notebook.utils import url_path_join +from notebook.tests.selenium.utils import wait_for_selector pjoin = os.path.join @@ -40,7 +41,6 @@ def get_list_items(browser): 'element': a, } for a in browser.find_elements_by_class_name('item_link')] - def only_dir_links(browser): """Return only links that point at other directories in the tree @@ -49,12 +49,6 @@ def only_dir_links(browser): return [i for i in items if url_in_tree(browser, i['link']) and i['label'] != '..'] - -def wait_for_selector(browser, selector, timeout=10): - wait = WebDriverWait(browser, timeout) - return wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, selector))) - - def test_items(authenticated_browser): visited_dict = {} # Going down the tree to collect links diff --git a/notebook/tests/selenium/test_markdown.py b/notebook/tests/selenium/test_markdown.py new file mode 100644 index 000000000..c71ef8a17 --- /dev/null +++ b/notebook/tests/selenium/test_markdown.py @@ -0,0 +1,43 @@ +import os + +import pytest +from selenium.webdriver.common.keys import Keys + +from .utils import wait_for_selector, Notebook + +pjoin = os.path.join + + +@pytest.fixture +def notebook(authenticated_browser): + return Notebook.new_notebook(authenticated_browser) + + +def get_rendered_contents(nb): + cl = ["text_cell", "render"] + rendered_cells = [cell.find_element_by_class_name("text_cell_render") + for cell in nb.cells + if all([c in cell.get_attribute("class") for c in cl])] + return [x.get_attribute('innerHTML').strip() + for x in rendered_cells + if x is not None] + + +def test_markdown_cell(notebook): + nb = notebook + cell_text = ["# Foo", + '**Bar**', + '*Baz*', + '```\nx = 1\n```', + '```aaaa\nx = 1\n```', + ] + expected_contents = ['
Bar
', + 'Baz
', + 'x = 1\n
',
+ 'x = 1\n
'
+ ]
+ nb.append(*cell_text, cell_type="markdown")
+ nb.run_all()
+ rendered_contents = get_rendered_contents(nb)
+ assert rendered_contents == expected_contents
diff --git a/notebook/tests/selenium/utils.py b/notebook/tests/selenium/utils.py
new file mode 100644
index 000000000..3aed1d554
--- /dev/null
+++ b/notebook/tests/selenium/utils.py
@@ -0,0 +1,222 @@
+import os
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.remote.webelement import WebElement
+
+from contextlib import contextmanager
+
+pjoin = os.path.join
+
+
+def wait_for_selector(browser, selector, timeout=10, visible=False, single=False):
+ wait = WebDriverWait(browser, timeout)
+ if single:
+ if visible:
+ conditional = EC.visibility_of_element_located
+ else:
+ conditional = EC.presence_of_element_located
+ else:
+ if visible:
+ conditional = EC.visibility_of_all_elements_located
+ else:
+ conditional = EC.presence_of_all_elements_located
+ return wait.until(conditional((By.CSS_SELECTOR, selector)))
+
+
+class CellTypeError(ValueError):
+
+ def __init__(self, message=""):
+ self.message = message
+
+
+class Notebook:
+
+ def __init__(self, browser):
+ self.browser = browser
+ self.disable_autosave_and_onbeforeunload()
+
+ def __len__(self):
+ return len(self.cells)
+
+ def __getitem__(self, key):
+ return self.cells[key]
+
+ def __setitem__(self, key, item):
+ if isinstance(key, int):
+ self.edit_cell(index=key, content=item, render=False)
+ # TODO: re-add slicing support, handle general python slicing behaviour
+ # includes: overwriting the entire self.cells object if you do
+ # self[:] = []
+ # elif isinstance(key, slice):
+ # indices = (self.index(cell) for cell in self[key])
+ # for k, v in zip(indices, item):
+ # self.edit_cell(index=k, content=v, render=False)
+
+ def __iter__(self):
+ return (cell for cell in self.cells)
+
+ @property
+ def body(self):
+ return self.browser.find_element_by_tag_name("body")
+
+ @property
+ def cells(self):
+ """Gets all cells once they are visible.
+
+ """
+ return self.browser.find_elements_by_class_name("cell")
+
+ @property
+ def current_index(self):
+ return self.index(self.current_cell)
+
+ def index(self, cell):
+ return self.cells.index(cell)
+
+ def disable_autosave_and_onbeforeunload(self):
+ """Disable request to save before closing window and autosave.
+
+ This is most easily done by using js directly.
+ """
+ self.browser.execute_script("window.onbeforeunload = null;")
+ self.browser.execute_script("Jupyter.notebook.set_autosave_interval(0)")
+
+ def to_command_mode(self):
+ """Changes us into command mode on currently focused cell
+
+ """
+ self.body.send_keys(Keys.ESCAPE)
+ self.browser.execute_script("return Jupyter.notebook.handle_command_mode("
+ "Jupyter.notebook.get_cell("
+ "Jupyter.notebook.get_edit_index()))")
+
+ def focus_cell(self, index=0):
+ cell = self.cells[index]
+ cell.click()
+ self.to_command_mode()
+ self.current_cell = cell
+
+ def convert_cell_type(self, index=0, cell_type="code"):
+ # TODO add check to see if it is already present
+ self.focus_cell(index)
+ cell = self.cells[index]
+ if cell_type == "markdown":
+ self.current_cell.send_keys("m")
+ elif cell_type == "raw":
+ self.current_cell.send_keys("r")
+ elif cell_type == "code":
+ self.current_cell.send_keys("y")
+ else:
+ raise CellTypeError(("{} is not a valid cell type,"
+ "use 'code', 'markdown', or 'raw'").format(cell_type))
+
+ self.wait_for_stale_cell(cell)
+ self.focus_cell(index)
+ return self.current_cell
+
+ def wait_for_stale_cell(self, cell):
+ """ This is needed to switch a cell's mode and refocus it, or to render it.
+
+ Warning: there is currently no way to do this when changing between
+ markdown and raw cells.
+ """
+ wait = WebDriverWait(self.browser, 10)
+ element = wait.until(EC.staleness_of(cell))
+
+ def edit_cell(self, cell=None, index=0, content="", render=False):
+ if cell is not None:
+ index = self.index(cell)
+ self.focus_cell(index)
+
+ for line_no, line in enumerate(content.splitlines()):
+ if line_no != 0:
+ self.current_cell.send_keys(Keys.ENTER, "\n")
+ self.current_cell.send_keys(Keys.ENTER, line)
+ if render:
+ self.execute_cell(self.current_index)
+
+ def execute_cell(self, cell_or_index=None):
+ if isinstance(cell_or_index, int):
+ index = cell_or_index
+ elif isinstance(cell_or_index, WebElement):
+ index = self.index(cell_or_index)
+ else:
+ raise TypeError("execute_cell only accepts a WebElement or an int")
+ self.focus_cell(index)
+ self.current_cell.send_keys(Keys.CONTROL, Keys.ENTER)
+
+ def add_cell(self, index=-1, cell_type="code", content=""):
+ self.focus_cell(index)
+ self.current_cell.send_keys("b")
+ new_index = index + 1 if index >= 0 else index
+ if content:
+ self.edit_cell(index=index, content=content)
+ self.convert_cell_type(index=new_index, cell_type=cell_type)
+
+ def add_markdown_cell(self, index=-1, content="", render=True):
+ self.add_cell(index, cell_type="markdown")
+ self.edit_cell(index=index, content=content, render=render)
+
+ def append(self, *values, cell_type="code"):
+ for i, value in enumerate(values):
+ if isinstance(value, str):
+ self.add_cell(cell_type=cell_type,
+ content=value)
+ else:
+ raise TypeError("Don't know how to add cell from %r" % value)
+
+ def extend(self, values):
+ self.append(*values)
+
+ def run_all(self):
+ for cell in self:
+ self.execute_cell(cell)
+
+ @classmethod
+ def new_notebook(cls, browser, kernel_name='kernel-python3'):
+ with new_window(browser, selector=".cell"):
+ select_kernel(browser, kernel_name=kernel_name)
+ return cls(browser)
+
+
+def select_kernel(browser, kernel_name='kernel-python3'):
+ """Clicks the "new" button and selects a kernel from the options.
+ """
+ new_button = wait_for_selector(browser, "#new-buttons", single=True)
+ new_button.click()
+ kernel_selector = '#{} a'.format(kernel_name)
+ kernel = wait_for_selector(browser, kernel_selector, single=True)
+ kernel.click()
+
+@contextmanager
+def new_window(browser, selector=None):
+ """Contextmanager for switching to & waiting for a window created.
+
+ This context manager gives you the ability to create a new window inside
+ the created context and it will switch you to that new window.
+
+ If you know a CSS selector that can be expected to appear on the window,
+ then this utility can wait on that selector appearing on the page before
+ releasing the context.
+
+ Usage example:
+
+ from notebook.tests.selenium.utils import new_window, Notebook
+
+ ⋮ # something that creates a browser object
+
+ with new_window(browser, selector=".cell"):
+ select_kernel(browser, kernel_name=kernel_name)
+ nb = Notebook(browser)
+
+ """
+ initial_window_handles = browser.window_handles
+ yield
+ new_window_handle = next(window for window in browser.window_handles
+ if window not in initial_window_handles)
+ browser.switch_to_window(new_window_handle)
+ if selector is not None:
+ wait_for_selector(browser, selector)