diff --git a/.github/workflows/flaky-selenium.yml b/.github/workflows/playwright.yml similarity index 53% rename from .github/workflows/flaky-selenium.yml rename to .github/workflows/playwright.yml index 2a418dc96..764137dfb 100644 --- a/.github/workflows/flaky-selenium.yml +++ b/.github/workflows/playwright.yml @@ -1,4 +1,4 @@ -name: Flaky Selenium Tests +name: Playwright Tests on: push: @@ -12,14 +12,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos] - python-version: [ '3.7', '3.8', '3.9'] - exclude: - - os: ubuntu - python-version: '3.7' - - os: ubuntu - python-version: '3.9' - - os: macos - python-version: '3.8' + python-version: [ '3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout uses: actions/checkout@v2 @@ -41,13 +34,8 @@ jobs: - name: Install Python dependencies run: | - python -m pip install -U pip setuptools wheel - pip install --upgrade selenium - pip install pytest - pip install .[test] + python tools/install_pydeps.py - - name: Run Tests + - name: Run Playwright Tests run: | - export JUPYTER_TEST_BROWSER=firefox - export MOZ_HEADLESS=1 - pytest -sv nbclassic/tests/selenium + pytest -sv nbclassic/tests/end_to_end diff --git a/.github/workflows/selenium.yml b/.github/workflows/selenium.yml deleted file mode 100644 index 83086c427..000000000 --- a/.github/workflows/selenium.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Selenium Tests - -on: - push: - branches: '*' - pull_request: - branches: '*' -jobs: - build: - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos] - python-version: [ '3.7', '3.8', '3.9', '3.10' ] - exclude: - - os: ubuntu - python-version: '3.8' - - os: macos - python-version: '3.7' - - os: macos - python-version: '3.9' - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x64' - - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: '12.x' - - - name: Install JS - run: | - npm install - - - name: Install Python dependencies - run: | - python -m pip install -U pip setuptools wheel - pip install --upgrade selenium - pip install pytest - pip install .[test] - - - name: Run Tests - run: | - export JUPYTER_TEST_BROWSER=firefox - export MOZ_HEADLESS=1 - pytest -sv nbclassic/tests/selenium diff --git a/nbclassic/tests/selenium/__init__.py b/nbclassic/tests/end_to_end/__init__.py similarity index 100% rename from nbclassic/tests/selenium/__init__.py rename to nbclassic/tests/end_to_end/__init__.py diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py new file mode 100644 index 000000000..a45309ffa --- /dev/null +++ b/nbclassic/tests/end_to_end/conftest.py @@ -0,0 +1,162 @@ +"""Fixtures for pytest/playwright end_to_end tests.""" + + +import datetime +import os +import json +import sys +import time +from os.path import join as pjoin +from subprocess import Popen +from tempfile import mkstemp +from urllib.parse import urljoin + +import pytest +import requests +from testpath.tempdir import TemporaryDirectory + +import nbformat +from nbformat.v4 import new_notebook, new_code_cell +from .utils import NotebookFrontend, BROWSER_CONTEXT, BROWSER_OBJ, TREE_PAGE, SERVER_INFO + + +def _wait_for_server(proc, info_file_path): + """Wait 30 seconds for the notebook server to start""" + for i in range(300): + if proc.poll() is not None: + raise RuntimeError("Notebook server failed to start") + if os.path.exists(info_file_path): + try: + with open(info_file_path) as f: + return json.load(f) + except ValueError: + # If the server is halfway through writing the file, we may + # get invalid JSON; it should be ready next iteration. + pass + time.sleep(0.1) + raise RuntimeError("Didn't find %s in 30 seconds", info_file_path) + + +@pytest.fixture(scope='function') +def notebook_server(): + info = {} + with TemporaryDirectory() as td: + nbdir = info['nbdir'] = pjoin(td, 'notebooks') + os.makedirs(pjoin(nbdir, 'sub ∂ir1', 'sub ∂ir 1a')) + os.makedirs(pjoin(nbdir, 'sub ∂ir2', 'sub ∂ir 1b')) + + info['extra_env'] = { + 'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'), + 'JUPYTER_RUNTIME_DIR': pjoin(td, 'jupyter_runtime'), + 'IPYTHONDIR': pjoin(td, 'ipython'), + } + env = os.environ.copy() + env.update(info['extra_env']) + + command = [sys.executable, '-m', 'nbclassic', + '--no-browser', + '--notebook-dir', nbdir, + # run with a base URL that would be escaped, + # to test that we don't double-escape URLs + '--ServerApp.base_url=/a@b/', + ] + print("command=", command) + proc = info['popen'] = Popen(command, cwd=nbdir, env=env) + info_file_path = pjoin(td, 'jupyter_runtime', + f'jpserver-{proc.pid:d}.json') + info.update(_wait_for_server(proc, info_file_path)) + + print("Notebook server info:", info) + yield info + + # Shut the server down + requests.post(urljoin(info['url'], 'api/shutdown'), + headers={'Authorization': 'token '+info['token']}) + + +@pytest.fixture(scope='function') +def playwright_browser(playwright): + start = datetime.datetime.now() + while (datetime.datetime.now() - start).seconds < 30: + try: + if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': + browser = playwright.chromium.launch() + else: + browser = playwright.firefox.launch() + break + except Exception: + time.sleep(.2) + + yield browser + + # Teardown + browser.close() + + +@pytest.fixture(scope='function') +def authenticated_browser_data(playwright_browser, notebook_server): + browser_obj = playwright_browser + browser_context = browser_obj.new_context() + browser_context.jupyter_server_info = notebook_server + tree_page = browser_context.new_page() + tree_page.goto("{url}?token={token}".format(**notebook_server)) + + auth_browser_data = { + BROWSER_CONTEXT: browser_context, + TREE_PAGE: tree_page, + SERVER_INFO: notebook_server, + BROWSER_OBJ: browser_obj, + } + + return auth_browser_data + + +@pytest.fixture(scope='function') +def notebook_frontend(authenticated_browser_data): + yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data) + + +@pytest.fixture(scope='function') +def prefill_notebook(playwright_browser, notebook_server): + browser_obj = playwright_browser + browser_context = browser_obj.new_context() + # playwright_browser is the browser_context, + # notebook_server is the server with directories + + # the return of function inner takes in a dictionary of strings to populate cells + def inner(cells): + cells = [new_code_cell(c) if isinstance(c, str) else c + for c in cells] + # new_notebook is an nbformat function that is imported so that it can create a + # notebook that is formatted as it needs to be + nb = new_notebook(cells=cells) + + # Create temporary file directory and store it's reference as well as the path + fd, path = mkstemp(dir=notebook_server['nbdir'], suffix='.ipynb') + + # Open the file and write the format onto the file + with open(fd, 'w', encoding='utf-8') as f: + nbformat.write(nb, f) + + # Grab the name of the file + fname = os.path.basename(path) + + # Add the notebook server as a property of the playwright browser with the name jupyter_server_info + browser_context.jupyter_server_info = notebook_server + # Open a new page in the browser and refer to it as the tree page + tree_page = browser_context.new_page() + + # Navigate that page to the base URL page AKA the tree page + tree_page.goto("{url}?token={token}".format(**notebook_server)) + + auth_browser_data = { + BROWSER_CONTEXT: browser_context, + TREE_PAGE: tree_page, + SERVER_INFO: notebook_server, + BROWSER_OBJ: browser_obj + } + + return NotebookFrontend.new_notebook_frontend(auth_browser_data, existing_file_name=fname) + + # Return the function that will take in the dict of code strings + return inner diff --git a/nbclassic/tests/end_to_end/manual_test_prototyper.py b/nbclassic/tests/end_to_end/manual_test_prototyper.py new file mode 100644 index 000000000..8ac816efa --- /dev/null +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -0,0 +1,30 @@ +"""Test basic cell execution methods, related shortcuts, and error modes + +Run this manually: + # Normal pytest run + pytest nbclassic/tests/end_to_end/test_interrupt.py + # with playwright debug (run and poke around in the web console) + PWDEBUG=1 pytest -s nbclassic/tests/end_to_end/test_interrupt.py +""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +# # Use/uncomment this for manual test prototytping +# # (the test suite will run this if it's uncommented) +# def test_do_something(notebook_frontend): +# # Do something with the notebook_frontend here +# notebook_frontend.add_cell() +# notebook_frontend.add_cell() +# assert len(notebook_frontend.cells) == 3 +# +# notebook_frontend.delete_all_cells() +# assert len(notebook_frontend.cells) == 1 +# +# notebook_frontend.editor_page.pause() +# cell_texts = ['aa = 1', 'bb = 2', 'cc = 3'] +# a, b, c = cell_texts +# notebook_frontend.populate(cell_texts) +# assert notebook_frontend.get_cells_contents() == [a, b, c] +# notebook_frontend._pause() diff --git a/nbclassic/tests/end_to_end/test_buffering.py b/nbclassic/tests/end_to_end/test_buffering.py new file mode 100644 index 000000000..59085fddc --- /dev/null +++ b/nbclassic/tests/end_to_end/test_buffering.py @@ -0,0 +1,45 @@ +"""Tests buffering of execution requests.""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +def test_kernels_buffer_without_conn(prefill_notebook): + """Test that execution request made while disconnected is buffered.""" + + notebook_frontend = prefill_notebook(["print(1 + 2)"]) + notebook_frontend.wait_for_kernel_ready() + + notebook_frontend.evaluate("() => { IPython.notebook.kernel.stop_channels }", page=EDITOR_PAGE) + notebook_frontend.execute_cell(0) + + notebook_frontend.evaluate("() => { IPython.notebook.kernel.reconnect }", page=EDITOR_PAGE) + notebook_frontend.wait_for_kernel_ready() + + outputs = notebook_frontend.wait_for_cell_output(0) + assert outputs.get_inner_text().strip() == '3' + + +def test_buffered_cells_execute_in_order(prefill_notebook): + """Test that buffered requests execute in order.""" + + notebook_frontend = prefill_notebook(['', 'k=1', 'k+=1', 'k*=3', 'print(k)']) + + # Repeated execution of cell queued up in the kernel executes + # each execution request in order. + notebook_frontend.wait_for_kernel_ready() + notebook_frontend.evaluate("() => IPython.notebook.kernel.stop_channels();", page=EDITOR_PAGE) + # k == 1 + notebook_frontend.execute_cell(1) + # k == 2 + notebook_frontend.execute_cell(2) + # k == 6 + notebook_frontend.execute_cell(3) + # k == 7 + notebook_frontend.execute_cell(2) + notebook_frontend.execute_cell(4) + notebook_frontend.evaluate("() => IPython.notebook.kernel.reconnect();", page=EDITOR_PAGE) + notebook_frontend.wait_for_kernel_ready() + + outputs = notebook_frontend.wait_for_cell_output(4) + assert outputs.get_inner_text().strip() == '7' diff --git a/nbclassic/tests/end_to_end/test_clipboard_multiselect.py b/nbclassic/tests/end_to_end/test_clipboard_multiselect.py new file mode 100644 index 000000000..a71b8e67a --- /dev/null +++ b/nbclassic/tests/end_to_end/test_clipboard_multiselect.py @@ -0,0 +1,48 @@ +"""Tests clipboard by copying, cutting and pasting multiple cells""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +# Optionally perfom this test with Ctrl+c and Ctrl+v +def test_clipboard_multiselect(prefill_notebook): + notebook = prefill_notebook(['', '1', '2', '3', '4', '5a', '6b', '7c', '8d']) + + assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '6b', '7c', '8d'] + + # Copy the first 3 cells + # Paste the values copied from the first three cells into the last 3 cells + + # Selecting the fist 3 cells + notebook.select_cell_range(1, 3) + + # Copy those selected cells + notebook.try_click_selector('#editlink', page=EDITOR_PAGE) + notebook.try_click_selector('//*[@id="copy_cell"]/a/span[1]', page=EDITOR_PAGE) + + # Select the last 3 cells + notebook.select_cell_range(6, 8) + + # Paste the cells in clipboard onto selected cells + notebook.try_click_selector('#editlink', page=EDITOR_PAGE) + notebook.try_click_selector('//*[@id="paste_cell_replace"]/a', page=EDITOR_PAGE) + + assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '1', '2', '3'] + + # Select the last four cells, cut them and paste them below the first cell + + # Select the last 4 cells + notebook.select_cell_range(5, 8) + + # Click Edit button and the select cut button + notebook.try_click_selector('#editlink', page=EDITOR_PAGE) + notebook.try_click_selector('//*[@id="cut_cell"]/a', page=EDITOR_PAGE) + + # Select the first cell + notebook.select_cell_range(0, 0) + + # Paste the cells in our clipboard below this first cell we are focused at + notebook.try_click_selector('#editlink', page=EDITOR_PAGE) + notebook.try_click_selector('//*[@id="paste_cell_below"]/a/span[1]', page=EDITOR_PAGE) + + assert notebook.get_cells_contents() == ['', '5a', '1', '2', '3', '1', '2', '3', '4'] diff --git a/nbclassic/tests/end_to_end/test_dashboard_nav.py b/nbclassic/tests/end_to_end/test_dashboard_nav.py new file mode 100644 index 000000000..558e04272 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -0,0 +1,55 @@ +"""Test navigation to directory links""" + + +import os +from .utils import TREE_PAGE +from jupyter_server.utils import url_path_join +pjoin = os.path.join + + +def url_in_tree(nb, url=None): + if url is None: + url = nb.get_page_url(page=TREE_PAGE) + + tree_url = url_path_join(nb.get_server_info(), 'tree') + return True if tree_url in url else False + + +def get_list_items(nb): + """ + Gets list items from a directory listing page + """ + + notebook_list = nb.locate('#notebook_list', page=TREE_PAGE) + link_items = notebook_list.locate_all('.item_link') + + return [{ + 'link': a.get_attribute('href'), + 'label': a.get_inner_text(), + 'element': a, + } for a in link_items if a.get_inner_text() != '..'] + + +def test_navigation(notebook_frontend): + + link_elements = get_list_items(notebook_frontend) + + def check_links(nb, list_of_link_elements): + if len(list_of_link_elements) < 1: + return False + + for item in list_of_link_elements: + item["element"].click() + + assert url_in_tree(notebook_frontend) == True + assert item["link"] in nb.get_page_url(page=TREE_PAGE) + + new_links = get_list_items(nb) + if len(new_links) > 0: + check_links(nb, new_links) + + nb.go_back(page=TREE_PAGE) + + return + + check_links(notebook_frontend, link_elements) diff --git a/nbclassic/tests/end_to_end/test_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py new file mode 100644 index 000000000..72c01f4f4 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -0,0 +1,66 @@ +"""Test cell deletion""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +def cell_is_deletable(notebook, index): + JS = f'() => {{ return Jupyter.notebook.get_cell({index}).is_deletable(); }}' + return notebook.evaluate(JS, EDITOR_PAGE) + + +def remove_all_cells(notebook): + for i in range(len(notebook.cells)): + notebook.delete_cell(0) + + +INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")'] + + +def test_delete_cells(notebook_frontend): + a, b, c = INITIAL_CELLS + notebook_frontend.populate(INITIAL_CELLS) + + # Validate initial state + assert notebook_frontend.get_cells_contents() == [a, b, c] + for cell in range(0, 3): + assert cell_is_deletable(notebook_frontend, cell) + + notebook_frontend.set_cell_metadata(0, 'deletable', 'false') + notebook_frontend.set_cell_metadata(1, 'deletable', 0 + ) + assert not cell_is_deletable(notebook_frontend, 0) + assert cell_is_deletable(notebook_frontend, 1) + assert cell_is_deletable(notebook_frontend, 2) + + # Try to delete cell a (should not be deleted) + notebook_frontend.delete_cell(0) + assert notebook_frontend.get_cells_contents() == [a, b, c] + + # Try to delete cell b (should succeed) + notebook_frontend.delete_cell(1) + assert notebook_frontend.get_cells_contents() == [a, c] + + # Try to delete cell c (should succeed) + notebook_frontend.delete_cell(1) + assert notebook_frontend.get_cells_contents() == [a] + + # Change the deletable state of cell a + notebook_frontend.set_cell_metadata(0, 'deletable', 'true') + + # Try to delete cell a (should succeed) + notebook_frontend.delete_cell(0) + assert len(notebook_frontend.cells) == 1 # it contains an empty cell + + # Make sure copied cells are deletable + notebook_frontend.edit_cell(index=0, content=a) + notebook_frontend.set_cell_metadata(0, 'deletable', 'false') + assert not cell_is_deletable(notebook_frontend, 0) + notebook_frontend.to_command_mode() + notebook_frontend.type_active('cv') + assert len(notebook_frontend.cells) == 2 + assert cell_is_deletable(notebook_frontend, 1) + + notebook_frontend.set_cell_metadata(0, 'deletable', 'true') # to perform below test, remove all the cells + notebook_frontend.delete_all_cells() + assert len(notebook_frontend.cells) == 1 # notebook should create one automatically on empty notebook diff --git a/nbclassic/tests/end_to_end/test_display_image.py b/nbclassic/tests/end_to_end/test_display_image.py new file mode 100644 index 000000000..93dba1224 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_display_image.py @@ -0,0 +1,80 @@ +"""Test display of images + +The effect of shape metadata is validated, using Image(retina=True) +""" + + +import re + + +# 2x2 black square in b64 jpeg and png +b64_image_data = { + "image/png": b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAC0lEQVR4nGNgQAYAAA4AAamRc7EA\\nAAAASUVORK5CYII', + "image/jpeg": b'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a\nHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy\nMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAACAAIDASIA\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/ooo\noAoo2Qoo' +} + + +def imports(notebook_frontend): + commands = [ + 'import base64', + 'from IPython.display import display, Image', + ] + notebook_frontend.edit_cell(index=0, content="\n".join(commands)) + notebook_frontend.execute_cell(0) + + +def validate_img(notebook_frontend, cell_index, image_fmt, retina): + """Validate that image renders as expected.""" + + # Show our test image in the notebook + b64data = b64_image_data[image_fmt] + commands = [ + f'b64data = {b64data}', + 'data = base64.decodebytes(b64data)', + f'display(Image(data, retina={retina}))' + ] + notebook_frontend.append("\n".join(commands)) + notebook_frontend.execute_cell(cell_index) + + # Find the image element that was just displayed + img_element = notebook_frontend.wait_for_tag("img", cell_index=cell_index) + + # Check image format + src = img_element.get_attribute("src") + prefix = src.split(',')[0] + expected_prefix = f"data:{image_fmt};base64" + assert prefix == expected_prefix + + # JS for obtaining img element dimensions + computed_width_js = ("(element) => { return window.getComputedStyle(element)" + ".getPropertyValue('width') }") + computed_height_js = ("(element) => { return window.getComputedStyle(element)" + ".getPropertyValue('height') }") + + # Obtain digit only string values for width/height + computed_width_raw = img_element.evaluate(computed_width_js) + computed_height_raw = img_element.evaluate(computed_height_js) + computed_width = re.search(r'[0-9]+', computed_width_raw) + computed_height = re.search(r'[0-9]+', computed_height_raw) + computed_width = None if computed_width is None else computed_width.group(0) + computed_height = None if computed_height is None else computed_height.group(0) + + # Check image dimensions + expected_size = "1" if retina else "2" + assert computed_width == expected_size + assert computed_height == expected_size + + +def test_display_image(notebook_frontend): + imports(notebook_frontend) + # PNG, non-retina + validate_img(notebook_frontend, 1, "image/png", False) + + # PNG, retina display + validate_img(notebook_frontend, 2, "image/png", True) + + # JPEG, non-retina + validate_img(notebook_frontend, 3, "image/jpeg", False) + + # JPEG, retina display + validate_img(notebook_frontend, 4, "image/jpeg", True) diff --git a/nbclassic/tests/selenium/test_display_isolation.py b/nbclassic/tests/end_to_end/test_display_isolation.py similarity index 64% rename from nbclassic/tests/selenium/test_display_isolation.py rename to nbclassic/tests/end_to_end/test_display_isolation.py index 51ca082bc..59ea9203a 100644 --- a/nbclassic/tests/selenium/test_display_isolation.py +++ b/nbclassic/tests/end_to_end/test_display_isolation.py @@ -3,19 +3,21 @@ An object whose metadata contains an "isolated" tag must be isolated from the rest of the document. """ -from .utils import wait_for_tag -def test_display_isolation(notebook): +from .utils import EDITOR_PAGE + + +def test_display_isolation(notebook_frontend): import_ln = "from IPython.core.display import HTML, SVG, display, display_svg" - notebook.edit_cell(index=0, content=import_ln) - notebook.execute_cell(notebook.current_cell) + notebook_frontend.edit_cell(index=0, content=import_ln) + notebook_frontend.execute_cell(0) try: - isolated_html(notebook) - isolated_svg(notebook) + isolated_html(notebook_frontend) + isolated_svg(notebook_frontend) finally: # Ensure we switch from iframe back to default content even if test fails - notebook.browser.switch_to.default_content() + notebook_frontend._editor_page.main_frame # TODO: def isolated_html(notebook): @@ -40,24 +42,18 @@ def isolated_html(notebook): display_i = f"display(HTML({isolated!r}), metadata={{'isolated':True}})" notebook.add_and_execute_cell(content=display_i) - iframe = wait_for_tag(notebook.browser, "iframe", single=True) - # The non-isolated div will be in the body - non_isolated_div = notebook.body.find_element_by_id("non-isolated") - assert non_isolated_div.value_of_css_property("color") == red + non_isolated_div = notebook.locate('#non-isolated', page=EDITOR_PAGE) + assert non_isolated_div.get_computed_property('color') == red # The non-isolated styling will have affected the output of other cells - test_div = notebook.body.find_element_by_id("test") - assert test_div.value_of_css_property("color") == red + test_div = notebook.locate('#test', page=EDITOR_PAGE) + assert test_div.get_computed_property('color') == red # The isolated div will be in an iframe, only that element will be blue - notebook.browser.switch_to.frame(iframe) - isolated_div = notebook.browser.find_element_by_id("isolated") - assert isolated_div.value_of_css_property("color") == blue - notebook.browser.switch_to.default_content() - # Clean up the html test cells - for i in range(1, len(notebook.cells)): - notebook.delete_cell(1) + notebook.wait_for_frame(count=2, page=EDITOR_PAGE) + isolated_div = notebook.locate_in_frame('#isolated', page=EDITOR_PAGE, frame_index=1) + assert isolated_div.get_computed_property("color") == blue def isolated_svg(notebook): @@ -76,18 +72,17 @@ def isolated_svg(notebook): content="display_svg(SVG(s1), metadata=dict(isolated=True))") notebook.add_and_execute_cell( content="display_svg(SVG(s2), metadata=dict(isolated=True))") - iframes = wait_for_tag(notebook.browser, "iframe", wait_for_n=2) + iframes = notebook.wait_for_tag("iframe", page=EDITOR_PAGE) # The first rectangle will be red - notebook.browser.switch_to.frame(iframes[0]) - isolated_svg_1 = notebook.browser.find_element_by_id('r1') - assert isolated_svg_1.value_of_css_property("fill") == yellow - notebook.browser.switch_to.default_content() - + notebook.wait_for_frame(count=2, page=EDITOR_PAGE) + isolated_svg_1 = notebook.locate_in_frame('#r1', page=EDITOR_PAGE, frame_index=2) + assert isolated_svg_1.get_computed_property("fill") == yellow + # The second rectangle will be black - notebook.browser.switch_to.frame(iframes[1]) - isolated_svg_2 = notebook.browser.find_element_by_id('r2') - assert isolated_svg_2.value_of_css_property("fill") == black + notebook.wait_for_frame(count=3, page=EDITOR_PAGE) + isolated_svg_2 = notebook.locate_in_frame('#r2', page=EDITOR_PAGE, frame_index=3) + assert isolated_svg_2.get_computed_property("fill") == black # Clean up the svg test cells for i in range(1, len(notebook.cells)): diff --git a/nbclassic/tests/end_to_end/test_dualmode_arrows.py b/nbclassic/tests/end_to_end/test_dualmode_arrows.py new file mode 100644 index 000000000..0db68ad76 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_arrows.py @@ -0,0 +1,105 @@ +"""Tests arrow keys on both command and edit mode""" + + +from .utils import EDITOR_PAGE + + +def test_dualmode_arrows(notebook_frontend): + + # Tests in command mode. + # Setting up the cells to test the keys to move up. + notebook_frontend.to_command_mode() + [notebook_frontend.press("b", page=EDITOR_PAGE) for i in range(3)] + + # Use both "k" and up arrow keys to moving up and enter a value. + # Once located on the top cell, use the up arrow keys to prove the top cell is still selected. + notebook_frontend.press("k", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("2", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + notebook_frontend.press("ArrowUp", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("1", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + notebook_frontend.press("k", page=EDITOR_PAGE) + notebook_frontend.press("ArrowUp", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("0", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0", "1", "2", ""] + + # Use the "k" key on the top cell as well + notebook_frontend.press("k", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.type(" edit #1", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0 edit #1", "1", "2", ""] + + # Setting up the cells to test the keys to move down + [notebook_frontend.press("j", page=EDITOR_PAGE) for i in range(3)] + [notebook_frontend.press("a", page=EDITOR_PAGE) for i in range(2)] + notebook_frontend.press("k", page=EDITOR_PAGE) + + # Use both "j" key and down arrow keys to moving down and enter a value. + # Once located on the bottom cell, use the down arrow key to prove the bottom cell is still selected. + notebook_frontend.press("ArrowDown", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("3", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + notebook_frontend.press("j", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("4", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + notebook_frontend.press("j", page=EDITOR_PAGE) + notebook_frontend.press("ArrowDown", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.press("5", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5"] + + # Use the "j" key on the top cell as well + notebook_frontend.press("j", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.type(" edit #1", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1"] + + # On the bottom cell, use both left and right arrow keys to prove the bottom cell is still selected. + notebook_frontend.press("ArrowLeft", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.type(", #2", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1, #2"] + notebook_frontend.press("ArrowRight", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + notebook_frontend.type(" and #3", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1, #2 and #3"] + + # Tests in edit mode. + # First, erase the previous content and then setup the cells to test the keys to move up. + [notebook_frontend.locate(".fa-cut.fa", page=EDITOR_PAGE).click() for i in range(6)] + [notebook_frontend.press("b", page=EDITOR_PAGE) for i in range(2)] + notebook_frontend.press("a", page=EDITOR_PAGE) + notebook_frontend.press("Enter", page=EDITOR_PAGE) + + # Use the up arrow key to move down and enter a value. + # We will use the left arrow key to move one char to the left since moving up on last character only moves selector to the first one. + # Once located on the top cell, use the up arrow key to prove the top cell is still selected. + notebook_frontend.press("ArrowUp", page=EDITOR_PAGE) + notebook_frontend.press("1", page=EDITOR_PAGE) + notebook_frontend.press("ArrowLeft", page=EDITOR_PAGE) + [notebook_frontend.press("ArrowUp", page=EDITOR_PAGE) for i in range(2)] + notebook_frontend.press("0", page=EDITOR_PAGE) + + # Use the down arrow key to move down and enter a value. + # We will use the right arrow key to move one char to the right since moving down puts selector to the last character. + # Once located on the bottom cell, use the down arrow key to prove the bottom cell is still selected. + notebook_frontend.press("ArrowDown", page=EDITOR_PAGE) + notebook_frontend.press("ArrowRight", page=EDITOR_PAGE) + notebook_frontend.press("ArrowDown", page=EDITOR_PAGE) + notebook_frontend.press("2", page=EDITOR_PAGE) + [notebook_frontend.press("ArrowDown", page=EDITOR_PAGE) for i in range(2)] + notebook_frontend.press("3", page=EDITOR_PAGE) + notebook_frontend.to_command_mode() + assert notebook_frontend.get_cells_contents() == ["0", "1", "2", "3"] diff --git a/nbclassic/tests/end_to_end/test_dualmode_cellmode.py b/nbclassic/tests/end_to_end/test_dualmode_cellmode.py new file mode 100644 index 000000000..9eafb658e --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_cellmode.py @@ -0,0 +1,63 @@ +"""Test keyboard shortcuts that change the cell's mode.""" + + +from .utils import EDITOR_PAGE + + +def test_dualmode_cellmode(notebook_frontend): + def get_cell_cm_mode(index): + code_mirror_mode = notebook_frontend.evaluate( + f"() => {{ return Jupyter.notebook.get_cell({index}).code_mirror.getMode().name; }}", + page=EDITOR_PAGE + ) + return code_mirror_mode + + index = 0 + a = 'hello\nmulti\nline' + + notebook_frontend.edit_cell(index=index, content=a) + + """check for the default cell type""" + notebook_frontend.to_command_mode() + notebook_frontend.press("r", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'raw' + assert get_cell_cm_mode(index) == 'null' + + """check cell type after changing to markdown""" + notebook_frontend.press("1", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '# ' + a + assert get_cell_cm_mode(index) == 'ipythongfm' + + notebook_frontend.press("2", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '## ' + a + + notebook_frontend.press("3", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '### ' + a + + notebook_frontend.press("4", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '#### ' + a + + notebook_frontend.press("5", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '##### ' + a + + notebook_frontend.press("6", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '###### ' + a + + notebook_frontend.press("m", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '###### ' + a + + notebook_frontend.press("y", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'code' + assert notebook_frontend.get_cell_contents(index) == '###### ' + a + assert get_cell_cm_mode(index) == 'ipython' + + notebook_frontend.press("1", page=EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert notebook_frontend.get_cell_contents(index) == '# ' + a diff --git a/nbclassic/tests/end_to_end/test_dualmode_clipboard.py b/nbclassic/tests/end_to_end/test_dualmode_clipboard.py new file mode 100644 index 000000000..0ea408e78 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_clipboard.py @@ -0,0 +1,58 @@ +"""Test clipboard functionality""" + + +from .utils import EDITOR_PAGE, validate_dualmode_state + + +INITIAL_CELLS = ['', 'print("a")', 'print("b")', 'print("c")'] + + +def test_dualmode_clipboard(prefill_notebook): + notebook_frontend = prefill_notebook(INITIAL_CELLS) + _, a, b, c = INITIAL_CELLS + for i in range(1, 4): + notebook_frontend.execute_cell(i) + + # Copy/paste/cut + num_cells = len(notebook_frontend.cells) + assert notebook_frontend.get_cell_contents(1) == a # Cell 1 is a + + notebook_frontend.focus_cell(1) + notebook_frontend.press("x", EDITOR_PAGE) # Cut + validate_dualmode_state(notebook_frontend, 'command', 1) + assert notebook_frontend.get_cell_contents(1) == b # Cell 2 is now where cell 1 was + assert len(notebook_frontend.cells) == num_cells - 1 # A cell was removed + + notebook_frontend.focus_cell(2) + notebook_frontend.press("v", EDITOR_PAGE) # Paste + validate_dualmode_state(notebook_frontend, 'command', 3) + assert notebook_frontend.get_cell_contents(3) == a # Cell 3 has the cut contents + assert len(notebook_frontend.cells) == num_cells # A cell was added + + notebook_frontend.press("v", EDITOR_PAGE) # Paste + validate_dualmode_state(notebook_frontend, 'command', 4) + assert notebook_frontend.get_cell_contents(4) == a # Cell a has the cut contents + assert len(notebook_frontend.cells) == num_cells + 1 # A cell was added + + notebook_frontend.focus_cell(1) + notebook_frontend.press("c", EDITOR_PAGE) # Copy + validate_dualmode_state(notebook_frontend, 'command', 1) + assert notebook_frontend.get_cell_contents(1) == b # Cell 1 is b + + notebook_frontend.focus_cell(2) + notebook_frontend.press("c", EDITOR_PAGE) # Copy + validate_dualmode_state(notebook_frontend, 'command', 2) + assert notebook_frontend.get_cell_contents(2) == c # Cell 2 is c + + notebook_frontend.focus_cell(4) + notebook_frontend.press("v", EDITOR_PAGE) # Paste + validate_dualmode_state(notebook_frontend, 'command', 5) + assert notebook_frontend.get_cell_contents(2) == c # Cell 2 has the copied contents + assert notebook_frontend.get_cell_contents(5) == c # Cell 5 has the copied contents + assert len(notebook_frontend.cells) == num_cells + 2 # A cell was added + + notebook_frontend.focus_cell(0) + notebook_frontend.press('v', EDITOR_PAGE, ['Shift']) # Paste + validate_dualmode_state(notebook_frontend, 'command', 0) + assert notebook_frontend.get_cell_contents(0) == c # Cell 0 has the copied contents + assert len(notebook_frontend.cells) == num_cells + 3 # A cell was added diff --git a/nbclassic/tests/end_to_end/test_dualmode_execute.py b/nbclassic/tests/end_to_end/test_dualmode_execute.py new file mode 100644 index 000000000..5dc3a9097 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_execute.py @@ -0,0 +1,76 @@ +"""Test keyboard invoked execution""" + + +from .utils import EDITOR_PAGE, validate_dualmode_state + + +INITIAL_CELLS = ['', 'print("a")', 'print("b")', 'print("c")'] + + +def test_dualmode_execute(prefill_notebook): + notebook_frontend = prefill_notebook(INITIAL_CELLS) + for i in range(1, 4): + notebook_frontend.execute_cell(i) + + # shift-enter + # ........... + # last cell in notebook + base_index = 3 + notebook_frontend.focus_cell(base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, ['Shift']) # creates one cell + validate_dualmode_state(notebook_frontend, 'edit', base_index + 1) + # ............................................... + # Not last cell in notebook & starts in edit mode + notebook_frontend.focus_cell(base_index) + notebook_frontend.press("Enter", EDITOR_PAGE) # Enter edit mode + validate_dualmode_state(notebook_frontend, 'edit', base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, ['Shift']) # creates one cell + validate_dualmode_state(notebook_frontend, 'command', base_index + 1) + # ...................... + # Starts in command mode + notebook_frontend.press('k', EDITOR_PAGE) + validate_dualmode_state(notebook_frontend, 'command', base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, ['Shift']) # creates one cell + validate_dualmode_state(notebook_frontend, 'command', base_index + 1) + + # Ctrl-enter + # .......... + # Last cell in notebook + base_index += 1 + notebook_frontend.press("Enter", EDITOR_PAGE, [notebook_frontend.get_platform_modifier_key()]) + validate_dualmode_state(notebook_frontend, 'command', base_index) + # ............................................... + # Not last cell in notebook & stats in edit mode + notebook_frontend.focus_cell(base_index - 1) + notebook_frontend.press("Enter", EDITOR_PAGE) # Enter edit mode + validate_dualmode_state(notebook_frontend, 'edit', base_index - 1) + notebook_frontend.press("Enter", EDITOR_PAGE, [notebook_frontend.get_platform_modifier_key()]) + # ............................................... + # Starts in command mode + notebook_frontend.press('j', EDITOR_PAGE) + validate_dualmode_state(notebook_frontend, 'command', base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, [notebook_frontend.get_platform_modifier_key()]) + validate_dualmode_state(notebook_frontend, 'command', base_index) + + # Alt-enter + # ............................................... + # Last cell in notebook + notebook_frontend.press("Enter", EDITOR_PAGE, ['Alt']) + validate_dualmode_state(notebook_frontend, 'edit', base_index + 1) + # Not last cell in notebook &starts in edit mode + notebook_frontend.focus_cell(base_index) + notebook_frontend.press("Enter", EDITOR_PAGE) # Enter edit mode + validate_dualmode_state(notebook_frontend, 'edit', base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, ['Alt']) + validate_dualmode_state(notebook_frontend, 'edit', base_index + 1) + # starts in command mode + notebook_frontend.press("Escape", EDITOR_PAGE) + notebook_frontend.press('k', EDITOR_PAGE) + validate_dualmode_state(notebook_frontend, 'command', base_index) + notebook_frontend.press("Enter", EDITOR_PAGE, ['Alt']) + validate_dualmode_state(notebook_frontend, 'edit', base_index + 1) + + # Notebook will now have 8 cells, the index of the last cell will be 7 + assert len(notebook_frontend.cells) == 8 # Cells were added + notebook_frontend.focus_cell(7) + validate_dualmode_state(notebook_frontend, 'command', 7) diff --git a/nbclassic/tests/end_to_end/test_dualmode_insertcell.py b/nbclassic/tests/end_to_end/test_dualmode_insertcell.py new file mode 100644 index 000000000..05fa33215 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_insertcell.py @@ -0,0 +1,56 @@ +"""Test cell insertion""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")'] + + +def test_insert_cell(prefill_notebook): + notebook_frontend = prefill_notebook(INITIAL_CELLS) + + notebook_frontend.to_command_mode() + notebook_frontend.focus_cell(2) + notebook_frontend.convert_cell_type(2, "markdown") + + # insert code cell above + notebook_frontend.press_active("a") + assert notebook_frontend.get_cell_contents(2).replace('\u200b', '') == "" + # ^TODO: Why are there NBSP's in here? Might be empty cells only? + assert notebook_frontend.get_cell_type(2) == "code" + assert len(notebook_frontend.cells) == 4 + + # insert code cell below + notebook_frontend.press_active("b") + assert notebook_frontend.get_cell_contents(2).replace('\u200b', '') == "" + assert notebook_frontend.get_cell_contents(3).replace('\u200b', '') == "" + assert notebook_frontend.get_cell_type(3) == "code" + assert len(notebook_frontend.cells) == 5 + + notebook_frontend.edit_cell(index=1, content="cell1") + notebook_frontend.focus_cell(1) + notebook_frontend.press_active("a") + assert notebook_frontend.get_cell_contents(1).replace('\u200b', '') == "" + assert notebook_frontend.get_cell_contents(2) == "cell1" + + notebook_frontend.edit_cell(index=1, content='cell1') + notebook_frontend.edit_cell(index=2, content='cell2') + notebook_frontend.edit_cell(index=3, content='cell3') + notebook_frontend.focus_cell(2) + notebook_frontend.press_active("b") + assert notebook_frontend.get_cell_contents(1) == "cell1" + assert notebook_frontend.get_cell_contents(2) == "cell2" + assert notebook_frontend.get_cell_contents(3).replace('\u200b', '') == "" + assert notebook_frontend.get_cell_contents(4) == "cell3" + + # insert above multiple selected cells + notebook_frontend.focus_cell(1) + notebook_frontend.press('ArrowDown', EDITOR_PAGE, ['Shift']) + notebook_frontend.press_active('a') + + # insert below multiple selected cells + notebook_frontend.focus_cell(2) + notebook_frontend.press('ArrowDown', EDITOR_PAGE, ['Shift']) + notebook_frontend.press_active('b') + assert notebook_frontend.get_cells_contents()[1:5] == ["", "cell1", "cell2", ""] diff --git a/nbclassic/tests/end_to_end/test_dualmode_markdown.py b/nbclassic/tests/end_to_end/test_dualmode_markdown.py new file mode 100644 index 000000000..4ab925326 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_markdown.py @@ -0,0 +1,52 @@ +"""Test markdown""" + + +from .utils import EDITOR_PAGE, validate_dualmode_state + + +def test_dualmode_markdown(notebook_frontend): + def is_cell_rendered(index): + JS = f'() => {{ return !!IPython.notebook.get_cell({index}).rendered; }}' + return notebook_frontend.evaluate(JS, EDITOR_PAGE) + + a = 'print("a")' + index = 1 + notebook_frontend.append(a) + + # Markdown rendering / unrendering + notebook_frontend.focus_cell(index) + validate_dualmode_state(notebook_frontend, 'command', index) + notebook_frontend.press("m", EDITOR_PAGE) + assert notebook_frontend.get_cell_type(index) == 'markdown' + assert not is_cell_rendered(index) # cell is not rendered + + notebook_frontend.press("Enter", EDITOR_PAGE) # cell is unrendered + assert not is_cell_rendered(index) # cell is not rendered + validate_dualmode_state(notebook_frontend, 'edit', index) + + notebook_frontend.press("Enter", EDITOR_PAGE, [notebook_frontend.get_platform_modifier_key()]) + assert is_cell_rendered(index) # cell is rendered with crtl+enter + validate_dualmode_state(notebook_frontend, 'command', index) + + notebook_frontend.press("Enter", EDITOR_PAGE) # cell is unrendered + assert not is_cell_rendered(index) # cell is not rendered + + notebook_frontend.focus_cell(index - 1) + assert not is_cell_rendered(index) # Select index-1; cell index is still not rendered + validate_dualmode_state(notebook_frontend, 'command', index - 1) + + notebook_frontend.focus_cell(index) + validate_dualmode_state(notebook_frontend, 'command', index) + notebook_frontend.press("Enter", EDITOR_PAGE, [notebook_frontend.get_platform_modifier_key()]) + assert is_cell_rendered(index) # Cell is rendered + + notebook_frontend.focus_cell(index - 1) + validate_dualmode_state(notebook_frontend, 'command', index - 1) + + notebook_frontend.press("Enter", EDITOR_PAGE, ['Shift']) + validate_dualmode_state(notebook_frontend, 'command', index) + assert is_cell_rendered(index) # Cell is rendered + + notebook_frontend.press("Enter", EDITOR_PAGE, ['Shift']) + validate_dualmode_state(notebook_frontend, 'edit', index + 1) + assert is_cell_rendered(index) # Cell is rendered diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py new file mode 100644 index 000000000..e5a0f0ef2 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -0,0 +1,90 @@ +"""Test basic cell execution methods, related shortcuts, and error modes""" + + +from .utils import CELL_OUTPUT_SELECTOR, EDITOR_PAGE + + +def test_execute_code(notebook_frontend): + # Execute cell with Javascript API + notebook_frontend.edit_cell(index=0, content='a=10; print(a)') + notebook_frontend.evaluate("Jupyter.notebook.get_cell(0).execute();", page=EDITOR_PAGE) + outputs = notebook_frontend.wait_for_cell_output(0) + outputs.wait_for('visible') + assert outputs.get_inner_text().strip() == '10' + + # Execute cell with Shift-Enter + notebook_frontend.edit_cell(index=0, content='a=11; print(a)') + notebook_frontend.clear_all_output() + notebook_frontend.press("Enter", EDITOR_PAGE, ["Shift"]) + outputs = notebook_frontend.wait_for_cell_output(0) + outputs.wait_for('visible') + assert outputs.get_inner_text().strip() == '11' + notebook_frontend.delete_cell(1) # Shift+Enter adds a cell + + # Execute cell with Ctrl-Enter (or equivalent) + notebook_frontend.edit_cell(index=0, content='a=12; print(a)') + notebook_frontend.clear_all_output() + notebook_frontend.press( + "Enter", + EDITOR_PAGE, + modifiers=[notebook_frontend.get_platform_modifier_key()] + ) + outputs = notebook_frontend.wait_for_cell_output(0) + outputs.wait_for('visible') + assert outputs.get_inner_text().strip() == '12' + + # Execute cell with toolbar button + notebook_frontend.edit_cell(index=0, content='a=13; print(a)') + notebook_frontend.clear_all_output() + notebook_frontend.click_toolbar_execute_btn() + outputs = notebook_frontend.wait_for_cell_output(0) + outputs.wait_for('visible') + assert outputs.get_inner_text().strip() == '13' + notebook_frontend.delete_cell(1) # Toolbar execute button adds a cell + + # Set up two cells to test stopping on error + notebook_frontend.type('a', EDITOR_PAGE) + notebook_frontend.edit_cell(index=0, content='raise IOError') + notebook_frontend.edit_cell(index=1, content='a=14; print(a)') + + # Default behaviour: stop on error + notebook_frontend.clear_all_output() + notebook_frontend.evaluate(""" + var cell0 = Jupyter.notebook.get_cell(0); + var cell1 = Jupyter.notebook.get_cell(1); + cell0.execute(); + cell1.execute(); + """, page=EDITOR_PAGE) + assert notebook_frontend.get_cell_output(0) is None + + # Execute a cell with stop_on_error=false + # ....................................... + # Make sure the previous eval call finished by checking for kernel_idle_icon + notebook_frontend.locate(".kernel_idle_icon", EDITOR_PAGE).wait_for('visible') + notebook_frontend.clear_all_output() + # Make sure clear cell output call is finished + notebook_frontend.wait_for_condition( + lambda: len( + [item for item in notebook_frontend.locate_all(CELL_OUTPUT_SELECTOR, EDITOR_PAGE)] + ) == 0, timeout=120, period=5 + ) + notebook_frontend.evaluate(""" + var cell0 = Jupyter.notebook.get_cell(0); + var cell1 = Jupyter.notebook.get_cell(1); + cell0.execute(false); + cell1.execute(); + """, page=EDITOR_PAGE) + # Make sure the previous eval call finished by checking for kernel_idle_icon + notebook_frontend.locate(".kernel_idle_icon", EDITOR_PAGE).wait_for('visible') + if notebook_frontend.locate(".kernel_busy_icon", EDITOR_PAGE).is_visible(): + # If kernel is busy, wait for it to finish + notebook_frontend.locate(".kernel_idle_icon", EDITOR_PAGE).wait_for('visible') + # Wait for cell outputs + notebook_frontend.wait_for_condition( + lambda: len( + [item for item in notebook_frontend.locate_all(CELL_OUTPUT_SELECTOR, EDITOR_PAGE)] + ) >= 2, timeout=120, period=5 + ) + outputs = notebook_frontend.wait_for_cell_output(1) + assert outputs is not None + assert outputs.get_inner_text().strip() == '14' diff --git a/nbclassic/tests/end_to_end/test_find_and_replace.py b/nbclassic/tests/end_to_end/test_find_and_replace.py new file mode 100644 index 000000000..787c9c19e --- /dev/null +++ b/nbclassic/tests/end_to_end/test_find_and_replace.py @@ -0,0 +1,24 @@ +"""Test Jupyter find/replace""" + + +INITIAL_CELLS = ["hello", "hellohello", "abc", "ello"] + + +def test_find_and_replace(notebook_frontend): + """ test find and replace on all the cells """ + notebook_frontend.populate(INITIAL_CELLS) + + find_str = "ello" + replace_str = "foo" + + notebook_frontend.wait_for_condition(lambda: notebook_frontend.get_cells_contents() == INITIAL_CELLS, timeout=120, period=5) + + # replace the strings + notebook_frontend.find_and_replace(index=0, find_txt=find_str, replace_txt=replace_str) + + # check content of the cells + notebook_frontend.wait_for_condition( + lambda: notebook_frontend.get_cells_contents() == [ + s.replace(find_str, replace_str) for s in INITIAL_CELLS + ] + ) diff --git a/nbclassic/tests/end_to_end/test_interrupt.py b/nbclassic/tests/end_to_end/test_interrupt.py new file mode 100644 index 000000000..8a16a2dcd --- /dev/null +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -0,0 +1,38 @@ +"""Test kernel interrupt""" + + +from .utils import EDITOR_PAGE + + +def interrupt_from_menu(notebook_frontend): + # Click interrupt button in kernel menu + notebook_frontend.try_click_selector('#kernellink', page=EDITOR_PAGE) + notebook_frontend.try_click_selector('#int_kernel', page=EDITOR_PAGE) + notebook_frontend.press('Escape', page=EDITOR_PAGE) + + +def interrupt_from_keyboard(notebook_frontend): + notebook_frontend.type("ii", page=EDITOR_PAGE) + notebook_frontend.press('Escape', page=EDITOR_PAGE) + + +def test_interrupt(notebook_frontend): + """ Test the interrupt function using both the button in the Kernel menu and the keyboard shortcut "ii" + """ + + text = ('import time\n' + 'for x in range(3):\n' + ' time.sleep(1)') + + notebook_frontend.edit_cell(index=0, content=text) + + for interrupt_method in (interrupt_from_menu, interrupt_from_keyboard): + notebook_frontend.clear_cell_output(0) + notebook_frontend.to_command_mode() + notebook_frontend.execute_cell(0) + + interrupt_method(notebook_frontend) + + # Wait for an output to appear + output = notebook_frontend.wait_for_cell_output(0) + assert 'KeyboardInterrupt' in output.get_inner_text() diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py new file mode 100644 index 000000000..ded44d468 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -0,0 +1,56 @@ +"""Test kernel menu""" + + +from .utils import EDITOR_PAGE + + +restart_selectors = [ + '#restart_kernel', '#restart_clear_output', '#restart_run_all' +] +notify_interaction = '#notification_kernel > span' +shutdown_selector = '#shutdown_kernel' +confirm_selector = '.btn-danger' +cancel_selector = ".modal-footer button:first-of-type" + + +def test_cancel_restart_or_shutdown(notebook_frontend): + """Click each of the restart options, then cancel the confirmation dialog""" + kernel_menu = notebook_frontend.locate('#kernellink', EDITOR_PAGE) + if not kernel_menu: + raise Exception('Could not find kernel_menu') + + for menu_item in restart_selectors + [shutdown_selector]: + kernel_menu.click() + notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() + notebook_frontend.wait_for_selector(cancel_selector, EDITOR_PAGE).click() + + modal = notebook_frontend.wait_for_selector('.modal-backdrop', EDITOR_PAGE) + modal.wait_for('hidden') + + assert notebook_frontend.is_kernel_running() + + +def test_menu_items(notebook_frontend): + kernel_menu = notebook_frontend.locate('#kernellink', EDITOR_PAGE) + + for menu_item in restart_selectors: + # Shutdown + kernel_menu.click() + notebook_frontend.wait_for_selector(shutdown_selector, EDITOR_PAGE).click() + + # Confirm shutdown + notebook_frontend.wait_for_selector(confirm_selector, EDITOR_PAGE).click() + + notebook_frontend.wait_for_condition(lambda: not notebook_frontend.is_kernel_running()) + + # Restart + # (can't click the menu while a modal dialog is fading out) + modal = notebook_frontend.locate('.modal-backdrop', EDITOR_PAGE).expect_not_to_be_visible() + kernel_menu.click() + + notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() + notebook_frontend.wait_for_condition( + lambda: notebook_frontend.is_kernel_running(), + timeout=120, + period=5 + ) diff --git a/nbclassic/tests/selenium/test_markdown.py b/nbclassic/tests/end_to_end/test_markdown.py similarity index 54% rename from nbclassic/tests/selenium/test_markdown.py rename to nbclassic/tests/end_to_end/test_markdown.py index cae1a7a03..fa501ad6b 100644 --- a/nbclassic/tests/selenium/test_markdown.py +++ b/nbclassic/tests/end_to_end/test_markdown.py @@ -1,41 +1,47 @@ +"""Test markdown rendering""" + + from nbformat.v4 import new_markdown_cell +from .utils import EDITOR_PAGE + + def get_rendered_contents(nb): + # TODO: Encapsulate element access/refactor so we're not accessing playwright element objects cl = ["text_cell", "render"] - rendered_cells = [cell.find_element_by_class_name("text_cell_render") - for cell in nb.cells + rendered_cells = [cell.locate(".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 + return [x.get_inner_html().strip() + for x in rendered_cells if x is not None] - + def test_markdown_cell(prefill_notebook): - nb = prefill_notebook([new_markdown_cell(md) for md in [ + notebook_frontend = prefill_notebook([new_markdown_cell(md) for md in [ '# Foo', '**Bar**', '*Baz*', '```\nx = 1\n```', '```aaaa\nx = 1\n```', '```python\ns = "$"\nt = "$"\n```' ]]) - assert get_rendered_contents(nb) == [ + assert get_rendered_contents(notebook_frontend) == [ '
Bar
', 'Baz
', 'x = 1
',
'x = 1
',
- '' +
+ '' +
's = "$"\n' +
't = "$"
'
]
-def test_markdown_headings(notebook):
- lst = list([1, 2, 3, 4, 5, 6, 2, 1])
- for i in lst:
- notebook.add_markdown_cell()
- cell_text = notebook.browser.execute_script(f"""
+
+def test_markdown_headings(notebook_frontend):
+ for i in [1, 2, 3, 4, 5, 6, 2, 1]:
+ notebook_frontend.add_markdown_cell()
+ cell_text = notebook_frontend.evaluate(f"""
var cell = IPython.notebook.get_cell(1);
cell.set_heading_level({i});
cell.get_text();
- """)
- assert notebook.get_cell_contents(1) == "#" * i + " "
- notebook.delete_cell(1)
-
+ """, page=EDITOR_PAGE)
+ assert notebook_frontend.get_cell_contents(1) == "#" * i + " "
+ notebook_frontend.delete_cell(1)
diff --git a/nbclassic/tests/end_to_end/test_merge_cells.py b/nbclassic/tests/end_to_end/test_merge_cells.py
new file mode 100644
index 000000000..5c627ba50
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_merge_cells.py
@@ -0,0 +1,40 @@
+"""Tests the merge cell api."""
+
+
+from .utils import EDITOR_PAGE
+
+
+INITIAL_CELLS = [
+ "foo = 5",
+ "bar = 10",
+ "baz = 15",
+ "print(foo)",
+ "print(bar)",
+ "print(baz)",
+]
+
+
+def test_merge_cells(prefill_notebook):
+ notebook_frontend = prefill_notebook(INITIAL_CELLS)
+ a, b, c, d, e, f = INITIAL_CELLS
+
+ # Before merging, there are 6 separate cells
+ assert notebook_frontend.get_cells_contents() == [a, b, c, d, e, f]
+
+ # Focus on the second cell and merge it with the cell above
+ notebook_frontend.focus_cell(1)
+ notebook_frontend.evaluate("Jupyter.notebook.merge_cell_above();", EDITOR_PAGE)
+ merged_a_b = f"{a}\n\n{b}"
+ assert notebook_frontend.get_cells_contents() == [merged_a_b, c, d, e, f]
+
+ # Focus on the second cell and merge it with the cell below
+ notebook_frontend.focus_cell(1)
+ notebook_frontend.evaluate("Jupyter.notebook.merge_cell_below();", EDITOR_PAGE)
+ merged_c_d = f"{c}\n\n{d}"
+ assert notebook_frontend.get_cells_contents() == [merged_a_b, merged_c_d, e, f]
+
+ # Merge everything down to a single cell with selected cells
+ notebook_frontend.select_cell_range(0, 3)
+ notebook_frontend.evaluate("Jupyter.notebook.merge_selected_cells();", EDITOR_PAGE)
+ merged_all = f"{merged_a_b}\n\n{merged_c_d}\n\n{e}\n\n{f}"
+ assert notebook_frontend.get_cells_contents() == [merged_all]
diff --git a/nbclassic/tests/end_to_end/test_move_multiselection.py b/nbclassic/tests/end_to_end/test_move_multiselection.py
new file mode 100644
index 000000000..a4266892e
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_move_multiselection.py
@@ -0,0 +1,65 @@
+"""Test cell multiselect move"""
+
+
+from .utils import EDITOR_PAGE
+
+
+INITIAL_CELLS = ['1', '2', '3', '4', '5', '6']
+
+
+def test_move_multiselection(prefill_notebook):
+ notebook_frontend = prefill_notebook(INITIAL_CELLS)
+
+ def assert_order(pre_message, expected_state):
+ for i in range(len(expected_state)):
+ assert expected_state[i] == notebook_frontend.get_cell_contents(
+ i), f"{pre_message}: Verify that cell {i} has for content: {expected_state[i]} found: {notebook_frontend.get_cell_contents(i)}"
+
+ # Select 3 first cells
+ notebook_frontend.select_cell_range(0, 2)
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_up();",
+ EDITOR_PAGE
+ )
+ # Should not move up at top
+ assert_order('move up at top', ['1', '2', '3', '4', '5', '6'])
+
+ # We do not need to reselect, move/up down should keep the selection.
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_down();",
+ EDITOR_PAGE
+ )
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_down();",
+ EDITOR_PAGE
+ )
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_down();",
+ EDITOR_PAGE
+ )
+
+ # 3 times down should move the 3 selected cells to the bottom
+ assert_order("move down to bottom", ['4', '5', '6', '1', '2', '3'])
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_down();",
+ EDITOR_PAGE
+ )
+
+ # They can't go any futher
+ assert_order("move down to bottom", ['4', '5', '6', '1', '2', '3'])
+
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_up();",
+ EDITOR_PAGE
+ )
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_up();",
+ EDITOR_PAGE
+ )
+ notebook_frontend.evaluate(
+ "Jupyter.notebook.move_selection_up();",
+ EDITOR_PAGE
+ )
+
+ # Bring them back on top
+ assert_order('move up at top', ['1', '2', '3', '4', '5', '6'])
diff --git a/nbclassic/tests/end_to_end/test_multiselect.py b/nbclassic/tests/end_to_end/test_multiselect.py
new file mode 100644
index 000000000..693983481
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_multiselect.py
@@ -0,0 +1,70 @@
+"""Test cell multiselection operations"""
+
+
+from .utils import EDITOR_PAGE
+
+
+INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
+
+
+def test_multiselect(prefill_notebook):
+ notebook_frontend = prefill_notebook(INITIAL_CELLS)
+
+ def extend_selection_by(delta):
+ notebook_frontend.evaluate(
+ f"Jupyter.notebook.extend_selection_by({delta});", EDITOR_PAGE)
+
+ def n_selected_cells():
+ return notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_selected_cells().length; }", EDITOR_PAGE)
+
+ notebook_frontend.focus_cell(0)
+ assert n_selected_cells() == 1
+
+ # Check that only one cell is selected according to CSS classes as well
+ selected_css = notebook_frontend.locate_all(
+ '.cell.jupyter-soft-selected, .cell.selected', EDITOR_PAGE)
+ assert len(selected_css) == 1
+
+ # Extend the selection down one
+ extend_selection_by(1)
+ assert n_selected_cells() == 2
+
+ # Contract the selection up one
+ extend_selection_by(-1)
+ assert n_selected_cells() == 1
+
+ # Extend the selection up one
+ notebook_frontend.focus_cell(1)
+ extend_selection_by(-1)
+ assert n_selected_cells() == 2
+
+ # Convert selected cells to Markdown
+ notebook_frontend.evaluate("Jupyter.notebook.cells_to_markdown();", EDITOR_PAGE)
+ cell_types = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.cell_type) }", EDITOR_PAGE)
+ assert cell_types == ['markdown', 'markdown', 'code']
+ # One cell left selected after conversion
+ assert n_selected_cells() == 1
+
+ # Convert selected cells to raw
+ notebook_frontend.focus_cell(1)
+ extend_selection_by(1)
+ assert n_selected_cells() == 2
+ notebook_frontend.evaluate("Jupyter.notebook.cells_to_raw();", EDITOR_PAGE)
+ cell_types = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.cell_type) }", EDITOR_PAGE)
+ assert cell_types == ['markdown', 'raw', 'raw']
+ # One cell left selected after conversion
+ assert n_selected_cells() == 1
+
+ # Convert selected cells to code
+ notebook_frontend.focus_cell(0)
+ extend_selection_by(2)
+ assert n_selected_cells() == 3
+ notebook_frontend.evaluate("Jupyter.notebook.cells_to_code();", EDITOR_PAGE)
+ cell_types = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.cell_type) }", EDITOR_PAGE)
+ assert cell_types == ['code'] * 3
+ # One cell left selected after conversion
+ assert n_selected_cells() == 1
diff --git a/nbclassic/tests/end_to_end/test_multiselect_toggle.py b/nbclassic/tests/end_to_end/test_multiselect_toggle.py
new file mode 100644
index 000000000..86801852c
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_multiselect_toggle.py
@@ -0,0 +1,52 @@
+"""Test multiselect toggle"""
+
+
+from .utils import EDITOR_PAGE
+
+
+INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
+
+
+def test_multiselect_toggle(prefill_notebook):
+ notebook_frontend = prefill_notebook(INITIAL_CELLS)
+
+ def extend_selection_by(delta):
+ notebook_frontend.evaluate(
+ f"Jupyter.notebook.extend_selection_by({delta});", EDITOR_PAGE)
+
+ def n_selected_cells():
+ return notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_selected_cells().length; }", EDITOR_PAGE)
+
+ def select_cells():
+ notebook_frontend.focus_cell(0)
+ extend_selection_by(2)
+
+ # Test that cells, which start off not collapsed, are collapsed after
+ # calling the multiselected cell toggle.
+ select_cells()
+ assert n_selected_cells() == 3
+ notebook_frontend.evaluate("Jupyter.notebook.execute_selected_cells();", EDITOR_PAGE)
+ select_cells()
+ notebook_frontend.evaluate("Jupyter.notebook.toggle_cells_outputs();", EDITOR_PAGE)
+ cell_output_states = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.collapsed) }", EDITOR_PAGE)
+ assert cell_output_states == [False] * 3, "ensure that all cells are not collapsed"
+
+ # Test that cells, which start off not scrolled are scrolled after
+ # calling the multiselected scroll toggle.
+ select_cells()
+ assert n_selected_cells() == 3
+ notebook_frontend.evaluate("Jupyter.notebook.toggle_cells_outputs_scroll();", EDITOR_PAGE)
+ cell_scrolled_states = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.output_area.scroll_state) }", EDITOR_PAGE)
+ assert all(cell_scrolled_states), "ensure that all have scrolling enabled"
+
+ # Test that cells, which start off not cleared are cleared after
+ # calling the multiselected scroll toggle.
+ select_cells()
+ assert n_selected_cells() == 3
+ notebook_frontend.evaluate("Jupyter.notebook.clear_cells_outputs();", EDITOR_PAGE)
+ cell_outputs_cleared = notebook_frontend.evaluate(
+ "() => { return Jupyter.notebook.get_cells().map(c => c.output_area.element.html()) }", EDITOR_PAGE)
+ assert cell_outputs_cleared == [""] * 3, "ensure that all cells are cleared"
diff --git a/nbclassic/tests/end_to_end/test_notifications.py b/nbclassic/tests/end_to_end/test_notifications.py
new file mode 100644
index 000000000..bf8d9666d
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_notifications.py
@@ -0,0 +1,118 @@
+"""Test the notification area and widgets"""
+
+
+import pytest
+from .utils import EDITOR_PAGE
+
+
+def get_widget(notebook, name):
+ return notebook.evaluate(
+ f"() => {{ return IPython.notification_area.get_widget('{name}') !== undefined }}",
+ page=EDITOR_PAGE
+ )
+
+
+def widget(notebook, name):
+ return notebook.evaluate(
+ f"() => {{ return IPython.notification_area.widget('{name}') !== undefined }}",
+ page=EDITOR_PAGE
+ )
+
+
+def new_notification_widget(notebook, name):
+ return notebook.evaluate(
+ f"() => {{ return IPython.notification_area.new_notification_widget('{name}') !== undefined }}",
+ page=EDITOR_PAGE
+ )
+
+
+def widget_has_class(notebook, name, class_name):
+ return notebook.evaluate(
+ f"""() => {{
+ var w = IPython.notification_area.get_widget('{name}');
+ return w.element.hasClass('{class_name}'); }}
+ """,
+ page=EDITOR_PAGE
+ )
+
+
+def widget_message(notebook, name):
+ return notebook.evaluate(
+ f"""() => {{
+ var w = IPython.notification_area.get_widget('{name}');
+ return w.get_message(); }}
+ """,
+ page=EDITOR_PAGE
+ )
+
+
+def test_notification(notebook_frontend):
+ # check that existing widgets are there
+ assert get_widget(notebook_frontend, "kernel") and widget(notebook_frontend, "kernel"),\
+ "The kernel notification widget exists"
+ assert get_widget(notebook_frontend, "notebook") and widget(notebook_frontend, "notebook"),\
+ "The notebook notification widget exists"
+
+ # try getting a non-existent widget
+ with pytest.raises(Exception):
+ get_widget(notebook_frontend, "foo")
+
+ # try creating a non-existent widget
+ assert widget(notebook_frontend, "bar"), "widget: new widget is created"
+
+ # try creating a widget that already exists
+ with pytest.raises(Exception):
+ new_notification_widget(notebook_frontend, "kernel")
+
+ # test creating 'info', 'warning' and 'danger' messages
+ for level in ("info", "warning", "danger"):
+ notebook_frontend.evaluate(
+ f"""
+ var tnw = IPython.notification_area.widget('test');
+ tnw.{level}('test {level}');
+ """,
+ page=EDITOR_PAGE
+ )
+ notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE)
+
+ assert widget_has_class(notebook_frontend, "test", level), f"{level}: class is incorrect"
+ assert widget_message(notebook_frontend, "test") == f"test {level}", f"{level}: message is incorrect"
+
+ # test message timeout
+ notebook_frontend.evaluate(
+ """
+ var tnw = IPython.notification_area.widget('test');
+ tnw.set_message('test timeout', 1000);
+ """,
+ page=EDITOR_PAGE
+ )
+ notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE)
+ assert widget_message(notebook_frontend, "test") == "test timeout", "timeout: message is incorrect"
+
+ notebook_frontend.wait_for_selector("#notification_test", EDITOR_PAGE, state='hidden')
+ assert widget_message(notebook_frontend, "test") == "", "timeout: message was not cleared"
+
+ # test click callback
+ notebook_frontend.evaluate(
+ """
+ var tnw = IPython.notification_area.widget('test');
+ tnw._clicked = false;
+ tnw.set_message('test click', undefined, function () {
+ tnw._clicked = true;
+ return true;
+ });
+ """,
+ page=EDITOR_PAGE
+ )
+ notebook_frontend.locate("#notification_test", page=EDITOR_PAGE)
+ assert widget_message(notebook_frontend, "test") == "test click", "callback: message is correct"
+
+ notebook_frontend.locate("#notification_test", page=EDITOR_PAGE).click()
+ notebook_frontend.wait_for_condition(
+ lambda: notebook_frontend.evaluate(
+ '() => { return IPython.notification_area.widget("test")._clicked; }', page=EDITOR_PAGE
+ )
+ )
+
+ notebook_frontend.wait_for_selector("#notification_test", EDITOR_PAGE, state='hidden')
+ assert widget_message(notebook_frontend, "test") == "", "callback: message was not cleared"
diff --git a/nbclassic/tests/selenium/test_prompt_numbers.py b/nbclassic/tests/end_to_end/test_prompt_numbers.py
old mode 100755
new mode 100644
similarity index 59%
rename from nbclassic/tests/selenium/test_prompt_numbers.py
rename to nbclassic/tests/end_to_end/test_prompt_numbers.py
index 38872b855..fad9dd654
--- a/nbclassic/tests/selenium/test_prompt_numbers.py
+++ b/nbclassic/tests/end_to_end/test_prompt_numbers.py
@@ -1,15 +1,22 @@
+"""Test multiselect toggle
+
+TODO: This changes the In []: label preceding the cell,
+ what's the purpose of this? Update the docstring
+"""
+
+
def test_prompt_numbers(prefill_notebook):
- notebook = prefill_notebook(['print("a")'])
+ notebook_frontend = prefill_notebook(['print("a")'])
def get_prompt():
return (
- notebook.cells[0].find_element_by_class_name('input')
- .find_element_by_class_name('input_prompt')
- .get_attribute('innerHTML').strip()
+ notebook_frontend.cells[0].locate('.input')
+ .locate('.input_prompt')
+ .get_inner_html().strip()
)
def set_prompt(value):
- notebook.set_cell_input_prompt(0, value)
+ notebook_frontend.set_cell_input_prompt(0, value)
assert get_prompt() == "In [ ]:"
diff --git a/nbclassic/tests/end_to_end/test_save.py b/nbclassic/tests/end_to_end/test_save.py
new file mode 100644
index 000000000..29c8d32af
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_save.py
@@ -0,0 +1,71 @@
+"""Test saving a notebook with escaped characters
+"""
+
+from tkinter import E
+from urllib.parse import quote
+from .utils import EDITOR_PAGE, TREE_PAGE
+
+# TODO: REWORK from polling => async
+def check_display_name(nb, nbname):
+ display_updated = False
+ count_check = 0
+
+ while not display_updated and count_check < 5:
+ displayed_name = nb.locate('#notebook_name', page=EDITOR_PAGE).get_inner_text()
+ if displayed_name + '.ipynb' == nbname:
+ display_updated = True
+ count_check += 1
+ assert displayed_name + '.ipynb' == nbname
+
+def test_save(notebook_frontend):
+ # don't use unicode with ambiguous composed/decomposed normalization
+ # because the filesystem may use a different normalization than literals.
+ # This causes no actual problems, but will break string comparison.
+ nbname = "has#hash and space and unicø∂e.ipynb"
+ escaped_name = quote(nbname)
+
+ notebook_frontend.edit_cell(index=0, content="s = '??'")
+
+ set_nb_name = f"() => Jupyter.notebook.set_notebook_name('{nbname}')"
+ notebook_frontend.evaluate(set_nb_name, page=EDITOR_PAGE)
+
+ model = notebook_frontend.evaluate("() => Jupyter.notebook.save_notebook()", page=EDITOR_PAGE)
+ assert model['name'] == nbname
+
+ current_name = notebook_frontend.evaluate("() => Jupyter.notebook.notebook_name", page=EDITOR_PAGE)
+ assert current_name == nbname
+
+ current_path = notebook_frontend.evaluate("() => Jupyter.notebook.notebook_path", page=EDITOR_PAGE)
+ assert current_path == nbname
+
+ check_display_name(notebook_frontend, nbname)
+
+ notebook_frontend.evaluate("() => Jupyter.notebook.save_checkpoint()", page=EDITOR_PAGE)
+
+ checkpoints = notebook_frontend.evaluate("() => Jupyter.notebook.checkpoints", page=EDITOR_PAGE)
+ assert len(checkpoints) == 1
+
+ notebook_frontend.try_click_selector('#ipython_notebook a', page=EDITOR_PAGE)
+ notebook_frontend.wait_for_selector('.item_link', page=EDITOR_PAGE)
+
+ hrefs_nonmatch = []
+ all_links = notebook_frontend.locate_all('a.item_link', page=EDITOR_PAGE)
+
+ for link in all_links:
+ href = link.get_attribute('href')
+
+ if escaped_name in href:
+ href = href.split('/a@b/')
+ notebook_frontend.navigate_to(page=EDITOR_PAGE, partial_url=href[1])
+ notebook_frontend.wait_for_selector('.cell', page=EDITOR_PAGE)
+ break
+ hrefs_nonmatch.append(href)
+ else:
+ raise AssertionError(f"{escaped_name!r} not found in {hrefs_nonmatch!r}")
+
+ current_name = notebook_frontend.evaluate("() => Jupyter.notebook.notebook_name", page=EDITOR_PAGE)
+ assert current_name == nbname
+
+ notebook_frontend.edit_cell(index=0, content="")
+ notebook_frontend.delete_all_cells()
+
\ No newline at end of file
diff --git a/nbclassic/tests/end_to_end/test_save_as_notebook.py b/nbclassic/tests/end_to_end/test_save_as_notebook.py
new file mode 100644
index 000000000..f90787812
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_save_as_notebook.py
@@ -0,0 +1,42 @@
+"""Test readonly notebook saved and renamed"""
+
+
+from .utils import EDITOR_PAGE
+from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
+
+
+def save_as(nb):
+ JS = '() => Jupyter.notebook.save_notebook_as()'
+ return nb.evaluate(JS, page=EDITOR_PAGE)
+
+def get_notebook_name(nb):
+ JS = '() => Jupyter.notebook.notebook_name'
+ return nb.evaluate(JS, page=EDITOR_PAGE)
+
+def set_notebook_name(nb, name):
+ JS = f'() => Jupyter.notebook.rename("{name}")'
+ nb.evaluate(JS, page=EDITOR_PAGE)
+
+
+def test_save_notebook_as(notebook_frontend):
+ set_notebook_name(notebook_frontend, name="nb1.ipynb")
+
+ notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE)
+
+ assert get_notebook_name(notebook_frontend) == "nb1.ipynb"
+
+ # Wait for Save As modal, save
+ save_as(notebook_frontend)
+ notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE)
+
+ # TODO: Add a function for locator assertions to FrontendElement
+ locator_element = notebook_frontend.locate_and_focus('//input[@data-testid="save-as"]', page=EDITOR_PAGE)
+ locator_element.wait_for('visible')
+
+ notebook_frontend.insert_text('new_notebook.ipynb', page=EDITOR_PAGE)
+ notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE)
+
+ locator_element.expect_not_to_be_visible()
+
+ assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb"
+ assert "new_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE)
diff --git a/nbclassic/tests/end_to_end/test_save_readonly_as.py b/nbclassic/tests/end_to_end/test_save_readonly_as.py
new file mode 100644
index 000000000..6b6bde96d
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py
@@ -0,0 +1,61 @@
+"""Test readonly notebook saved and renamed"""
+
+
+from .utils import EDITOR_PAGE
+from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
+
+
+def save_as(nb):
+ JS = '() => Jupyter.notebook.save_notebook_as()'
+ return nb.evaluate(JS, page=EDITOR_PAGE)
+
+def get_notebook_name(nb):
+ JS = '() => Jupyter.notebook.notebook_name'
+ return nb.evaluate(JS, page=EDITOR_PAGE)
+
+def set_notebook_name(nb, name):
+ JS = f'() => Jupyter.notebook.rename("{name}")'
+ nb.evaluate(JS, page=EDITOR_PAGE)
+
+
+def test_save_readonly_as(notebook_frontend):
+ notebook_frontend.edit_cell(index=0, content='a=10; print(a)')
+ notebook_frontend.wait_for_kernel_ready()
+ notebook_frontend.wait_for_selector(".input", page=EDITOR_PAGE)
+
+ # Set a name for comparison later
+ set_notebook_name(notebook_frontend, name="nb1.ipynb")
+ assert get_notebook_name(notebook_frontend) == "nb1.ipynb"
+
+ # Wait for Save As modal, save
+ save_as(notebook_frontend)
+
+ # Wait for modal to pop up
+ notebook_frontend.wait_for_selector('//input[@data-testid="save-as"]', page=EDITOR_PAGE)
+
+ # TODO: Add a function for locator assertions to FrontendElement
+ locator_element = notebook_frontend.locate_and_focus('//input[@data-testid="save-as"]', page=EDITOR_PAGE)
+ locator_element.wait_for('visible')
+
+ modal_footer = notebook_frontend.locate('.modal-footer', page=EDITOR_PAGE)
+ modal_footer.wait_for('visible')
+
+ notebook_frontend.insert_text('new_notebook.ipynb', page=EDITOR_PAGE)
+
+ save_btn = modal_footer.locate('text=Save')
+ save_btn.wait_for('visible')
+ save_btn.click()
+ # notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE)
+
+ # locator_element.expect_not_to_be_visible()
+ notebook_frontend.wait_for_condition(
+ lambda: get_notebook_name(notebook_frontend) == "new_notebook.ipynb", timeout=120, period=5
+ )
+
+ # notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE)
+
+ # # Test that the name changed
+ # assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb"
+
+ # Test that address bar was updated
+ assert "new_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE)
diff --git a/nbclassic/tests/end_to_end/test_shutdown.py b/nbclassic/tests/end_to_end/test_shutdown.py
new file mode 100644
index 000000000..6c2a5010d
--- /dev/null
+++ b/nbclassic/tests/end_to_end/test_shutdown.py
@@ -0,0 +1,14 @@
+"""Tests shutdown of the Kernel."""
+from .utils import EDITOR_PAGE
+
+def test_shutdown(prefill_notebook):
+ notebook_frontend = prefill_notebook(["print(21)"])
+
+ notebook_frontend.try_click_selector('//a[text()="Kernel"]', page=EDITOR_PAGE)
+ notebook_frontend.try_click_selector('#shutdown_kernel', page=EDITOR_PAGE)
+ notebook_frontend.try_click_selector('.btn.btn-default.btn-sm.btn-danger', page=EDITOR_PAGE)
+
+ notebook_frontend.execute_cell(0)
+
+ assert not notebook_frontend.is_kernel_running()
+ assert not notebook_frontend.get_cell_output()
diff --git a/nbclassic/tests/selenium/test_undelete.py b/nbclassic/tests/end_to_end/test_undelete.py
similarity index 75%
rename from nbclassic/tests/selenium/test_undelete.py
rename to nbclassic/tests/end_to_end/test_undelete.py
index d9823cda5..a38195622 100644
--- a/nbclassic/tests/selenium/test_undelete.py
+++ b/nbclassic/tests/end_to_end/test_undelete.py
@@ -1,8 +1,8 @@
-from selenium.webdriver.common.keys import Keys
-from .utils import shift
+from .utils import EDITOR_PAGE
+
def undelete(nb):
- nb.browser.execute_script('Jupyter.notebook.undelete_cell();')
+ nb.evaluate('() => Jupyter.notebook.undelete_cell();', page=EDITOR_PAGE)
INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")', 'print("d")']
@@ -15,13 +15,13 @@ def test_undelete_cells(prefill_notebook):
# Delete cells [1, 2]
notebook.focus_cell(1)
- shift(notebook.browser, Keys.DOWN)
- notebook.current_cell.send_keys('dd')
+ notebook.press('j', EDITOR_PAGE, ['Shift'])
+ notebook.press('d+d', EDITOR_PAGE)
assert notebook.get_cells_contents() == [a, d]
# Delete new cell 1 (which contains d)
notebook.focus_cell(1)
- notebook.current_cell.send_keys('dd')
+ notebook.press('d+d', EDITOR_PAGE)
assert notebook.get_cells_contents() == [a]
# Undelete d
@@ -38,16 +38,16 @@ def test_undelete_cells(prefill_notebook):
# Delete first two cells and restore
notebook.focus_cell(0)
- shift(notebook.browser, Keys.DOWN)
- notebook.current_cell.send_keys('dd')
+ notebook.press('j', EDITOR_PAGE, ['Shift'])
+ notebook.press('d+d', EDITOR_PAGE)
assert notebook.get_cells_contents() == [c, d]
undelete(notebook)
assert notebook.get_cells_contents() == [a, b, c, d]
# Delete last two cells and restore
notebook.focus_cell(-1)
- shift(notebook.browser, Keys.UP)
- notebook.current_cell.send_keys('dd')
+ notebook.press('k', EDITOR_PAGE, ['Shift'])
+ notebook.press('d+d', EDITOR_PAGE)
assert notebook.get_cells_contents() == [a, b]
undelete(notebook)
assert notebook.get_cells_contents() == [a, b, c, d]
@@ -55,8 +55,8 @@ def test_undelete_cells(prefill_notebook):
# Merge cells [1, 2], restore the deleted one
bc = b + "\n\n" + c
notebook.focus_cell(1)
- shift(notebook.browser, 'j')
- shift(notebook.browser, 'm')
+ notebook.press('j', EDITOR_PAGE, ['Shift'])
+ notebook.press('m', EDITOR_PAGE, ['Shift'])
assert notebook.get_cells_contents() == [a, bc, d]
undelete(notebook)
assert notebook.get_cells_contents() == [a, bc, c, d]
@@ -64,8 +64,8 @@ def test_undelete_cells(prefill_notebook):
# Merge cells [2, 3], restore the deleted one
cd = c + "\n\n" + d
notebook.focus_cell(-1)
- shift(notebook.browser, 'k')
- shift(notebook.browser, 'm')
+ notebook.press('k', EDITOR_PAGE, ['Shift'])
+ notebook.press('m', EDITOR_PAGE, ['Shift'])
assert notebook.get_cells_contents() == [a, bc, cd]
undelete(notebook)
assert notebook.get_cells_contents() == [a, bc, cd, d]
@@ -78,7 +78,7 @@ def test_undelete_cells(prefill_notebook):
# Merge cell below, restore the deleted one
ab = a + "\n\n" + b
notebook.focus_cell(0)
- notebook.browser.execute_script("Jupyter.notebook.merge_cell_below();")
+ notebook.evaluate("() => Jupyter.notebook.merge_cell_below();", page=EDITOR_PAGE)
assert notebook.get_cells_contents() == [ab, c, d]
undelete(notebook)
assert notebook.get_cells_contents() == [ab, b, c, d]
@@ -86,7 +86,7 @@ def test_undelete_cells(prefill_notebook):
# Merge cell above, restore the deleted one
cd = c + "\n\n" + d
notebook.focus_cell(-1)
- notebook.browser.execute_script("Jupyter.notebook.merge_cell_above();")
+ notebook.evaluate("() => Jupyter.notebook.merge_cell_above();", page=EDITOR_PAGE)
assert notebook.get_cells_contents() == [ab, b, cd]
undelete(notebook)
assert notebook.get_cells_contents() == [ab, b, c, cd]
diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py
new file mode 100644
index 000000000..4ed698f4b
--- /dev/null
+++ b/nbclassic/tests/end_to_end/utils.py
@@ -0,0 +1,916 @@
+"""Utility module for end_to_end testing.
+
+The primary utilities are:
+ * NotebookFrontend
+ * FrontendElement
+
+Users should use these utilities to write their tests,
+and avoid calling the underlying testing library directly.
+If you need to do something that isn't currently available,
+try to build it onto the utility classes instead of using
+playwright functionality/objects directly.
+
+This module was converted and refactored from the older
+selenium test suite.
+"""
+import copy
+import datetime
+import os
+import time
+import traceback
+
+from playwright.sync_api import ElementHandle, JSHandle
+from playwright.sync_api import expect
+
+
+# Key constants for browser_data
+BROWSER_CONTEXT = 'BROWSER_CONTEXT'
+TREE_PAGE = 'TREE_PAGE'
+EDITOR_PAGE = 'EDITOR_PAGE'
+SERVER_INFO = 'SERVER_INFO'
+BROWSER_OBJ = 'BROWSER_OBJ'
+# Other constants
+CELL_OUTPUT_SELECTOR = '.output_subarea'
+
+
+class TimeoutError(Exception):
+
+ def get_result(self):
+ return None if not self.args else self.args[0]
+
+
+class CellTypeError(ValueError):
+
+ def __init__(self, message=""):
+ self.message = message
+
+
+class FrontendError(Exception):
+ pass
+
+
+class FrontendElement:
+ """Performs high level tasks on frontend interface components
+
+ FrontendElement serves these goals:
+ - Offers some abstraction/hiding of the underlying testing
+ library (with the goal of making future refactors easier
+ through providing a single point of reimplementation via
+ this utility class rather than exposing implementation
+ details of the web library to individual tests)
+ - Unifies disparate library syntax for common functionalities
+
+ FrontendElement wraps JSHandle, Locator and ElementHandle from
+ playwright, and provides a unified endpoint for test writers
+ to grab attributes, inner text, find child elements etc.
+ """
+
+ def __init__(self, item, user_data=None):
+ """Wrap a frontend item.
+
+ :param item: JSHandle, Locator or ElementHandle
+ :param user_data: Mainly intended to hold cell data (like cell index),
+ pass a dict in and put what you want inside.
+ """
+ self._raw = item
+ self._element = item
+ self._bool = True # Was the item created successfully?
+ self._user_data = {} if user_data is None else user_data
+
+ # We need either a locator or an ElementHandle for most ops, obtain it
+ if item is None:
+ self._bool = False
+ if hasattr(item, 'count') and item.count() == 0:
+ self._bool = False
+ if not isinstance(item, ElementHandle) and isinstance(item, JSHandle):
+ as_element = item.as_element()
+ if as_element:
+ self._element = as_element
+ else:
+ self._bool = False
+ if isinstance(item, FrontendElement):
+ self._raw = item
+ self._element = item._element
+ self._bool = item._bool
+ self._user_data = copy.deepcopy(item._user_data)
+ self._user_data.update(user_data)
+
+ def __bool__(self):
+ """Returns True if construction succeeded"""
+ # (Quick/dirty )We can debug on failures by deferring bad inits and testing for them here
+ return self._bool
+
+ def click(self):
+ return self._element.click()
+
+ def get_inner_text(self):
+ return self._element.inner_text()
+
+ def get_inner_html(self):
+ return self._element.inner_html()
+
+ def get_attribute(self, attribute):
+ return self._element.get_attribute(attribute)
+
+ def get_computed_property(self, prop_name):
+ js = ("(element) => { return window.getComputedStyle(element)"
+ f".getPropertyValue('{prop_name}') }}")
+ return self._element.evaluate(js)
+
+ def evaluate(self, text):
+ return self._element.evaluate(text)
+
+ def wait_for(self, state):
+ if hasattr(self._element, 'wait_for_element_state'):
+ return self._element.wait_for_element_state(state=state)
+ elif hasattr(self._element, 'wait_for'):
+ return self._element.wait_for(state=state)
+ else:
+ raise FrontendError('Could not wait for state on element')
+
+ def focus(self):
+ self._element.focus()
+
+ def locate(self, selector):
+ """Locate child elements with the given selector"""
+ element = self._element
+
+ if hasattr(element, 'locator'):
+ result = element.locator(selector)
+ elif hasattr(element, 'query_selector'):
+ result = element.query_selector(selector)
+ else:
+ # TODO: FIX these - Raise exception or return None
+ result = None
+
+ return FrontendElement(result)
+
+ def locate_all(self, selector):
+ """Locate all child elements with the given selector"""
+ element = self._element
+
+ if hasattr(element, 'query_selector_all'):
+ matches = element.query_selector_all(selector)
+ element_list = [FrontendElement(item) for item in matches]
+ elif hasattr(element, 'locator'):
+ matches = element.locator(selector)
+ element_list = [FrontendElement(matches.nth(index)) for index in range(matches.count())]
+ else:
+ # TODO: FIX these - Raise exception or return None
+ element_list = None
+
+ return element_list
+
+ def type(self, text):
+ """Sends the given text as key presses to the element"""
+ return self._element.type(text)
+
+ def press(self, key):
+ """Send a key press to the element"""
+ return self._element.press(key)
+
+ def get_user_data(self):
+ """Currently this is an unmanaged user data area, use it as you please"""
+ return self._user_data
+
+ def expect_not_to_be_visible(self):
+ try:
+ expect(self._element).not_to_be_visible()
+ except ValueError as err:
+ raise Exception('Cannot expect not_to_be_visible on this type!') from err
+
+ def expect_to_have_text(self, text):
+ try:
+ expect(self._element).to_have_text(text)
+ except ValueError as err:
+ raise Exception('Cannot expect to have text on this type!') from err
+
+ def is_visible(self):
+ return self._element.is_visible()
+
+
+class NotebookFrontend:
+ """Performs high level Notebook tasks for automated testing.
+
+ NotebookFrontend serves these goals:
+ - Drives high level application tasks for testing
+ - Offers some encapsulation of the underlying testing
+ library, to allow test writers to focus their efforts
+ on application features rather than implementation
+ details for any given testing task
+
+ NotebookFrontend holds a _tree_page (Jupyter file browser), and
+ an _editor_page (a Python 3 notebook editor page), with the goal
+ of allowing test writers to drive any desired application tasks,
+ with the option of selecting a page in most methods.
+
+ Many tasks are accomplished by using the evaluate method to run
+ frontend Jupyter Javascript code on a selected page.
+
+ A cells property returns a list of the current notebook cells.
+
+ Other design notes: This class works together with FrontendElement
+ to abstract the testing library implementation away from test
+ writers. NotebookFrontend holds (private) handles to the underlying
+ browser/context.
+
+ TODO:
+ Possible future improvements, current limitations, etc
+ """
+
+ # Some constants for users of the class
+ TREE_PAGE = TREE_PAGE
+ EDITOR_PAGE = EDITOR_PAGE
+ CELL_OUTPUT_SELECTOR = CELL_OUTPUT_SELECTOR
+
+ def __init__(self, browser_data, existing_file_name=None):
+ """Start the Notebook app via the web UI or from a file.
+
+ If an existing_file_name is provided, the web interface
+ looks for and clicks the notebook entry in the tree page.
+ If not, the web interface will start a new Python3 notebook
+ from the kernel selection menu.
+
+ :param browser_data: Interfacing object to the web UI
+ :param str existing_file_name: An existing notebook filename to open
+ """
+ # Keep a reference to source data
+ self._browser_data = browser_data
+
+ # Define tree and editor attributes
+ self._tree_page = browser_data[TREE_PAGE]
+ self._editor_page = self._open_notebook_editor_page(existing_file_name)
+
+ # Do some needed frontend setup
+ self._wait_for_start()
+ self.disable_autosave_and_onbeforeunload()
+ self.current_cell = None # Defined/used below # TODO refactor/remove
+
+ def _wait_for_start(self):
+ """Wait until the notebook interface is loaded and the kernel started"""
+ def check_is_kernel_running():
+ try:
+ status = (self.is_jupyter_defined()
+ and self.is_notebook_defined()
+ and self.is_kernel_running())
+ except Exception:
+ return False
+
+ return status
+
+ self.wait_for_condition(check_is_kernel_running)
+
+ @property
+ def body(self):
+ return self._editor_page.locator("body")
+
+ @property
+ def _cells(self):
+ """Return a list of the current Notebook cells."""
+ return self._editor_page.query_selector_all(".cell")
+
+ @property
+ def cells(self):
+ """User facing cell list, gives a list of FrontendElement's"""
+ cells = [
+ FrontendElement(cell, user_data={'index': index})
+ for index, cell in enumerate(self._cells)
+ ]
+
+ return cells
+
+ @property
+ def current_index(self):
+ return self.index(self.current_cell)
+
+ def index(self, cell):
+ return self._cells.index(cell)
+
+ def press(self, keycode, page, modifiers=None):
+ """Press a key on the specified page
+
+ :param str keycode: The keycode value, see MDN
+ :param str page: The page name to run on
+ :param modifiers: A list of modifier keycode strings to press
+ """
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ mods = ""
+ if modifiers is not None:
+ mods = "+".join(m for m in modifiers)
+ mods += "+"
+
+ specified_page.keyboard.press(mods + keycode)
+
+ def type(self, text, page):
+ """Mimics a user typing the given text on the specified page"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+ specified_page.keyboard.type(text)
+
+ def insert_text(self, text, page):
+ """ """
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ specified_page.keyboard.insert_text(text)
+
+ def press_active(self, keycode, modifiers=None):
+ """Press a key on the current_cell"""
+ mods = ""
+ if modifiers is not None:
+ mods = "+".join(m for m in modifiers)
+ mods += "+"
+
+ self.current_cell.press(mods + keycode)
+
+ def type_active(self, text):
+ """Mimics a user typing the given text on the current_cell"""
+ self.current_cell.type(text)
+
+ def try_click_selector(self, selector, page):
+ """Attempts to find and click an element with the selector on the given page"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+ elem = specified_page.locator(selector)
+
+ elem.click()
+
+ def wait_for_selector(self, selector, page, state=None):
+ """Wait for the given selector (in the given state) on the specified page"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+ if state is not None:
+ return FrontendElement(specified_page.wait_for_selector(selector, state=state))
+
+ return FrontendElement(specified_page.wait_for_selector(selector))
+
+ def get_platform_modifier_key(self):
+ """Jupyter Notebook uses different modifier keys on win (Control) vs mac (Meta)"""
+ if os.uname()[0] == "Darwin":
+ return "Meta"
+ else:
+ return "Control"
+
+ def evaluate(self, text, page):
+ """Run some Javascript on the frontend in the given page
+
+ :param text: JS to evaluate
+ :param page: Page (str constant) to evaluate JS in
+ :return: The result of the evaluated JS
+ """
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ return specified_page.evaluate(text)
+
+ def _pause(self):
+ self._editor_page.pause()
+
+ def locate(self, selector, page):
+ """Find an element matching selector on the given page"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to locate from!')
+
+ return FrontendElement(specified_page.locator(selector))
+
+ def locate_all(self, selector, page):
+ """Find a list of elements matching the selector on the given page"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to locate from!')
+
+ # Get a locator, make a list of FrontendElement's for each match
+ result = specified_page.locator(selector)
+ element_list = [FrontendElement(result.nth(index)) for index in range(result.count())]
+ return element_list
+
+ def locate_and_focus(self, selector, page):
+ """Find selector in page and focus"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to locate from!')
+
+ locator = specified_page.locator(selector)
+ locator.focus()
+ return FrontendElement(locator)
+
+ def wait_for_frame(self, count=None, name=None, page=None):
+ """Waits for availability of a frame with the given name"""
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to wait for frame from!')
+
+ if name is not None:
+ def frame_wait():
+ frames = [f for f in specified_page.frames if f.name == name]
+ return frames
+ if count is not None:
+ def frame_wait():
+ frames = [f for f in specified_page.frames]
+ return len(frames) >= count
+
+ self.wait_for_condition(frame_wait)
+
+ def locate_in_frame(self, selector, page, frame_name=None, frame_index=None):
+ """Finds an element inside a frame"""
+ if frame_name is None and frame_index is None:
+ raise Exception('Error, must provide a frame name or frame index!')
+ if frame_name is not None and frame_index is not None:
+ raise Exception('Error, provide only one either frame name or frame index!')
+
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to locate in frame from!')
+
+ if frame_name is not None:
+ frame_matches = [f for f in specified_page.frames if f.name == frame_name]
+ if not frame_matches:
+ raise Exception('No frames found!')
+ frame = frame_matches[0]
+ if frame_index is not None:
+ frame = specified_page.frames[frame_index]
+
+ element = frame.wait_for_selector(selector)
+ return FrontendElement(element)
+
+ def wait_for_tag(self, tag, page=None, cell_index=None):
+ """Waits for availability of a given tag on the page"""
+ if cell_index is None and page is None:
+ raise FrontendError('Provide a page or cell to wait from!')
+ if cell_index is not None and page is not None:
+ raise FrontendError('Provide only one of [page, cell] to wait from!')
+
+ result = None
+ if page is not None:
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ result = specified_page.locator(tag)
+ if cell_index is not None:
+ result = self._cells[cell_index].wait_for_selector(tag)
+
+ return FrontendElement(result)
+
+ # TODO remove this
+ def _locate(self, selector, page):
+ """Find an frontend element by selector (Tag, CSS, XPath etc.)"""
+ result = None
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ return specified_page.locator(selector)
+
+ def clear_all_output(self):
+ """Clear all cell outputs"""
+ return self.evaluate(
+ "Jupyter.notebook.clear_all_output();",
+ page=EDITOR_PAGE
+ )
+
+ def clear_cell_output(self, index):
+ """Clear single cell output"""
+ JS = f'Jupyter.notebook.clear_output({index})'
+ self.evaluate(JS, page=EDITOR_PAGE)
+
+ def delete_all_cells(self):
+ # Note: After deleting all cells, a single default cell will remain
+
+ for _ in range(len(self._cells)):
+ self.delete_cell(0)
+
+ def populate(self, cell_texts):
+ """Delete all cells, then add cells using the list of specified cell_texts"""
+ self.delete_all_cells()
+
+ for _ in range(len(cell_texts) - 1): # Remove 1, there will already be 1 default cell
+ self.add_cell()
+ for index, txt in enumerate(cell_texts):
+ self.edit_cell(None, index, txt)
+
+ def click_toolbar_execute_btn(self):
+ """Mimics a user pressing the execute button in the UI"""
+ execute_button = self._editor_page.locator(
+ "button["
+ "data-jupyter-action="
+ "'jupyter-notebook:run-cell-and-select-next'"
+ "]"
+ )
+ execute_button.click()
+
+ 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.evaluate("window.onbeforeunload = null;", page=EDITOR_PAGE)
+ self.evaluate("Jupyter.notebook.set_autosave_interval(0)", page=EDITOR_PAGE)
+
+ def to_command_mode(self):
+ """Changes us into command mode on currently focused cell"""
+ self.body.press('Escape')
+ self.evaluate(" () => { return Jupyter.notebook.handle_command_mode("
+ "Jupyter.notebook.get_cell("
+ "Jupyter.notebook.get_edit_index())) }", page=EDITOR_PAGE)
+
+ def focus_cell(self, index=0):
+ """Mimic a user focusing on the given cell"""
+ cell = self._cells[index]
+ cell.click()
+ self.to_command_mode()
+ self.current_cell = cell
+
+ def select_cell_range(self, initial_index=0, final_index=0):
+ self.focus_cell(initial_index)
+ self.to_command_mode()
+ for i in range(final_index - initial_index):
+ self.press('j', EDITOR_PAGE, ['Shift'])
+
+ def find_and_replace(self, index=0, find_txt='', replace_txt=''):
+ """Uses Jupyter's find and replace"""
+ self.focus_cell(index)
+ self.to_command_mode()
+ self.press('f', EDITOR_PAGE)
+ self._editor_page.locator('#find-and-replace')
+ self._editor_page.locator('#findreplace_allcells_btn').click()
+ find_input = self.locate('#findreplace_find_inp', page=EDITOR_PAGE)
+ # find and replace fields are HTML input elements, use .value to get/set text
+ find_input.evaluate(f"(elem) => {{ elem.value = '{find_txt}'; return elem.value; }}")
+ self.wait_for_condition(
+ lambda: find_input.evaluate(
+ '(elem) => { return elem.value; }') == find_txt,
+ timeout=30,
+ period=5
+ )
+ rep_input = self.locate('#findreplace_replace_inp', page=EDITOR_PAGE)
+ rep_input.evaluate(f"(elem) => {{ elem.value = '{replace_txt}'; return elem.value; }}")
+ self.wait_for_condition(
+ lambda: rep_input.evaluate('(elem) => { return elem.value; }') == replace_txt,
+ timeout=30,
+ period=5
+ )
+ self._editor_page.locator('#findreplace_replaceall_btn').click()
+
+ 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.press("m")
+ elif cell_type == "raw":
+ self.current_cell.press("r")
+ elif cell_type == "code":
+ self.current_cell.press("y")
+ else:
+ raise CellTypeError(f"{cell_type} is not a valid cell type,use 'code', 'markdown', or 'raw'")
+
+ 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.
+ """
+ cell.wait_for_element_state('hidden')
+
+ def get_cells_contents(self):
+ """Get a list of the text inside each cell"""
+ JS = '() => { return Jupyter.notebook.get_cells().map(function(c) {return c.get_text();}) }'
+ return self.evaluate(JS, page=EDITOR_PAGE)
+
+ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'):
+ """Get the text inside a given cell"""
+ return self._cells[index].query_selector(selector).inner_text()
+
+ def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR):
+ """Get the cell output for a given cell"""
+ cell = self._cells[index].as_element().query_selector(output) # Find cell child elements
+
+ if cell is None:
+ return None
+
+ element = FrontendElement(cell, user_data={'index': index})
+
+ return element
+
+ def wait_for_condition(self, check_func, timeout=30, period=.1):
+ """Wait for check_func to return a truthy value, return it or raise an exception upon timeout"""
+ # TODO refactor/remove
+
+ begin = datetime.datetime.now()
+ while (datetime.datetime.now() - begin).seconds < timeout:
+ try:
+ condition = check_func()
+ if condition:
+ return condition
+ time.sleep(period)
+ except Exception as err:
+ # Log (print) exception and continue
+ traceback.print_exc()
+ print('\n[NotebookFrontend] Ignoring exception in wait_for_condition, read more above')
+ else:
+ raise TimeoutError()
+
+ def wait_for_cell_output(self, index=0, timeout=30):
+ """Waits for the cell to finish executing and return the cell output"""
+ if not self._cells:
+ raise Exception('Error, no cells exist!')
+
+ milliseconds_to_seconds = 1000
+ cell = self._cells[index].as_element()
+ try:
+ cell.wait_for_selector(CELL_OUTPUT_SELECTOR, timeout=timeout * milliseconds_to_seconds)
+ except Exception:
+ # None were found / timeout
+ pass
+
+ return self.get_cell_output(index=index)
+
+ def set_cell_metadata(self, index, key, value):
+ JS = f'Jupyter.notebook.get_cell({index}).metadata.{key} = {value}'
+ return self.evaluate(JS, page=EDITOR_PAGE)
+
+ def get_cell_type(self, index=0):
+ JS = f'() => {{ return Jupyter.notebook.get_cell({index}).cell_type }}'
+ return self.evaluate(JS, page=EDITOR_PAGE)
+
+ def set_cell_input_prompt(self, index, prmpt_val):
+ JS = f'Jupyter.notebook.get_cell({index}).set_input_prompt({prmpt_val})'
+ self.evaluate(JS, page=EDITOR_PAGE)
+
+ def edit_cell(self, cell=None, index=0, content="", render=False):
+ """Set the contents of a cell to *content*, by cell object or by index
+ """
+ if cell is not None:
+ index = self.index(cell)
+ self.focus_cell(index)
+
+ # Select & delete anything already in the cell
+ self.press('Enter', EDITOR_PAGE)
+ self.press('a', EDITOR_PAGE, [self.get_platform_modifier_key()])
+ self.press('Delete', EDITOR_PAGE)
+
+ self.type(content, page=EDITOR_PAGE)
+ if render:
+ self.execute_cell(index)
+
+ def execute_cell(self, cell_or_index=None):
+ if isinstance(cell_or_index, int):
+ index = cell_or_index
+ elif isinstance(cell_or_index, ElementHandle):
+ # TODO: This probably doesn't work, fix/check
+ index = self.index(cell_or_index)
+ else:
+ raise TypeError("execute_cell only accepts an ElementHandle or an int")
+ self.focus_cell(index)
+ self.current_cell.press("Control+Enter")
+
+ def add_cell(self, index=-1, cell_type="code", content=""):
+ # TODO fix/respect cell_type arg
+ self.focus_cell(index)
+ self.current_cell.press("b")
+ new_index = index + 1 if index >= 0 else index
+ if content:
+ self.edit_cell(index=index, content=content)
+ if cell_type != 'code':
+ self.convert_cell_type(index=new_index, cell_type=cell_type)
+
+ def add_and_execute_cell(self, index=-1, cell_type="code", content=""):
+ self.add_cell(index=index, cell_type=cell_type, content=content)
+ self.execute_cell(index)
+
+ def delete_cell(self, index):
+ self.focus_cell(index)
+ self.to_command_mode()
+ self.current_cell.type('dd')
+
+ 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 value in values:
+ if isinstance(value, str):
+ self.add_cell(cell_type=cell_type,
+ content=value)
+ else:
+ raise TypeError(f"Don't know how to add cell from {value!r}")
+
+ def is_jupyter_defined(self):
+ """Checks that the Jupyter object is defined on the frontend"""
+ return self.evaluate(
+ "() => {"
+ " try {"
+ " return Jupyter != false;"
+ " } catch (e) {"
+ " return false;"
+ " }"
+ "}",
+ page=EDITOR_PAGE
+ )
+
+ def is_notebook_defined(self):
+ """Checks that the Jupyter.notebook object is defined on the frontend"""
+ return self.evaluate(
+ "() => {"
+ " try {"
+ " return Jupyter.notebook != false;"
+ " } catch (e) {"
+ " return false;"
+ " }"
+ "}",
+ page=EDITOR_PAGE
+ )
+
+ def is_kernel_running(self):
+ return self.evaluate(
+ "() => { return Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected() }",
+ page=EDITOR_PAGE
+ )
+
+ def wait_for_kernel_ready(self):
+ self._editor_page.wait_for_selector(".kernel_idle_icon")
+
+ def _open_notebook_editor_page(self, existing_file_name=None):
+ tree_page = self._tree_page
+
+ if existing_file_name is not None:
+ existing_notebook = tree_page.locator(f"text={existing_file_name}")
+ existing_notebook.click()
+ self._tree_page.reload() # TODO: FIX this, page count does not update to 2
+ else:
+ # Simulate a user opening a new notebook/kernel
+ new_dropdown_element = tree_page.locator('#new-dropdown-button')
+ new_dropdown_element.click()
+ kernel_name = 'kernel-python3'
+ kernel_selector = f'#{kernel_name} a'
+ new_notebook_element = tree_page.locator(kernel_selector)
+ new_notebook_element.click()
+
+ def wait_for_new_page():
+ return [pg for pg in self._browser_data[BROWSER_CONTEXT].pages if 'tree' not in pg.url]
+
+ new_pages = self.wait_for_condition(wait_for_new_page)
+ editor_page = new_pages[0]
+ return editor_page
+
+ def get_page_url(self, page):
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ return specified_page.url
+
+ def go_back(self, page):
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ return specified_page.go_back()
+
+ def get_server_info(self):
+ return self._browser_data[SERVER_INFO]['url']
+
+ def navigate_to(self, page, partial_url):
+ if page == TREE_PAGE:
+ specified_page = self._tree_page
+ elif page == EDITOR_PAGE:
+ specified_page = self._editor_page
+ else:
+ raise Exception('Error, provide a valid page to evaluate from!')
+
+ specified_page.goto(self._browser_data[SERVER_INFO]['url'] + partial_url)
+
+ # TODO: Refactor/consider removing this (legacy cruft)
+ @classmethod
+ def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', existing_file_name=None):
+ instance = cls(browser_data, existing_file_name)
+
+ return instance
+
+
+def validate_dualmode_state(notebook, mode, index):
+ """Validate the entire dual mode state of the notebook.
+ Checks if the specified cell is selected, and the mode and keyboard mode are the same.
+ Depending on the mode given:
+ Command: Checks that no cells are in focus or in edit mode.
+ Edit: Checks that only the specified cell is in focus and in edit mode.
+ """
+ def is_only_cell_edit(index):
+ JS = '() => { return Jupyter.notebook.get_cells().map(function(c) {return c.mode;}) }'
+ cells_mode = notebook.evaluate(JS, EDITOR_PAGE)
+ # None of the cells are in edit mode
+ if index is None:
+ for mode in cells_mode:
+ if mode == 'edit':
+ return False
+ return True
+ # Only the index cell is on edit mode
+ for i, mode in enumerate(cells_mode):
+ if i == index:
+ if mode != 'edit':
+ return False
+ else:
+ if mode == 'edit':
+ return False
+ return True
+
+ def is_focused_on(index):
+ JS = "() => { return $('#notebook .CodeMirror-focused textarea').length; }"
+ focused_cells = notebook.evaluate(JS, EDITOR_PAGE)
+ if index is None:
+ return focused_cells == 0
+
+ if focused_cells != 1: # only one cell is focused
+ return False
+
+ JS = "() => { return $('#notebook .CodeMirror-focused textarea')[0]; }"
+ focused_cell = notebook.evaluate(JS, EDITOR_PAGE)
+ JS = f"() => {{ return IPython.notebook.get_cell({index}).code_mirror.getInputField() }}"
+ cell = notebook.evaluate(JS, EDITOR_PAGE)
+ return focused_cell == cell
+
+ # general test
+ JS = "() => { return IPython.keyboard_manager.mode; }"
+ keyboard_mode = notebook.evaluate(JS, EDITOR_PAGE)
+ JS = "() => { return IPython.notebook.mode; }"
+ notebook_mode = notebook.evaluate(JS, EDITOR_PAGE)
+
+ # validate selected cell
+ JS = "() => { return Jupyter.notebook.get_selected_cells_indices(); }"
+ cell_index = notebook.evaluate(JS, EDITOR_PAGE)
+ assert cell_index == [index] # only the index cell is selected
+
+ if mode != 'command' and mode != 'edit':
+ raise Exception('An unknown mode was send: mode = "%s"'%mode) # An unknown mode is send
+
+ # validate mode
+ assert mode == keyboard_mode # keyboard mode is correct
+
+ if mode == 'command':
+ assert is_focused_on(None) # no focused cells
+
+ assert is_only_cell_edit(None) # no cells in edit mode
+
+ elif mode == 'edit':
+ assert is_focused_on(index) # The specified cell is focused
+
+ assert is_only_cell_edit(index) # The specified cell is the only one in edit mode
diff --git a/nbclassic/tests/selenium/conftest.py b/nbclassic/tests/selenium/conftest.py
deleted file mode 100644
index 07747b2cf..000000000
--- a/nbclassic/tests/selenium/conftest.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import json
-import nbformat
-from nbformat.v4 import new_notebook, new_code_cell
-import os
-import pytest
-import requests
-from subprocess import Popen
-import sys
-from tempfile import mkstemp
-from testpath.tempdir import TemporaryDirectory
-import time
-from urllib.parse import urljoin
-
-from selenium.webdriver import Firefox, Remote, Chrome
-from .utils import Notebook
-
-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):
- if proc.poll() is not None:
- raise RuntimeError("Notebook server failed to start")
- if os.path.exists(info_file_path):
- try:
- with open(info_file_path) as f:
- return json.load(f)
- except ValueError:
- # If the server is halfway through writing the file, we may
- # get invalid JSON; it should be ready next iteration.
- pass
- time.sleep(0.1)
- raise RuntimeError("Didn't find %s in 30 seconds", info_file_path)
-
-
-@pytest.fixture(scope='session')
-def notebook_server():
- info = {}
- with TemporaryDirectory() as td:
- nbdir = info['nbdir'] = pjoin(td, 'notebooks')
- os.makedirs(pjoin(nbdir, 'sub ∂ir1', 'sub ∂ir 1a'))
- os.makedirs(pjoin(nbdir, 'sub ∂ir2', 'sub ∂ir 1b'))
-
- info['extra_env'] = {
- 'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'),
- 'JUPYTER_RUNTIME_DIR': pjoin(td, 'jupyter_runtime'),
- 'IPYTHONDIR': pjoin(td, 'ipython'),
- }
- env = os.environ.copy()
- env.update(info['extra_env'])
-
- command = [sys.executable, '-m', 'nbclassic',
- '--no-browser',
- '--notebook-dir', nbdir,
- # run with a base URL that would be escaped,
- # to test that we don't double-escape URLs
- '--ServerApp.base_url=/a@b/',
- ]
- print("command=", command)
- proc = info['popen'] = Popen(command, cwd=nbdir, env=env)
- info_file_path = pjoin(td, 'jupyter_runtime',
- f'jpserver-{proc.pid:d}.json')
- info.update(_wait_for_server(proc, info_file_path))
-
- print("Notebook server info:", info)
- yield info
-
- # Shut the server down
- 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 = f"{username}:{access_key}@localhost:4445"
- print("Connecting remote driver on Sauce Labs")
- driver = Remote(desired_capabilities=capabilities,
- command_executor=f"http://{hub_url}/wd/hub")
- return driver
-
-
-@pytest.fixture(scope='session')
-def selenium_driver():
- if os.environ.get('SAUCE_USERNAME'):
- driver = make_sauce_driver()
- elif os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome':
- driver = Chrome()
- else:
- driver = Firefox()
-
- yield driver
-
- # Teardown
- driver.quit()
-
-
-@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))
- return selenium_driver
-
-@pytest.fixture
-def notebook(authenticated_browser):
- tree_wh = authenticated_browser.current_window_handle
- yield Notebook.new_notebook(authenticated_browser)
- authenticated_browser.switch_to.window(tree_wh)
-
-@pytest.fixture
-def prefill_notebook(selenium_driver, notebook_server):
- def inner(cells):
- cells = [new_code_cell(c) if isinstance(c, str) else c
- for c in cells]
- nb = new_notebook(cells=cells)
- fd, path = mkstemp(dir=notebook_server['nbdir'], suffix='.ipynb')
- with open(fd, 'w', encoding='utf-8') as f:
- nbformat.write(nb, f)
- fname = os.path.basename(path)
- selenium_driver.get(
- "{url}notebooks/{}?token={token}".format(fname, **notebook_server)
- )
- return Notebook(selenium_driver)
-
- return inner
diff --git a/nbclassic/tests/selenium/quick_selenium.py b/nbclassic/tests/selenium/quick_selenium.py
deleted file mode 100644
index 2b3a66515..000000000
--- a/nbclassic/tests/selenium/quick_selenium.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""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 nbclassic.tests.selenium.utils import Notebook
-from jupyter_server.serverapp 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 nbclassic.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') from e
- 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 nbclassic.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/nbclassic/tests/selenium/test_buffering.py b/nbclassic/tests/selenium/test_buffering.py
deleted file mode 100644
index 4e4f45692..000000000
--- a/nbclassic/tests/selenium/test_buffering.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Tests buffering of execution requests."""
-
-from .utils import wait_for_selector
-
-
-def wait_for_cell_text_output(notebook, index):
- cell = notebook.cells[index]
- output = wait_for_selector(cell, ".output_text", single=True)
- return output.text
-
-
-def wait_for_kernel_ready(notebook):
- wait_for_selector(notebook.browser, ".kernel_idle_icon")
-
-
-def test_kernels_buffer_without_conn(prefill_notebook):
- """Test that execution request made while disconnected is buffered."""
- notebook = prefill_notebook(["print(1 + 2)"])
-
- wait_for_kernel_ready(notebook)
- notebook.browser.execute_script("IPython.notebook.kernel.stop_channels();")
- notebook.execute_cell(0)
- notebook.browser.execute_script("IPython.notebook.kernel.reconnect();")
- wait_for_kernel_ready(notebook)
-
- assert wait_for_cell_text_output(notebook, 0) == "3"
-
-
-def test_buffered_cells_execute_in_order(prefill_notebook):
- """Test that buffered requests execute in order."""
- notebook = prefill_notebook(['', 'k=1', 'k+=1', 'k*=3', 'print(k)'])
-
- # Repeated execution of cell queued up in the kernel executes
- # each execution request in order.
- wait_for_kernel_ready(notebook)
- notebook.browser.execute_script("IPython.notebook.kernel.stop_channels();")
- # k == 1
- notebook.execute_cell(1)
- # k == 2
- notebook.execute_cell(2)
- # k == 6
- notebook.execute_cell(3)
- # k == 7
- notebook.execute_cell(2)
- notebook.execute_cell(4)
- notebook.browser.execute_script("IPython.notebook.kernel.reconnect();")
- wait_for_kernel_ready(notebook)
-
- # Check that current value of k is 7
- assert wait_for_cell_text_output(notebook, 4) == "7"
diff --git a/nbclassic/tests/selenium/test_clipboard_multiselect.py b/nbclassic/tests/selenium/test_clipboard_multiselect.py
deleted file mode 100644
index e75380c8e..000000000
--- a/nbclassic/tests/selenium/test_clipboard_multiselect.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Tests clipboard by copying, cutting and pasting multiple cells"""
-from selenium.webdriver.common.keys import Keys
-from .utils import wait_for_selector, wait_for_xpath
-
-def test_clipboard_multiselect(prefill_notebook):
- notebook = prefill_notebook(['', '1', '2', '3', '4', '5a', '6b', '7c', '8d'])
-
- assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '6b', '7c', '8d']
-
- # Select the first 3 cells with value and replace the last 3
- [notebook.body.send_keys(Keys.UP) for i in range(8)]
- notebook.select_cell_range(1, 3)
- notebook.body.send_keys("c")
- notebook.select_cell_range(6, 8)
- wait_for_xpath(notebook.browser, '//a[text()="Edit"]', single=True).click()
- wait_for_selector(notebook.browser, '#paste_cell_replace', single=True).click()
-
- assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '1', '2', '3']
-
- # Select the last four cells, cut them and paste them below the first cell
- notebook.select_cell_range(5, 8)
- wait_for_selector(notebook.browser, '.fa-cut.fa', single=True).click()
- for i in range(8):
- notebook.body.send_keys(Keys.UP)
- notebook.body.send_keys("v")
-
- assert notebook.get_cells_contents() == ['', '5a', '1', '2', '3', '1', '2', '3', '4']
diff --git a/nbclassic/tests/selenium/test_dashboard_nav.py b/nbclassic/tests/selenium/test_dashboard_nav.py
deleted file mode 100644
index 57b4634f5..000000000
--- a/nbclassic/tests/selenium/test_dashboard_nav.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import os
-
-from jupyter_server.utils import url_path_join
-from nbclassic.tests.selenium.utils import wait_for_selector
-pjoin = os.path.join
-
-
-class PageError(Exception):
- """Error for an action being incompatible with the current jupyter web page."""
- def __init__(self, message):
- self.message = message
-
-
-def url_in_tree(browser, url=None):
- if url is None:
- url = browser.current_url
- tree_url = url_path_join(browser.jupyter_server_info['url'], 'tree')
- return url.startswith(tree_url)
-
-
-def get_list_items(browser):
- """Gets list items from a directory listing page
-
- Raises PageError if not in directory listing page (url has tree in it)
- """
- if not url_in_tree(browser):
- raise PageError("You are not in the notebook's file tree view."
- "This function can only be used the file tree context.")
- # we need to make sure that at least one item link loads
- wait_for_selector(browser, '.item_link')
-
- return [{
- 'link': a.get_attribute('href'),
- 'label': a.find_element_by_class_name('item_name').text,
- '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"""
- items = get_list_items(browser)
- return [i for i in items
- if url_in_tree(browser, i['link']) and i['label'] != '..']
-
-def test_items(authenticated_browser):
- visited_dict = {}
- # Going down the tree to collect links
- while True:
- wait_for_selector(authenticated_browser, '.item_link')
- current_url = authenticated_browser.current_url
- items = visited_dict[current_url] = only_dir_links(authenticated_browser)
- try:
- item = items[0]
- item["element"].click()
- assert authenticated_browser.current_url == item['link']
- except IndexError:
- break
- # Going back up the tree while we still have unvisited links
- while visited_dict:
- current_items = only_dir_links(authenticated_browser)
- current_items_links = [item["link"] for item in current_items]
- stored_items = visited_dict.pop(authenticated_browser.current_url)
- stored_items_links = [item["link"] for item in stored_items]
- assert stored_items_links == current_items_links
- authenticated_browser.back()
-
diff --git a/nbclassic/tests/selenium/test_deletecell.py b/nbclassic/tests/selenium/test_deletecell.py
deleted file mode 100644
index 0e60adfaa..000000000
--- a/nbclassic/tests/selenium/test_deletecell.py
+++ /dev/null
@@ -1,57 +0,0 @@
-def cell_is_deletable(nb, index):
- JS = f'return Jupyter.notebook.get_cell({index}).is_deletable();'
- return nb.browser.execute_script(JS)
-
-def remove_all_cells(notebook):
- for i in range(len(notebook.cells)):
- notebook.delete_cell(0)
-
-INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
-
-def test_delete_cells(prefill_notebook):
- a, b, c = INITIAL_CELLS
- notebook = prefill_notebook(INITIAL_CELLS)
-
- # Validate initial state
- assert notebook.get_cells_contents() == [a, b, c]
- for cell in range(0, 3):
- assert cell_is_deletable(notebook, cell)
-
- notebook.set_cell_metadata(0, 'deletable', 'false')
- notebook.set_cell_metadata(1, 'deletable', 0
- )
- assert not cell_is_deletable(notebook, 0)
- assert cell_is_deletable(notebook, 1)
- assert cell_is_deletable(notebook, 2)
-
- # Try to delete cell a (should not be deleted)
- notebook.delete_cell(0)
- assert notebook.get_cells_contents() == [a, b, c]
-
- # Try to delete cell b (should succeed)
- notebook.delete_cell(1)
- assert notebook.get_cells_contents() == [a, c]
-
- # Try to delete cell c (should succeed)
- notebook.delete_cell(1)
- assert notebook.get_cells_contents() == [a]
-
- # Change the deletable state of cell a
- notebook.set_cell_metadata(0, 'deletable', 'true')
-
- # Try to delete cell a (should succeed)
- notebook.delete_cell(0)
- assert len(notebook.cells) == 1 # it contains an empty cell
-
- # Make sure copied cells are deletable
- notebook.edit_cell(index=0, content=a)
- notebook.set_cell_metadata(0, 'deletable', 'false')
- assert not cell_is_deletable(notebook, 0)
- notebook.to_command_mode()
- notebook.current_cell.send_keys('cv')
- assert len(notebook.cells) == 2
- assert cell_is_deletable(notebook, 1)
-
- notebook.set_cell_metadata(0, 'deletable', 'true') # to perform below test, remove all the cells
- remove_all_cells(notebook)
- assert len(notebook.cells) == 1 # notebook should create one automatically on empty notebook
diff --git a/nbclassic/tests/selenium/test_display_image.py b/nbclassic/tests/selenium/test_display_image.py
deleted file mode 100644
index 4e3adfd59..000000000
--- a/nbclassic/tests/selenium/test_display_image.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""Test display of images
-
-The effect of shape metadata is validated, using Image(retina=True)
-"""
-
-from .utils import wait_for_tag
-
-
-# 2x2 black square in b64 jpeg and png
-b64_image_data = {
- "image/png" : b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAC0lEQVR4nGNgQAYAAA4AAamRc7EA\\nAAAASUVORK5CYII',
- "image/jpeg" : b'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a\nHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy\nMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAACAAIDASIA\nAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA\nAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3\nODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm\np6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA\nAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx\nBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK\nU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3\nuLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5/ooo\noAoo2Qoo'
-}
-
-
-def imports(notebook):
- commands = [
- 'import base64',
- 'from IPython.display import display, Image',
- ]
- notebook.edit_cell(index=0, content="\n".join(commands))
- notebook.execute_cell(0)
-
-
-def validate_img(notebook, cell_index, image_fmt, retina):
- """Validate that image renders as expected."""
-
- b64data = b64_image_data[image_fmt]
- commands = [
- f'b64data = {b64data}',
- 'data = base64.decodebytes(b64data)',
- f'display(Image(data, retina={retina}))'
- ]
- notebook.append("\n".join(commands))
- notebook.execute_cell(cell_index)
-
- # Find the image element that was just displayed
- wait_for_tag(notebook.cells[cell_index], "img", single=True)
- img_element = notebook.cells[cell_index].find_element_by_tag_name("img")
-
- src = img_element.get_attribute("src")
- prefix = src.split(',')[0]
- expected_prefix = f"data:{image_fmt};base64"
- assert prefix == expected_prefix
-
- expected_size = 1 if retina else 2
- assert img_element.size["width"] == expected_size
- assert img_element.size["height"] == expected_size
- assert img_element.get_attribute("width") == str(expected_size)
- assert img_element.get_attribute("height") == str(expected_size)
-
-
-def test_display_image(notebook):
- imports(notebook)
- # PNG, non-retina
- validate_img(notebook, 1, "image/png", False)
-
- # PNG, retina display
- validate_img(notebook, 2, "image/png", True)
-
- # JPEG, non-retina
- validate_img(notebook, 3, "image/jpeg", False)
-
- # JPEG, retina display
- validate_img(notebook, 4, "image/jpeg", True)
diff --git a/nbclassic/tests/selenium/test_dualmode_arrows.py b/nbclassic/tests/selenium/test_dualmode_arrows.py
deleted file mode 100644
index c881f053c..000000000
--- a/nbclassic/tests/selenium/test_dualmode_arrows.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""Tests arrow keys on both command and edit mode"""
-from selenium.webdriver.common.keys import Keys
-
-def test_dualmode_arrows(notebook):
-
- # Tests in command mode.
- # Setting up the cells to test the keys to move up.
- notebook.to_command_mode()
- [notebook.body.send_keys("b") for i in range(3)]
-
- # Use both "k" and up arrow keys to moving up and enter a value.
- # Once located on the top cell, use the up arrow keys to prove the top cell is still selected.
- notebook.body.send_keys("k")
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("2")
- notebook.to_command_mode()
- notebook.body.send_keys(Keys.UP)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("1")
- notebook.to_command_mode()
- notebook.body.send_keys("k")
- notebook.body.send_keys(Keys.UP)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("0")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0", "1", "2", ""]
-
- # Use the "k" key on the top cell as well
- notebook.body.send_keys("k")
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys(" edit #1")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0 edit #1", "1", "2", ""]
-
- # Setting up the cells to test the keys to move down
- [notebook.body.send_keys("j") for i in range(3)]
- [notebook.body.send_keys("a") for i in range(2)]
- notebook.body.send_keys("k")
-
- # Use both "j" key and down arrow keys to moving down and enter a value.
- # Once located on the bottom cell, use the down arrow key to prove the bottom cell is still selected.
- notebook.body.send_keys(Keys.DOWN)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("3")
- notebook.to_command_mode()
- notebook.body.send_keys("j")
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("4")
- notebook.to_command_mode()
- notebook.body.send_keys("j")
- notebook.body.send_keys(Keys.DOWN)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys("5")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5"]
-
- # Use the "j" key on the top cell as well
- notebook.body.send_keys("j")
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys(" edit #1")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1"]
-
- # On the bottom cell, use both left and right arrow keys to prove the bottom cell is still selected.
- notebook.body.send_keys(Keys.LEFT)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys(", #2")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1, #2"]
- notebook.body.send_keys(Keys.RIGHT)
- notebook.body.send_keys(Keys.ENTER)
- notebook.body.send_keys(" and #3")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0 edit #1", "1", "2", "3", "4", "5 edit #1, #2 and #3"]
-
-
- # Tests in edit mode.
- # First, erase the previous content and then setup the cells to test the keys to move up.
- [notebook.browser.find_element_by_class_name("fa-cut.fa").click() for i in range(6)]
- [notebook.body.send_keys("b") for i in range(2)]
- notebook.body.send_keys("a")
- notebook.body.send_keys(Keys.ENTER)
-
- # Use the up arrow key to move down and enter a value.
- # We will use the left arrow key to move one char to the left since moving up on last character only moves selector to the first one.
- # Once located on the top cell, use the up arrow key to prove the top cell is still selected.
- notebook.body.send_keys(Keys.UP)
- notebook.body.send_keys("1")
- notebook.body.send_keys(Keys.LEFT)
- [notebook.body.send_keys(Keys.UP) for i in range(2)]
- notebook.body.send_keys("0")
-
- # Use the down arrow key to move down and enter a value.
- # We will use the right arrow key to move one char to the right since moving down puts selector to the last character.
- # Once located on the bottom cell, use the down arrow key to prove the bottom cell is still selected.
- notebook.body.send_keys(Keys.DOWN)
- notebook.body.send_keys(Keys.RIGHT)
- notebook.body.send_keys(Keys.DOWN)
- notebook.body.send_keys("2")
- [notebook.body.send_keys(Keys.DOWN) for i in range(2)]
- notebook.body.send_keys("3")
- notebook.to_command_mode()
- assert notebook.get_cells_contents() == ["0", "1", "2", "3"]
\ No newline at end of file
diff --git a/nbclassic/tests/selenium/test_dualmode_cellmode.py b/nbclassic/tests/selenium/test_dualmode_cellmode.py
deleted file mode 100644
index c89d6acd9..000000000
--- a/nbclassic/tests/selenium/test_dualmode_cellmode.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""Test keyboard shortcuts that change the cell's mode."""
-
-def test_dualmode_cellmode(notebook):
- def get_cell_cm_mode(index):
- code_mirror_mode = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cell(%s).code_mirror.getMode().name;"%index)
- return code_mirror_mode
-
-
- index = 0
- a = 'hello\nmulti\nline'
-
- notebook.edit_cell(index=index, content=a)
-
- """check for the default cell type"""
- notebook.to_command_mode()
- notebook.body.send_keys("r")
- assert notebook.get_cell_type(index) == 'raw'
- assert get_cell_cm_mode(index) == 'null'
-
- """check cell type after changing to markdown"""
- notebook.body.send_keys("1")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '# ' + a
- assert get_cell_cm_mode(index) == 'ipythongfm'
-
- notebook.body.send_keys("2")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '## ' + a
-
- notebook.body.send_keys("3")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '### ' + a
-
- notebook.body.send_keys("4")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '#### ' + a
-
- notebook.body.send_keys("5")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '##### ' + a
-
- notebook.body.send_keys("6")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '###### ' + a
-
- notebook.body.send_keys("m")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '###### ' + a
-
- notebook.body.send_keys("y")
- assert notebook.get_cell_type(index) == 'code'
- assert notebook.get_cell_contents(index) == '###### ' + a
- assert get_cell_cm_mode(index) == 'ipython'
-
- notebook.body.send_keys("1")
- assert notebook.get_cell_type(index) == 'markdown'
- assert notebook.get_cell_contents(index) == '# ' + a
\ No newline at end of file
diff --git a/nbclassic/tests/selenium/test_dualmode_clipboard.py b/nbclassic/tests/selenium/test_dualmode_clipboard.py
deleted file mode 100644
index 66d718d91..000000000
--- a/nbclassic/tests/selenium/test_dualmode_clipboard.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""Test"""
-from .utils import shift, validate_dualmode_state
-
-INITIAL_CELLS = ['', 'print("a")', 'print("b")', 'print("c")']
-
-def test_dualmode_clipboard(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
- _, a, b, c = INITIAL_CELLS
- for i in range(1, 4):
- notebook.execute_cell(i)
-
- #Copy/past/cut
- num_cells = len(notebook.cells)
- assert notebook.get_cell_contents(1) == a #Cell 1 is a
-
- notebook.focus_cell(1)
- notebook.body.send_keys("x") #Cut
- validate_dualmode_state(notebook, 'command', 1)
- assert notebook.get_cell_contents(1) == b #Cell 2 is now where cell 1 was
- assert len(notebook.cells) == num_cells-1 #A cell was removed
-
- notebook.focus_cell(2)
- notebook.body.send_keys("v") #Paste
- validate_dualmode_state(notebook, 'command', 3)
- assert notebook.get_cell_contents(3) == a #Cell 3 has the cut contents
- assert len(notebook.cells) == num_cells #A cell was added
-
- notebook.body.send_keys("v") #Paste
- validate_dualmode_state(notebook, 'command', 4)
- assert notebook.get_cell_contents(4) == a #Cell a has the cut contents
- assert len(notebook.cells) == num_cells+1 #A cell was added
-
- notebook.focus_cell(1)
- notebook.body.send_keys("c") #Copy
- validate_dualmode_state(notebook, 'command', 1)
- assert notebook.get_cell_contents(1) == b #Cell 1 is b
-
- notebook.focus_cell(2)
- notebook.body.send_keys("c") #Copy
- validate_dualmode_state(notebook, 'command', 2)
- assert notebook.get_cell_contents(2) == c #Cell 2 is c
-
- notebook.focus_cell(4)
- notebook.body.send_keys("v") #Paste
- validate_dualmode_state(notebook, 'command', 5)
- assert notebook.get_cell_contents(2) == c #Cell 2 has the copied contents
- assert notebook.get_cell_contents(5) == c #Cell 5 has the copied contents
- assert len(notebook.cells) == num_cells+2 #A cell was added
-
- notebook.focus_cell(0)
- shift(notebook.browser, 'v') #Paste
- validate_dualmode_state(notebook, 'command', 0)
- assert notebook.get_cell_contents(0) == c #Cell 0 has the copied contents
- assert len(notebook.cells) == num_cells+3 #A cell was added
diff --git a/nbclassic/tests/selenium/test_dualmode_execute.py b/nbclassic/tests/selenium/test_dualmode_execute.py
deleted file mode 100644
index 08bc3346f..000000000
--- a/nbclassic/tests/selenium/test_dualmode_execute.py
+++ /dev/null
@@ -1,74 +0,0 @@
-''' Test keyboard invoked execution '''
-
-from selenium.webdriver.common.keys import Keys
-
-from .utils import shift, cmdtrl, alt, validate_dualmode_state
-
-INITIAL_CELLS = ['', 'print("a")', 'print("b")', 'print("c")']
-
-def test_dualmode_execute(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
- for i in range(1, 4):
- notebook.execute_cell(i)
-
- #shift-enter
- #last cell in notebook
- base_index = 3
- notebook.focus_cell(base_index)
- shift(notebook.browser, Keys.ENTER) #creates one cell
- validate_dualmode_state(notebook, 'edit', base_index + 1)
-
- #Not last cell in notebook & starts in edit mode
- notebook.focus_cell(base_index)
- notebook.body.send_keys(Keys.ENTER) #Enter edit mode
- validate_dualmode_state(notebook, 'edit', base_index)
- shift(notebook.browser, Keys.ENTER) #creates one cell
- validate_dualmode_state(notebook, 'command', base_index + 1)
-
- #Starts in command mode
- notebook.body.send_keys('k')
- validate_dualmode_state(notebook, 'command', base_index)
- shift(notebook.browser, Keys.ENTER) #creates one cell
- validate_dualmode_state(notebook, 'command', base_index + 1)
-
-
- #Ctrl-enter
- #Last cell in notebook
- base_index += 1
- cmdtrl(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'command', base_index)
-
- #Not last cell in notebook & stats in edit mode
- notebook.focus_cell(base_index - 1)
- notebook.body.send_keys(Keys.ENTER) #Enter edit mode
- validate_dualmode_state(notebook, 'edit', base_index - 1)
- cmdtrl(notebook.browser, Keys.ENTER)
-
- #Starts in command mode
- notebook.body.send_keys('j')
- validate_dualmode_state(notebook, 'command', base_index)
- cmdtrl(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'command', base_index)
-
-
- #Alt-enter
- #Last cell in notebook
- alt(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'edit', base_index + 1)
- #Not last cell in notebook &starts in edit mode
- notebook.focus_cell(base_index)
- notebook.body.send_keys(Keys.ENTER) #Enter edit mode
- validate_dualmode_state(notebook, 'edit', base_index)
- alt(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'edit', base_index + 1)
- #starts in command mode
- notebook.body.send_keys(Keys.ESCAPE, 'k')
- validate_dualmode_state(notebook, 'command', base_index)
- alt(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'edit', base_index + 1)
-
-
- #Notebook will now have 8 cells, the index of the last cell will be 7
- assert len(notebook) == 8 #Cells where added
- notebook.focus_cell(7)
- validate_dualmode_state(notebook, 'command', 7)
diff --git a/nbclassic/tests/selenium/test_dualmode_insertcell.py b/nbclassic/tests/selenium/test_dualmode_insertcell.py
deleted file mode 100644
index 77f38ea2c..000000000
--- a/nbclassic/tests/selenium/test_dualmode_insertcell.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from selenium.webdriver.common.keys import Keys
-from .utils import shift
-
-INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
-
-def test_insert_cell(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
-
- notebook.to_command_mode()
- notebook.focus_cell(2)
- notebook.convert_cell_type(2, "markdown")
-
- # insert code cell above
- notebook.current_cell.send_keys("a")
- assert notebook.get_cell_contents(2) == ""
- assert notebook.get_cell_type(2) == "code"
- assert len(notebook.cells) == 4
-
- # insert code cell below
- notebook.current_cell.send_keys("b")
- assert notebook.get_cell_contents(2) == ""
- assert notebook.get_cell_contents(3) == ""
- assert notebook.get_cell_type(3) == "code"
- assert len(notebook.cells) == 5
-
- notebook.edit_cell(index=1, content="cell1")
- notebook.focus_cell(1)
- notebook.current_cell.send_keys("a")
- assert notebook.get_cell_contents(1) == ""
- assert notebook.get_cell_contents(2) == "cell1"
-
- notebook.edit_cell(index=1, content='cell1')
- notebook.edit_cell(index=2, content='cell2')
- notebook.edit_cell(index=3, content='cell3')
- notebook.focus_cell(2)
- notebook.current_cell.send_keys("b")
- assert notebook.get_cell_contents(1) == "cell1"
- assert notebook.get_cell_contents(2) == "cell2"
- assert notebook.get_cell_contents(3) == ""
- assert notebook.get_cell_contents(4) == "cell3"
-
- # insert above multiple selected cells
- notebook.focus_cell(1)
- shift(notebook.browser, Keys.DOWN)
- notebook.current_cell.send_keys('a')
-
- # insert below multiple selected cells
- notebook.focus_cell(2)
- shift(notebook.browser, Keys.DOWN)
- notebook.current_cell.send_keys('b')
- assert notebook.get_cells_contents()[1:5] == ["", "cell1", "cell2", ""]
diff --git a/nbclassic/tests/selenium/test_dualmode_markdown.py b/nbclassic/tests/selenium/test_dualmode_markdown.py
deleted file mode 100644
index af5ce3122..000000000
--- a/nbclassic/tests/selenium/test_dualmode_markdown.py
+++ /dev/null
@@ -1,53 +0,0 @@
-'''Test'''
-
-from selenium.webdriver.common.keys import Keys
-
-from .utils import cmdtrl, shift, validate_dualmode_state
-
-def test_dualmode_markdown(notebook):
- def is_cell_rendered(index):
- JS = 'return !!IPython.notebook.get_cell(%s).rendered;'%index
- return notebook.browser.execute_script(JS)
-
-
- a = 'print("a")'
- index = 1
- notebook.append(a)
-
- #Markdown rendering / unrendering
- notebook.focus_cell(index)
- validate_dualmode_state(notebook, 'command', index)
- notebook.body.send_keys("m")
- assert notebook.get_cell_type(index) == 'markdown'
- assert not is_cell_rendered(index) #cell is not rendered
-
- notebook.body.send_keys(Keys.ENTER)#cell is unrendered
- assert not is_cell_rendered(index) #cell is not rendered
- validate_dualmode_state(notebook, 'edit', index)
-
- cmdtrl(notebook.browser, Keys.ENTER)
- assert is_cell_rendered(index) #cell is rendered with crtl+enter
- validate_dualmode_state(notebook, 'command', index)
-
- notebook.body.send_keys(Keys.ENTER)#cell is unrendered
- assert not is_cell_rendered(index) #cell is not rendered
-
- notebook.focus_cell(index - 1)
- assert not is_cell_rendered(index) #Select index-1; cell index is still not rendered
- validate_dualmode_state(notebook, 'command', index - 1)
-
- notebook.focus_cell(index)
- validate_dualmode_state(notebook, 'command', index)
- cmdtrl(notebook.browser, Keys.ENTER)
- assert is_cell_rendered(index)#Cell is rendered
-
- notebook.focus_cell(index - 1)
- validate_dualmode_state(notebook, 'command', index - 1)
-
- shift(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'command', index)
- assert is_cell_rendered(index)#Cell is rendered
-
- shift(notebook.browser, Keys.ENTER)
- validate_dualmode_state(notebook, 'edit', index + 1)
- assert is_cell_rendered(index)#Cell is rendered
diff --git a/nbclassic/tests/selenium/test_execute_code.py b/nbclassic/tests/selenium/test_execute_code.py
deleted file mode 100644
index 5cf12778c..000000000
--- a/nbclassic/tests/selenium/test_execute_code.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from selenium.webdriver.common.keys import Keys
-from .utils import shift, cmdtrl
-
-
-def test_execute_code(notebook):
- browser = notebook.browser
-
- def clear_outputs():
- return notebook.browser.execute_script(
- "Jupyter.notebook.clear_all_output();")
-
- # Execute cell with Javascript API
- notebook.edit_cell(index=0, content='a=10; print(a)')
- browser.execute_script("Jupyter.notebook.get_cell(0).execute();")
- outputs = notebook.wait_for_cell_output(0)
- assert outputs[0].text == '10'
-
- # Execute cell with Shift-Enter
- notebook.edit_cell(index=0, content='a=11; print(a)')
- clear_outputs()
- shift(notebook.browser, Keys.ENTER)
- outputs = notebook.wait_for_cell_output(0)
- assert outputs[0].text == '11'
- notebook.delete_cell(index=1)
-
- # Execute cell with Ctrl-Enter
- notebook.edit_cell(index=0, content='a=12; print(a)')
- clear_outputs()
- cmdtrl(notebook.browser, Keys.ENTER)
- outputs = notebook.wait_for_cell_output(0)
- assert outputs[0].text == '12'
-
- # Execute cell with toolbar button
- notebook.edit_cell(index=0, content='a=13; print(a)')
- clear_outputs()
- notebook.browser.find_element_by_css_selector(
- "button[data-jupyter-action='jupyter-notebook:run-cell-and-select-next']").click()
- outputs = notebook.wait_for_cell_output(0)
- assert outputs[0].text == '13'
-
- # Set up two cells to test stopping on error
- notebook.edit_cell(index=0, content='raise IOError')
- notebook.edit_cell(index=1, content='a=14; print(a)')
-
- # Default behaviour: stop on error
- clear_outputs()
- browser.execute_script("""
- var cell0 = Jupyter.notebook.get_cell(0);
- var cell1 = Jupyter.notebook.get_cell(1);
- cell0.execute();
- cell1.execute();
- """)
- outputs = notebook.wait_for_cell_output(0)
- assert notebook.get_cell_output(1) == []
-
- # Execute a cell with stop_on_error=false
- clear_outputs()
- browser.execute_script("""
- var cell0 = Jupyter.notebook.get_cell(0);
- var cell1 = Jupyter.notebook.get_cell(1);
- cell0.execute(false);
- cell1.execute();
- """)
- outputs = notebook.wait_for_cell_output(1)
- assert outputs[0].text == '14'
-
diff --git a/nbclassic/tests/selenium/test_find_and_replace.py b/nbclassic/tests/selenium/test_find_and_replace.py
deleted file mode 100755
index 07e2f1b00..000000000
--- a/nbclassic/tests/selenium/test_find_and_replace.py
+++ /dev/null
@@ -1,16 +0,0 @@
-INITIAL_CELLS = ["hello", "hellohello", "abc", "ello"]
-
-def test_find_and_replace(prefill_notebook):
- """ test find and replace on all the cells """
- notebook = prefill_notebook(INITIAL_CELLS)
-
- find_str = "ello"
- replace_str = "foo"
-
- # replace the strings
- notebook.find_and_replace(index=0, find_txt=find_str, replace_txt=replace_str)
-
- # check content of the cells
- assert notebook.get_cells_contents() == [
- s.replace(find_str, replace_str) for s in INITIAL_CELLS
- ]
diff --git a/nbclassic/tests/selenium/test_interrupt.py b/nbclassic/tests/selenium/test_interrupt.py
deleted file mode 100644
index 56fd7564f..000000000
--- a/nbclassic/tests/selenium/test_interrupt.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from .utils import wait_for_selector
-
-def interrupt_from_menu(notebook):
- # Click interrupt button in kernel menu
- notebook.browser.find_element_by_id('kernellink').click()
- wait_for_selector(notebook.browser, '#int_kernel', single=True).click()
-
-def interrupt_from_keyboard(notebook):
- notebook.body.send_keys("ii")
-
-
-def test_interrupt(notebook):
- """ Test the interrupt function using both the button in the Kernel menu and the keyboard shortcut "ii"
-
- Having trouble accessing the Interrupt message when execution is halted. I am assuming that the
- message does not lie in the "outputs" field of the cell's JSON object. Using a timeout work-around for
- test with an infinite loop. We know the interrupt function is working if this test passes.
- Hope this is a good start.
- """
-
- text = ('import time\n'
- 'for x in range(3):\n'
- ' time.sleep(1)')
-
- notebook.edit_cell(index=0, content=text)
-
- for interrupt_method in (interrupt_from_menu, interrupt_from_keyboard):
- notebook.clear_cell_output(0)
- notebook.to_command_mode()
- notebook.execute_cell(0)
-
- interrupt_method(notebook)
-
- # Wait for an output to appear
- output = wait_for_selector(notebook.browser, '.output_subarea', single=True)
- assert 'KeyboardInterrupt' in output.text
diff --git a/nbclassic/tests/selenium/test_kernel_menu.py b/nbclassic/tests/selenium/test_kernel_menu.py
deleted file mode 100644
index 7eb100e22..000000000
--- a/nbclassic/tests/selenium/test_kernel_menu.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import WebDriverWait
-from nbclassic.tests.selenium.utils import wait_for_selector
-
-restart_selectors = [
- '#restart_kernel', '#restart_clear_output', '#restart_run_all'
-]
-notify_interaction = '#notification_kernel > span'
-
-shutdown_selector = '#shutdown_kernel'
-confirm_selector = '.btn-danger'
-cancel_selector = ".modal-footer button:first-of-type"
-
-
-def test_cancel_restart_or_shutdown(notebook):
- """Click each of the restart options, then cancel the confirmation dialog"""
- browser = notebook.browser
- kernel_menu = browser.find_element_by_id('kernellink')
-
- for menu_item in restart_selectors + [shutdown_selector]:
- kernel_menu.click()
- wait_for_selector(browser, menu_item, visible=True, single=True).click()
- wait_for_selector(browser, cancel_selector, visible=True, single=True).click()
- WebDriverWait(browser, 3).until(
- EC.invisibility_of_element((By.CSS_SELECTOR, '.modal-backdrop'))
- )
- assert notebook.is_kernel_running()
-
-
-def test_menu_items(notebook):
- browser = notebook.browser
- kernel_menu = browser.find_element_by_id('kernellink')
-
- for menu_item in restart_selectors:
- # Shutdown
- kernel_menu.click()
- wait_for_selector(browser, shutdown_selector, visible=True, single=True).click()
-
- # Confirm shutdown
- wait_for_selector(browser, confirm_selector, visible=True, single=True).click()
-
- WebDriverWait(browser, 3).until(
- lambda b: not notebook.is_kernel_running(),
- message="Kernel did not shut down as expected"
- )
-
- # Restart
- # Selenium can't click the menu while a modal dialog is fading out
- WebDriverWait(browser, 3).until(
- EC.invisibility_of_element((By.CSS_SELECTOR, '.modal-backdrop'))
- )
- kernel_menu.click()
-
- wait_for_selector(browser, menu_item, visible=True, single=True).click()
- WebDriverWait(browser, 10).until(
- lambda b: notebook.is_kernel_running(),
- message=f"Restart ({menu_item!r}) after shutdown did not start kernel"
- )
diff --git a/nbclassic/tests/selenium/test_merge_cells.py b/nbclassic/tests/selenium/test_merge_cells.py
deleted file mode 100644
index 0fb4dd566..000000000
--- a/nbclassic/tests/selenium/test_merge_cells.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""Tests the merge cell api."""
-
-INITIAL_CELLS = [
- "foo = 5",
- "bar = 10",
- "baz = 15",
- "print(foo)",
- "print(bar)",
- "print(baz)",
-]
-
-def test_merge_cells(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
- a, b, c, d, e, f = INITIAL_CELLS
-
- # Before merging, there are 6 separate cells
- assert notebook.get_cells_contents() == [a, b, c, d, e, f]
-
- # Focus on the second cell and merge it with the cell above
- notebook.focus_cell(1)
- notebook.browser.execute_script("Jupyter.notebook.merge_cell_above();")
- merged_a_b = f"{a}\n\n{b}"
- assert notebook.get_cells_contents() == [merged_a_b, c, d, e, f]
-
- # Focus on the second cell and merge it with the cell below
- notebook.focus_cell(1)
- notebook.browser.execute_script("Jupyter.notebook.merge_cell_below();")
- merged_c_d = f"{c}\n\n{d}"
- assert notebook.get_cells_contents() == [merged_a_b, merged_c_d, e, f]
-
- # Merge everything down to a single cell with selected cells
- notebook.select_cell_range(0,3)
- notebook.browser.execute_script("Jupyter.notebook.merge_selected_cells();")
- merged_all = f"{merged_a_b}\n\n{merged_c_d}\n\n{e}\n\n{f}"
- assert notebook.get_cells_contents() == [merged_all]
-
diff --git a/nbclassic/tests/selenium/test_move_multiselection.py b/nbclassic/tests/selenium/test_move_multiselection.py
deleted file mode 100644
index 7a07887b9..000000000
--- a/nbclassic/tests/selenium/test_move_multiselection.py
+++ /dev/null
@@ -1,47 +0,0 @@
-INITIAL_CELLS = ['1', '2', '3', '4', '5', '6']
-def test_move_multiselection(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
- def assert_oder(pre_message, expected_state):
- for i in range(len(expected_state)):
- assert expected_state[i] == notebook.get_cell_contents(i), f"{pre_message}: Verify that cell {i} has for content: {expected_state[i]} found: {notebook.get_cell_contents(i)}"
-
- # Select 3 first cells
- notebook.select_cell_range(0, 2)
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_up();"
- )
- # Should not move up at top
- assert_oder('move up at top', ['1', '2', '3', '4', '5','6'])
-
- # We do not need to reselect, move/up down should keep the selection.
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_down();"
- )
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_down();"
- )
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_down();"
- )
-
- # 3 times down should move the 3 selected cells to the bottom
- assert_oder("move down to bottom", ['4', '5', '6', '1', '2', '3'])
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_down();"
- )
-
- # They can't go any futher
- assert_oder("move down to bottom", ['4', '5', '6', '1', '2', '3'])
-
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_up();"
- )
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_up();"
- )
- notebook.browser.execute_script(
- "Jupyter.notebook.move_selection_up();"
- )
-
- # Bring them back on top
- assert_oder('move up at top', ['1', '2', '3', '4', '5','6'])
diff --git a/nbclassic/tests/selenium/test_multiselect.py b/nbclassic/tests/selenium/test_multiselect.py
deleted file mode 100644
index 6e09b25c0..000000000
--- a/nbclassic/tests/selenium/test_multiselect.py
+++ /dev/null
@@ -1,63 +0,0 @@
-INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
-
-def test_multiselect(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
-
- def extend_selection_by(delta):
- notebook.browser.execute_script(
- "Jupyter.notebook.extend_selection_by(arguments[0]);", delta)
-
- def n_selected_cells():
- return notebook.browser.execute_script(
- "return Jupyter.notebook.get_selected_cells().length;")
-
- notebook.focus_cell(0)
- assert n_selected_cells() == 1
-
- # Check that only one cell is selected according to CSS classes as well
- selected_css = notebook.browser.find_elements_by_css_selector(
- '.cell.jupyter-soft-selected, .cell.selected')
- assert len(selected_css) == 1
-
- # Extend the selection down one
- extend_selection_by(1)
- assert n_selected_cells() == 2
-
- # Contract the selection up one
- extend_selection_by(-1)
- assert n_selected_cells() == 1
-
- # Extend the selection up one
- notebook.focus_cell(1)
- extend_selection_by(-1)
- assert n_selected_cells() == 2
-
- # Convert selected cells to Markdown
- notebook.browser.execute_script("Jupyter.notebook.cells_to_markdown();")
- cell_types = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.cell_type)")
- assert cell_types == ['markdown', 'markdown', 'code']
- # One cell left selected after conversion
- assert n_selected_cells() == 1
-
- # Convert selected cells to raw
- notebook.focus_cell(1)
- extend_selection_by(1)
- assert n_selected_cells() == 2
- notebook.browser.execute_script("Jupyter.notebook.cells_to_raw();")
- cell_types = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.cell_type)")
- assert cell_types == ['markdown', 'raw', 'raw']
- # One cell left selected after conversion
- assert n_selected_cells() == 1
-
- # Convert selected cells to code
- notebook.focus_cell(0)
- extend_selection_by(2)
- assert n_selected_cells() == 3
- notebook.browser.execute_script("Jupyter.notebook.cells_to_code();")
- cell_types = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.cell_type)")
- assert cell_types == ['code'] * 3
- # One cell left selected after conversion
- assert n_selected_cells() == 1
diff --git a/nbclassic/tests/selenium/test_multiselect_toggle.py b/nbclassic/tests/selenium/test_multiselect_toggle.py
deleted file mode 100644
index 372d83b27..000000000
--- a/nbclassic/tests/selenium/test_multiselect_toggle.py
+++ /dev/null
@@ -1,43 +0,0 @@
-INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")']
-def test_multiselect_toggle(prefill_notebook):
- notebook = prefill_notebook(INITIAL_CELLS)
- def extend_selection_by(delta):
- notebook.browser.execute_script(
- "Jupyter.notebook.extend_selection_by(arguments[0]);", delta)
-
- def n_selected_cells():
- return notebook.browser.execute_script(
- "return Jupyter.notebook.get_selected_cells().length;")
-
- def select_cells():
- notebook.focus_cell(0)
- extend_selection_by(2)
-
- # Test that cells, which start off not collapsed, are collapsed after
- # calling the multiselected cell toggle.
- select_cells()
- assert n_selected_cells() == 3
- notebook.browser.execute_script("Jupyter.notebook.execute_selected_cells();")
- select_cells()
- notebook.browser.execute_script("Jupyter.notebook.toggle_cells_outputs();")
- cell_output_states = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.collapsed)")
- assert cell_output_states == [False] * 3, "ensure that all cells are not collapsed"
-
- # Test that cells, which start off not scrolled are scrolled after
- # calling the multiselected scroll toggle.
- select_cells()
- assert n_selected_cells() == 3
- notebook.browser.execute_script("Jupyter.notebook.toggle_cells_outputs_scroll();")
- cell_scrolled_states = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.output_area.scroll_state)")
- assert all(cell_scrolled_states), "ensure that all have scrolling enabled"
-
- # Test that cells, which start off not cleared are cleared after
- # calling the multiselected scroll toggle.
- select_cells()
- assert n_selected_cells() == 3
- notebook.browser.execute_script("Jupyter.notebook.clear_cells_outputs();")
- cell_outputs_cleared = notebook.browser.execute_script(
- "return Jupyter.notebook.get_cells().map(c => c.output_area.element.html())")
- assert cell_outputs_cleared == [""] * 3, "ensure that all cells are cleared"
diff --git a/nbclassic/tests/selenium/test_notifications.py b/nbclassic/tests/selenium/test_notifications.py
deleted file mode 100644
index 8e14f4682..000000000
--- a/nbclassic/tests/selenium/test_notifications.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""
-Test the notification area and widgets
-"""
-import pytest
-
-from .utils import wait_for_selector, wait_for_script_to_return_true
-
-
-def get_widget(notebook, name):
- return notebook.browser.execute_script(
- f"return IPython.notification_area.get_widget('{name}') !== undefined"
- )
-
-
-def widget(notebook, name):
- return notebook.browser.execute_script(
- f"return IPython.notification_area.widget('{name}') !== undefined"
- )
-
-
-def new_notification_widget(notebook, name):
- return notebook.browser.execute_script(
- f"return IPython.notification_area.new_notification_widget('{name}') !== undefined"
- )
-
-
-def widget_has_class(notebook, name, class_name):
- return notebook.browser.execute_script(
- f"""
- var w = IPython.notification_area.get_widget('{name}');
- return w.element.hasClass('{class_name}');
- """
- )
-
-
-def widget_message(notebook, name):
- return notebook.browser.execute_script(
- f"""
- var w = IPython.notification_area.get_widget('{name}');
- return w.get_message();
- """
- )
-
-
-def test_notification(notebook):
- # check that existing widgets are there
- assert get_widget(notebook, "kernel") and widget(notebook, "kernel"),\
- "The kernel notification widget exists"
- assert get_widget(notebook, "notebook") and widget(notebook, "notebook"),\
- "The notebook notification widget exists"
-
- # try getting a non-existent widget
- with pytest.raises(Exception):
- get_widget(notebook, "foo")
-
- # try creating a non-existent widget
- assert widget(notebook, "bar"), "widget: new widget is created"
-
- # try creating a widget that already exists
- with pytest.raises(Exception):
- new_notification_widget(notebook, "kernel")
-
- # test creating 'info', 'warning' and 'danger' messages
- for level in ("info", "warning", "danger"):
- notebook.browser.execute_script(f"""
- var tnw = IPython.notification_area.widget('test');
- tnw.{level}('test {level}');
- """)
- wait_for_selector(notebook.browser, "#notification_test", visible=True)
-
- assert widget_has_class(notebook, "test", level), f"{level}: class is correct"
- assert widget_message(notebook, "test") == f"test {level}", f"{level}: message is correct"
-
- # test message timeout
- notebook.browser.execute_script("""
- var tnw = IPython.notification_area.widget('test');
- tnw.set_message('test timeout', 1000);
- """)
- wait_for_selector(notebook.browser, "#notification_test", visible=True)
-
- assert widget_message(notebook, "test") == "test timeout", "timeout: message is correct"
- wait_for_selector(notebook.browser, "#notification_test", obscures=True)
- assert widget_message(notebook, "test") == "", "timeout: message was cleared"
-
- # test click callback
- notebook.browser.execute_script("""
- var tnw = IPython.notification_area.widget('test');
- tnw._clicked = false;
- tnw.set_message('test click', undefined, function () {
- tnw._clicked = true;
- return true;
- });
- """)
- wait_for_selector(notebook.browser, "#notification_test", visible=True)
-
- assert widget_message(notebook, "test") == "test click", "callback: message is correct"
-
- notebook.browser.find_element_by_id("notification_test").click()
- wait_for_script_to_return_true(notebook.browser,
- 'return IPython.notification_area.widget("test")._clicked;')
- wait_for_selector(notebook.browser, "#notification_test", obscures=True)
-
- assert widget_message(notebook, "test") == "", "callback: message was cleared"
diff --git a/nbclassic/tests/selenium/test_save.py b/nbclassic/tests/selenium/test_save.py
deleted file mode 100644
index d566866b0..000000000
--- a/nbclassic/tests/selenium/test_save.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Test saving a notebook with escaped characters
-"""
-
-from urllib.parse import quote
-from .utils import wait_for_selector
-
-promise_js = """
-var done = arguments[arguments.length - 1];
-%s.then(
- data => { done(["success", data]); },
- error => { done(["error", error]); }
-);
-"""
-
-def execute_promise(js, browser):
- state, data = browser.execute_async_script(promise_js % js)
- if state == 'success':
- return data
- raise Exception(data)
-
-
-def test_save(notebook):
- # don't use unicode with ambiguous composed/decomposed normalization
- # because the filesystem may use a different normalization than literals.
- # This causes no actual problems, but will break string comparison.
- nbname = "has#hash and space and unicø∂e.ipynb"
- escaped_name = quote(nbname)
-
- notebook.edit_cell(index=0, content="s = '??'")
-
- notebook.browser.execute_script("Jupyter.notebook.set_notebook_name(arguments[0])", nbname)
-
- model = execute_promise("Jupyter.notebook.save_notebook()", notebook.browser)
- assert model['name'] == nbname
-
- current_name = notebook.browser.execute_script("return Jupyter.notebook.notebook_name")
- assert current_name == nbname
-
- current_path = notebook.browser.execute_script("return Jupyter.notebook.notebook_path")
- assert current_path == nbname
-
- displayed_name = notebook.browser.find_element_by_id('notebook_name').text
- assert displayed_name + '.ipynb' == nbname
-
- execute_promise("Jupyter.notebook.save_checkpoint()", notebook.browser)
-
- checkpoints = notebook.browser.execute_script("return Jupyter.notebook.checkpoints")
- assert len(checkpoints) == 1
-
- notebook.browser.find_element_by_css_selector('#ipython_notebook a').click()
- hrefs_nonmatch = []
- for link in wait_for_selector(notebook.browser, 'a.item_link'):
- href = link.get_attribute('href')
- if escaped_name in href:
- print("Opening", href)
- notebook.browser.get(href)
- wait_for_selector(notebook.browser, '.cell')
- break
- hrefs_nonmatch.append(href)
- else:
- raise AssertionError(f"{escaped_name!r} not found in {hrefs_nonmatch!r}")
-
- current_name = notebook.browser.execute_script("return Jupyter.notebook.notebook_name")
- assert current_name == nbname
diff --git a/nbclassic/tests/selenium/test_save_as_notebook.py b/nbclassic/tests/selenium/test_save_as_notebook.py
deleted file mode 100644
index f309db01b..000000000
--- a/nbclassic/tests/selenium/test_save_as_notebook.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from nbclassic.tests.selenium.utils import wait_for_selector
-from selenium.webdriver.common.keys import Keys
-from selenium.webdriver.support.ui import WebDriverWait
-
-def wait_for_rename(browser, nbname, timeout=10):
- wait = WebDriverWait(browser, timeout)
- def notebook_renamed(browser):
- elem = browser.find_element_by_id('notebook_name')
- current_name = browser.execute_script('return arguments[0].innerText', elem)
- return current_name == nbname
- return wait.until(notebook_renamed)
-
-def save_as(nb):
- JS = 'Jupyter.notebook.save_notebook_as()'
- return nb.browser.execute_script(JS)
-
-def get_notebook_name(nb):
- JS = 'return Jupyter.notebook.notebook_name'
- return nb.browser.execute_script(JS)
-
-def set_notebook_name(nb, name):
- JS = f'Jupyter.notebook.rename("{name}")'
- nb.browser.execute_script(JS)
-
-def test_save_notebook_as(notebook):
- # Set a name for comparison later
- set_notebook_name(notebook, name="nb1.ipynb")
- wait_for_rename(notebook.browser, "nb1")
- assert get_notebook_name(notebook) == "nb1.ipynb"
- # Wait for Save As modal, save
- save_as(notebook)
- wait_for_selector(notebook.browser, '.save-message')
- inp = notebook.browser.find_element_by_xpath('//input[@data-testid="save-as"]')
- inp.send_keys('new_notebook.ipynb')
- inp.send_keys(Keys.RETURN)
- wait_for_rename(notebook.browser, "new_notebook")
- # Test that the name changed
- assert get_notebook_name(notebook) == "new_notebook.ipynb"
- # Test that address bar was updated (TODO: get the base url)
- assert "new_notebook.ipynb" in notebook.browser.current_url
\ No newline at end of file
diff --git a/nbclassic/tests/selenium/test_save_readonly_as.py b/nbclassic/tests/selenium/test_save_readonly_as.py
deleted file mode 100644
index c20be9437..000000000
--- a/nbclassic/tests/selenium/test_save_readonly_as.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from nbclassic.tests.selenium.utils import wait_for_selector
-from selenium.webdriver.common.keys import Keys
-from selenium.webdriver.support.ui import WebDriverWait
-
-
-promise_js = """
-var done = arguments[arguments.length - 1];
-(%s).then(
- data => { done(["success", data]); },
- error => { done(["error", error]); }
-);
-"""
-
-def execute_promise(js, browser):
- state, data = browser.execute_async_script(promise_js % js)
- if state == 'success':
- return data
- raise Exception(data)
-
-def wait_for_rename(browser, nbname, timeout=10):
- wait = WebDriverWait(browser, timeout)
- def notebook_renamed(browser):
- elem = browser.find_element_by_id('notebook_name')
- current_name = browser.execute_script('return arguments[0].innerText', elem)
- return current_name == nbname
- return wait.until(notebook_renamed)
-
-def save_as(nb):
- JS = 'Jupyter.notebook.save_notebook_as()'
- return nb.browser.execute_script(JS)
-
-def get_notebook_name(nb):
- JS = 'return Jupyter.notebook.notebook_name'
- return nb.browser.execute_script(JS)
-
-def refresh_notebook(nb):
- nb.browser.refresh()
- nb.__init__(nb.browser)
-
-def test_save_readonly_notebook_as(notebook):
- # Make notebook read-only
- notebook.edit_cell(index=0, content='import os\nimport stat\nos.chmod("'
- + notebook.browser.current_url.split('?')[0].split('/')[-1] + '", stat.S_IREAD)\nprint(0)')
- notebook.browser.execute_script("Jupyter.notebook.get_cell(0).execute();")
- notebook.wait_for_cell_output(0)
- refresh_notebook(notebook)
- # Test that the notebook is read-only
- assert notebook.browser.execute_script('return Jupyter.notebook.writable') == False
-
- # Add some content
- test_content_0 = "print('a simple')\nprint('test script')"
- notebook.edit_cell(index=0, content=test_content_0)
-
- # Wait for Save As modal, save
- save_as(notebook)
- wait_for_selector(notebook.browser, '.save-message')
- inp = notebook.browser.find_element_by_xpath('//input[@data-testid="save-as"]')
- inp.send_keys('writable_notebook.ipynb')
- inp.send_keys(Keys.RETURN)
- wait_for_rename(notebook.browser, "writable_notebook")
- # Test that the name changed
- assert get_notebook_name(notebook) == "writable_notebook.ipynb"
- # Test that address bar was updated (TODO: get the base url)
- assert "writable_notebook.ipynb" in notebook.browser.current_url
- # Test that it is no longer read-only
- assert notebook.browser.execute_script('return Jupyter.notebook.writable') == True
-
- # Add some more content
- test_content_1 = "print('a second simple')\nprint('script to test save feature')"
- notebook.add_and_execute_cell(content=test_content_1)
- # and save the notebook
- execute_promise("Jupyter.notebook.save_notebook()", notebook.browser)
-
- # Test that it still contains the content
- assert notebook.get_cell_contents(index=0) == test_content_0
- assert notebook.get_cell_contents(index=1) == test_content_1
- # even after a refresh
- refresh_notebook(notebook)
- assert notebook.get_cell_contents(index=0) == test_content_0
- assert notebook.get_cell_contents(index=1) == test_content_1
diff --git a/nbclassic/tests/selenium/test_shutdown.py b/nbclassic/tests/selenium/test_shutdown.py
deleted file mode 100644
index 4d061a134..000000000
--- a/nbclassic/tests/selenium/test_shutdown.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Tests shutdown of the Kernel."""
-from .utils import wait_for_selector, wait_for_xpath
-
-def test_shutdown(notebook):
- notebook.edit_cell(content="print(21)")
- wait_for_xpath(notebook.browser, '//a[text()="Kernel"]', single=True).click()
- wait_for_selector(notebook.browser, '#shutdown_kernel', single=True).click()
- wait_for_selector(notebook.browser, '.btn.btn-default.btn-sm.btn-danger', single=True).click()
-
- #Wait until all shutdown modal elements disappear before trying to execute the cell
- wait_for_xpath(notebook.browser, "//div[contains(@class,'modal')]", obscures=True)
- notebook.execute_cell(0)
-
- assert not notebook.is_kernel_running()
- assert len(notebook.get_cell_output()) == 0
\ No newline at end of file
diff --git a/nbclassic/tests/selenium/utils.py b/nbclassic/tests/selenium/utils.py
deleted file mode 100644
index dbcdb9b41..000000000
--- a/nbclassic/tests/selenium/utils.py
+++ /dev/null
@@ -1,468 +0,0 @@
-import os
-from selenium.webdriver import ActionChains
-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(driver, selector, timeout=10, visible=False, single=False, wait_for_n=1, obscures=False):
- if wait_for_n > 1:
- return _wait_for_multiple(
- driver, By.CSS_SELECTOR, selector, timeout, wait_for_n, visible)
- return _wait_for(driver, By.CSS_SELECTOR, selector, timeout, visible, single, obscures)
-
-
-def wait_for_tag(driver, tag, timeout=10, visible=False, single=False, wait_for_n=1, obscures=False):
- if wait_for_n > 1:
- return _wait_for_multiple(
- driver, By.TAG_NAME, tag, timeout, wait_for_n, visible)
- return _wait_for(driver, By.TAG_NAME, tag, timeout, visible, single, obscures)
-
-
-def wait_for_xpath(driver, xpath, timeout=10, visible=False, single=False, wait_for_n=1, obscures=False):
- if wait_for_n > 1:
- return _wait_for_multiple(
- driver, By.XPATH, xpath, timeout, wait_for_n, visible)
- return _wait_for(driver, By.XPATH, xpath, timeout, visible, single, obscures)
-
-
-def wait_for_script_to_return_true(driver, script, timeout=10):
- WebDriverWait(driver, timeout).until(lambda d: d.execute_script(script))
-
-
-def _wait_for(driver, locator_type, locator, timeout=10, visible=False, single=False, obscures=False):
- """Waits `timeout` seconds for the specified condition to be met. Condition is
- met if any matching element is found. Returns located element(s) when found.
-
- Args:
- driver: Selenium web driver instance
- locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME)
- locator: name of tag, class, etc. to wait for
- timeout: how long to wait for presence/visibility of element
- visible: if True, require that element is not only present, but visible
- single: if True, return a single element, otherwise return a list of matching
- elements
- obscures: if True, waits until the element becomes invisible
- """
- wait = WebDriverWait(driver, timeout)
- if obscures:
- conditional = EC.invisibility_of_element_located
- elif 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((locator_type, locator)))
-
-
-def _wait_for_multiple(driver, locator_type, locator, timeout, wait_for_n, visible=False):
- """Waits until `wait_for_n` matching elements to be present (or visible).
- Returns located elements when found.
-
- Args:
- driver: Selenium web driver instance
- locator_type: type of locator (e.g. By.CSS_SELECTOR or By.TAG_NAME)
- locator: name of tag, class, etc. to wait for
- timeout: how long to wait for presence/visibility of element
- wait_for_n: wait until this number of matching elements are present/visible
- visible: if True, require that elements are not only present, but visible
- """
- wait = WebDriverWait(driver, timeout)
-
- def multiple_found(driver):
- elements = driver.find_elements(locator_type, locator)
- if visible:
- elements = [e for e in elements if e.is_displayed()]
- if len(elements) < wait_for_n:
- return False
- return elements
-
- return wait.until(multiple_found)
-
-
-class CellTypeError(ValueError):
-
- def __init__(self, message=""):
- self.message = message
-
-
-class Notebook:
-
- def __init__(self, browser):
- self.browser = browser
- self._wait_for_start()
- 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)
-
- def _wait_for_start(self):
- """Wait until the notebook interface is loaded and the kernel started"""
- wait_for_selector(self.browser, '.cell')
- WebDriverWait(self.browser, 10).until(
- lambda drvr: self.is_kernel_running()
- )
-
- @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 select_cell_range(self, initial_index=0, final_index=0):
- self.focus_cell(initial_index)
- self.to_command_mode()
- for i in range(final_index - initial_index):
- shift(self.browser, 'j')
-
- def find_and_replace(self, index=0, find_txt='', replace_txt=''):
- self.focus_cell(index)
- self.to_command_mode()
- self.body.send_keys('f')
- wait_for_selector(self.browser, "#find-and-replace", single=True)
- self.browser.find_element_by_id("findreplace_allcells_btn").click()
- self.browser.find_element_by_id("findreplace_find_inp").send_keys(find_txt)
- self.browser.find_element_by_id("findreplace_replace_inp").send_keys(replace_txt)
- self.browser.find_element_by_id("findreplace_replaceall_btn").click()
-
- 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(f"{cell_type} is not a valid cell type,use 'code', 'markdown', or 'raw'")
-
- 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 wait_for_element_availability(self, element):
- _wait_for(self.browser, By.CLASS_NAME, element, visible=True)
-
- def get_cells_contents(self):
- JS = 'return Jupyter.notebook.get_cells().map(function(c) {return c.get_text();})'
- return self.browser.execute_script(JS)
-
- def get_cell_contents(self, index=0, selector='div .CodeMirror-code'):
- return self.cells[index].find_element_by_css_selector(selector).text
-
- def get_cell_output(self, index=0, output='output_subarea'):
- return self.cells[index].find_elements_by_class_name(output)
-
- def wait_for_cell_output(self, index=0, timeout=10):
- return WebDriverWait(self.browser, timeout).until(
- lambda b: self.get_cell_output(index)
- )
-
- def set_cell_metadata(self, index, key, value):
- JS = f'Jupyter.notebook.get_cell({index}).metadata.{key} = {value}'
- return self.browser.execute_script(JS)
-
- def get_cell_type(self, index=0):
- JS = f'return Jupyter.notebook.get_cell({index}).cell_type'
- return self.browser.execute_script(JS)
-
- def set_cell_input_prompt(self, index, prmpt_val):
- JS = f'Jupyter.notebook.get_cell({index}).set_input_prompt({prmpt_val})'
- self.browser.execute_script(JS)
-
- def edit_cell(self, cell=None, index=0, content="", render=False):
- """Set the contents of a cell to *content*, by cell object or by index
- """
- if cell is not None:
- index = self.index(cell)
- self.focus_cell(index)
-
- # Select & delete anything already in the cell
- self.current_cell.send_keys(Keys.ENTER)
- cmdtrl(self.browser, 'a')
- self.current_cell.send_keys(Keys.DELETE)
-
- 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)
- if cell_type != 'code':
- self.convert_cell_type(index=new_index, cell_type=cell_type)
-
- def add_and_execute_cell(self, index=-1, cell_type="code", content=""):
- self.add_cell(index=index, cell_type=cell_type, content=content)
- self.execute_cell(index)
-
- def delete_cell(self, index):
- self.focus_cell(index)
- self.to_command_mode()
- self.current_cell.send_keys('dd')
-
- 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(f"Don't know how to add cell from {value!r}")
-
- def extend(self, values):
- self.append(*values)
-
- def run_all(self):
- for cell in self:
- self.execute_cell(cell)
-
- def trigger_keydown(self, keys):
- trigger_keystrokes(self.body, keys)
-
- def is_kernel_running(self):
- return self.browser.execute_script(
- "return Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected()"
- )
-
- def clear_cell_output(self, index):
- JS = f'Jupyter.notebook.clear_output({index})'
- self.browser.execute_script(JS)
-
- @classmethod
- def new_notebook(cls, browser, kernel_name='kernel-python3'):
- with new_window(browser):
- 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.
- """
- wait = WebDriverWait(browser, 10)
- new_button = wait.until(EC.element_to_be_clickable((By.ID, "new-dropdown-button")))
- new_button.click()
- kernel_selector = f'#{kernel_name} a'
- kernel = wait_for_selector(browser, kernel_selector, single=True)
- kernel.click()
-
-
-@contextmanager
-def new_window(browser):
- """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.
-
- Usage example:
-
- from nbclassic.tests.selenium.utils import new_window, Notebook
-
- ⋮ # something that creates a browser object
-
- with new_window(browser):
- select_kernel(browser, kernel_name=kernel_name)
- nb = Notebook(browser)
-
- """
- initial_window_handles = browser.window_handles
- yield
- new_window_handles = [window for window in browser.window_handles
- if window not in initial_window_handles]
- if not new_window_handles:
- raise Exception("No new windows opened during context")
- browser.switch_to.window(new_window_handles[0])
-
-def shift(browser, k):
- """Send key combination Shift+(k)"""
- trigger_keystrokes(browser, "shift-%s"%k)
-
-def cmdtrl(browser, k):
- """Send key combination Ctrl+(k) or Command+(k) for MacOS"""
- trigger_keystrokes(browser, "command-%s"%k) if os.uname()[0] == "Darwin" else trigger_keystrokes(browser, "control-%s"%k)
-
-def alt(browser, k):
- """Send key combination Alt+(k)"""
- trigger_keystrokes(browser, 'alt-%s'%k)
-
-def trigger_keystrokes(browser, *keys):
- """ Send the keys in sequence to the browser.
- Handles following key combinations
- 1. with modifiers eg. 'control-alt-a', 'shift-c'
- 2. just modifiers eg. 'alt', 'esc'
- 3. non-modifiers eg. 'abc'
- Modifiers : http://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html
- """
- for each_key_combination in keys:
- keys = each_key_combination.split('-')
- if len(keys) > 1: # key has modifiers eg. control, alt, shift
- modifiers_keys = [getattr(Keys, x.upper()) for x in keys[:-1]]
- ac = ActionChains(browser)
- for i in modifiers_keys: ac = ac.key_down(i)
- ac.send_keys(keys[-1])
- for i in modifiers_keys[::-1]: ac = ac.key_up(i)
- ac.perform()
- else: # single key stroke. Check if modifier eg. "up"
- browser.send_keys(getattr(Keys, keys[0].upper(), keys[0]))
-
-def validate_dualmode_state(notebook, mode, index):
- '''Validate the entire dual mode state of the notebook.
- Checks if the specified cell is selected, and the mode and keyboard mode are the same.
- Depending on the mode given:
- Command: Checks that no cells are in focus or in edit mode.
- Edit: Checks that only the specified cell is in focus and in edit mode.
- '''
- def is_only_cell_edit(index):
- JS = 'return Jupyter.notebook.get_cells().map(function(c) {return c.mode;})'
- cells_mode = notebook.browser.execute_script(JS)
- #None of the cells are in edit mode
- if index is None:
- for mode in cells_mode:
- if mode == 'edit':
- return False
- return True
- #Only the index cell is on edit mode
- for i, mode in enumerate(cells_mode):
- if i == index:
- if mode != 'edit':
- return False
- else:
- if mode == 'edit':
- return False
- return True
-
- def is_focused_on(index):
- JS = "return $('#notebook .CodeMirror-focused textarea').length;"
- focused_cells = notebook.browser.execute_script(JS)
- if index is None:
- return focused_cells == 0
-
- if focused_cells != 1: #only one cell is focused
- return False
-
- JS = "return $('#notebook .CodeMirror-focused textarea')[0];"
- focused_cell = notebook.browser.execute_script(JS)
- JS = "return IPython.notebook.get_cell(%s).code_mirror.getInputField()"%index
- cell = notebook.browser.execute_script(JS)
- return focused_cell == cell
-
-
- #general test
- JS = "return IPython.keyboard_manager.mode;"
- keyboard_mode = notebook.browser.execute_script(JS)
- JS = "return IPython.notebook.mode;"
- notebook_mode = notebook.browser.execute_script(JS)
-
- #validate selected cell
- JS = "return Jupyter.notebook.get_selected_cells_indices();"
- cell_index = notebook.browser.execute_script(JS)
- assert cell_index == [index] #only the index cell is selected
-
- if mode != 'command' and mode != 'edit':
- raise Exception('An unknown mode was send: mode = "%s"'%mode) #An unknown mode is send
-
- #validate mode
- assert mode == keyboard_mode #keyboard mode is correct
-
- if mode == 'command':
- assert is_focused_on(None) #no focused cells
-
- assert is_only_cell_edit(None) #no cells in edit mode
-
- elif mode == 'edit':
- assert is_focused_on(index) #The specified cell is focused
-
- assert is_only_cell_edit(index) #The specified cell is the only one in edit mode
diff --git a/setup.py b/setup.py
index ff16590c9..b6168338c 100644
--- a/setup.py
+++ b/setup.py
@@ -123,7 +123,7 @@
],
extras_require = {
'test': ['pytest', 'coverage', 'requests', 'testpath',
- 'nbval', 'selenium==4.1.5', 'pytest', 'pytest-cov', 'pytest_tornasync'],
+ 'nbval', 'pytest-playwright', 'pytest-cov', 'pytest_tornasync'],
'docs': ['sphinx', 'nbsphinx', 'sphinxcontrib_github_alt',
'sphinx_rtd_theme', 'myst-parser'],
'test:sys_platform != "win32"': ['requests-unixsocket'],
diff --git a/tools/install_pydeps.py b/tools/install_pydeps.py
new file mode 100644
index 000000000..b25dc5c55
--- /dev/null
+++ b/tools/install_pydeps.py
@@ -0,0 +1,44 @@
+"""CI/CD script for installing python dependencies in test runners"""
+
+
+import subprocess
+import time
+
+
+def attempt(arg_list, max_attempts=1, name=''):
+ retries = 0
+ while retries < max_attempts:
+ proc = subprocess.Popen(arg_list)
+
+ # Keep running until finish or failure (may hang)
+ while proc.poll() is None:
+ time.sleep(.1)
+
+ # Proc finished, check for valid return code
+ if proc.returncode == 0:
+ print(f"\n[INSTALL_PYDEPS] SUCCESS for process '{name}'\n")
+ break
+ else:
+ # Likely failure, retry
+ print(f"\n[INSTALL_PYDEPS] FAILURE for process '{name}', retrying!\n")
+ retries += 1
+ else:
+ # Retries exceeded
+ raise Exception(f"[INSTALL_PYDEPS] Retries exceeded for proc '{name}'!")
+
+
+def run():
+ steps = {
+ 'step1': """python -m pip install -U pip setuptools wheel""".split(' '),
+ 'step2': """pip install pytest-playwright""".split(' '),
+ 'step3': """playwright install""".split(' '),
+ 'step4': """pip install .[test]""".split(' '),
+ }
+
+ for step_name, step_arglist in steps.items():
+ print(f"\n[INSTALL_PYDEPS] Attempt '{step_name}' -> Run '{' '.join(step_arglist)}'\n")
+ attempt(step_arglist, max_attempts=3, name=step_name)
+
+
+if __name__ == '__main__':
+ run()
diff --git a/tools/runsuite_repeat.py b/tools/runsuite_repeat.py
new file mode 100644
index 000000000..b8b4b821d
--- /dev/null
+++ b/tools/runsuite_repeat.py
@@ -0,0 +1,20 @@
+"""CI/CD debug script"""
+
+
+import os
+import subprocess
+import time
+
+
+def run():
+ for stepnum in range(10):
+ try:
+ proc = subprocess.run(['pytest', '-sv', 'nbclassic/tests/end_to_end'])
+ except Exception:
+ print(f'\n[RUNSUITE_REPEAT] Exception -> Run {stepnum}\n')
+ continue
+ print(f'\n[RUNSUITE_REPEAT] {"Success" if proc.returncode == 0 else proc.returncode} -> Run {stepnum}\n')
+
+
+if __name__ == '__main__':
+ run()