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) == [ '

Foo

', '

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()