mirror of
https://github.com/jupyter/notebook.git
synced 2025-01-18 11:55:46 +08:00
Merge pull request #3458 from mpacer/selenium_utils
Selenium utils + markdown rendering tests
This commit is contained in:
commit
faa0cab302
@ -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))
|
||||
|
53
notebook/tests/selenium/quick_selenium.py
Normal file
53
notebook/tests/selenium/quick_selenium.py
Normal file
@ -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())
|
@ -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
|
||||
|
43
notebook/tests/selenium/test_markdown.py
Normal file
43
notebook/tests/selenium/test_markdown.py
Normal file
@ -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 = ['<h1 id="Foo">Foo<a class="anchor-link" href="#Foo">¶</a></h1>',
|
||||
'<p><strong>Bar</strong></p>',
|
||||
'<p><em>Baz</em></p>',
|
||||
'<pre><code>x = 1\n</code></pre>',
|
||||
'<pre><code class="cm-s-ipython language-aaaa">x = 1\n</code></pre>'
|
||||
]
|
||||
nb.append(*cell_text, cell_type="markdown")
|
||||
nb.run_all()
|
||||
rendered_contents = get_rendered_contents(nb)
|
||||
assert rendered_contents == expected_contents
|
222
notebook/tests/selenium/utils.py
Normal file
222
notebook/tests/selenium/utils.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user