From 5943cc96d7121b55d62ed38a1fa238b6abfc9828 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 2 Aug 2022 10:32:19 -0400 Subject: [PATCH 001/131] Initial skeleton subpackage. --- nbclassic/tests/end_to_end/__init__.py | 0 nbclassic/tests/end_to_end/conftest.py | 143 ++++++ nbclassic/tests/end_to_end/test_playwright.py | 72 +++ nbclassic/tests/end_to_end/utils.py | 468 ++++++++++++++++++ 4 files changed, 683 insertions(+) create mode 100644 nbclassic/tests/end_to_end/__init__.py create mode 100644 nbclassic/tests/end_to_end/conftest.py create mode 100644 nbclassic/tests/end_to_end/test_playwright.py create mode 100644 nbclassic/tests/end_to_end/utils.py diff --git a/nbclassic/tests/end_to_end/__init__.py b/nbclassic/tests/end_to_end/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py new file mode 100644 index 000000000..07747b2cf --- /dev/null +++ b/nbclassic/tests/end_to_end/conftest.py @@ -0,0 +1,143 @@ +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/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py new file mode 100644 index 000000000..3ed8bf17b --- /dev/null +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -0,0 +1,72 @@ +"""Proof of concept for playwright testing, uses a reimplementation of test_execute_code""" + + +from playwright.sync_api import Page, expect + +# TODO: Remove +# 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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py new file mode 100644 index 000000000..05e6c48ff --- /dev/null +++ b/nbclassic/tests/end_to_end/utils.py @@ -0,0 +1,468 @@ +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, page): + self.page = page + 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.page.locator("body") + + @property + def cells(self): + """Gets all cells once they are visible. + + """ + return self.page.locator("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 From 455d5a85c3526fdafd3623c8dd7dfad6776de470 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 9 Aug 2022 10:38:25 -0400 Subject: [PATCH 002/131] Initial WIP playwright test skeleton. --- nbclassic/tests/end_to_end/conftest.py | 154 ++++++++++-------- nbclassic/tests/end_to_end/test_playwright.py | 103 ++++++------ nbclassic/tests/end_to_end/utils.py | 154 ++++++++++-------- 3 files changed, 222 insertions(+), 189 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 07747b2cf..da330c7df 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -1,20 +1,20 @@ -import json -import nbformat -from nbformat.v4 import new_notebook, new_code_cell import os -import pytest -import requests -from subprocess import Popen +import json import sys -from tempfile import mkstemp -from testpath.tempdir import TemporaryDirectory import time +from os.path import join as pjoin +from subprocess import Popen +from tempfile import mkstemp from urllib.parse import urljoin -from selenium.webdriver import Firefox, Remote, Chrome -from .utils import Notebook +import pytest +import requests +from testpath.tempdir import TemporaryDirectory +# from selenium.webdriver import Firefox, Remote, Chrome -pjoin = os.path.join +import nbformat +from nbformat.v4 import new_notebook, new_code_cell +from .utils import NotebookFrontend def _wait_for_server(proc, info_file_path): @@ -71,73 +71,91 @@ def notebook_server(): 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 +# 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() +def playwright_driver(playwright): + # TODO: Fix + # if os.environ.get('SAUCE_USERNAME'): + # driver = make_sauce_driver() + if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': + driver = playwright.chromium.launch() else: - driver = Firefox() + driver = playwright.firefox.launch() + driver = driver.new_context().new_page() yield driver - # Teardown - driver.quit() + # # 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(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 +def authenticated_browser(playwright_driver, notebook_server): + playwright_driver.jupyter_server_info = notebook_server + playwright_driver.goto("{url}?token={token}".format(**notebook_server)) + return playwright_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 +def notebook(authenticated_browser): + # tree_wh = authenticated_browser.current_window_handle + yield NotebookFrontend.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/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index 3ed8bf17b..32112b54a 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -9,64 +9,63 @@ def test_execute_code(notebook): - browser = notebook.browser + # browser = notebook.browser def clear_outputs(): - return notebook.browser.execute_script( + return notebook.evaluate( "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();") + notebook.evaluate("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' - + # # 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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 05e6c48ff..6131bc1f6 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -1,14 +1,15 @@ 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 +from os.path import join as pjoin + +from playwright.sync_api import ElementHandle -pjoin = os.path.join +# 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 def wait_for_selector(driver, selector, timeout=10, visible=False, single=False, wait_for_n=1, obscures=False): @@ -97,12 +98,13 @@ def __init__(self, message=""): self.message = message -class Notebook: +class NotebookFrontend: def __init__(self, page): self.page = page self._wait_for_start() self.disable_autosave_and_onbeforeunload() + # self.current_cell = None # Defined/used below def __len__(self): return len(self.cells) @@ -126,10 +128,14 @@ def __iter__(self): 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() - ) + # wait_for_selector(self.browser, '.cell') + self.page.locator('.cell') + # TODO: Refactor/fix + import time + time.sleep(10) + # WebDriverWait(self.browser, 10).until( + # lambda drvr: self.is_kernel_running() + # ) @property def body(self): @@ -140,7 +146,7 @@ def cells(self): """Gets all cells once they are visible. """ - return self.page.locator("cell") + return self.page.query_selector_all("cell") @property def current_index(self): @@ -149,20 +155,22 @@ def current_index(self): def index(self, cell): return self.cells.index(cell) + # TODO: Do we need to wrap in an anonymous function? + def evaluate(self, text): + return self.page.evaluate(text) + 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)") + self.page.evaluate("window.onbeforeunload = null;") + self.page.evaluate("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(" + """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()))") @@ -225,12 +233,13 @@ 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) + return self.cells[index].query_selector(output) # Find cell child elements def wait_for_cell_output(self, index=0, timeout=10): - return WebDriverWait(self.browser, timeout).until( - lambda b: self.get_cell_output(index) - ) + # return WebDriverWait(self.browser, timeout).until( + # lambda b: self.get_cell_output(index) + # ) + return self.get_cell_output() def set_cell_metadata(self, index, key, value): JS = f'Jupyter.notebook.get_cell({index}).metadata.{key} = {value}' @@ -252,26 +261,27 @@ def edit_cell(self, cell=None, index=0, content="", render=False): 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) + self.current_cell.press('Enter') + cmdtrl(self.page, 'a') # TODO: FIX + self.current_cell.press('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) + self.page.type("Enter") + self.page.type("Enter") + self.page.type(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): + elif isinstance(cell_or_index, ElementHandle): index = self.index(cell_or_index) else: - raise TypeError("execute_cell only accepts a WebElement or an int") + raise TypeError("execute_cell only accepts an ElementHandle or an int") self.focus_cell(index) - self.current_cell.send_keys(Keys.CONTROL, Keys.ENTER) + self.current_cell.press("Control+Enter") def add_cell(self, index=-1, cell_type="code", content=""): self.focus_cell(index) @@ -323,61 +333,67 @@ def clear_cell_output(self, 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 new_notebook(cls, page, kernel_name='kernel-python3'): + # with new_window(page): + select_kernel(page, kernel_name=kernel_name) + return cls(page) -def select_kernel(browser, kernel_name='kernel-python3'): +def select_kernel(page, 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"))) + # wait = WebDriverWait(browser, 10) + # new_button = wait.until(EC.element_to_be_clickable((By.ID, "new-dropdown-button"))) + new_button = page.locator('#new-dropdown-button') new_button.click() kernel_selector = f'#{kernel_name} a' - kernel = wait_for_selector(browser, kernel_selector, single=True) + # kernel = wait_for_selector(page, kernel_selector, single=True) + kernel = page.locator(kernel_selector) 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: +# @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]) - 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 cmdtrl(page, key): + """Send key combination Ctrl+(key) or Command+(key) for MacOS""" + page.press("Meta+{}".format(key)) if os.uname()[0] == "Darwin" else page.press("Control+{}".format(key)) + 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 @@ -398,6 +414,7 @@ def trigger_keystrokes(browser, *keys): 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. @@ -439,7 +456,6 @@ def is_focused_on(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) From 7c00171f16e31288c762880e5ab7cef09db6232a Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 12 Aug 2022 13:50:03 -0400 Subject: [PATCH 003/131] WIP util edits. --- nbclassic/tests/end_to_end/test_playwright.py | 6 +++--- nbclassic/tests/end_to_end/utils.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index 32112b54a..fa3f46181 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -11,9 +11,9 @@ def test_execute_code(notebook): # browser = notebook.browser - def clear_outputs(): - return notebook.evaluate( - "Jupyter.notebook.clear_all_output();") + # def clear_outputs(): + # return notebook.evaluate( + # "Jupyter.notebook.clear_all_output();") # Execute cell with Javascript API notebook.edit_cell(index=0, content='a=10; print(a)') diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 6131bc1f6..b850cc4ab 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -1,4 +1,6 @@ +import datetime import os +import time from contextlib import contextmanager from os.path import join as pjoin @@ -131,8 +133,16 @@ def _wait_for_start(self): # wait_for_selector(self.browser, '.cell') self.page.locator('.cell') # TODO: Refactor/fix - import time time.sleep(10) + # begin = datetime.datetime.now() + # while (datetime.datetime.now() - begin).seconds < 100: + # while not self.is_kernel_running(): + # print(self.is_kernel_running()) + # time.sleep(.1) + # else: + # print("Kernel running!") + # else: + # raise Exception('Kernel not running!') # WebDriverWait(self.browser, 10).until( # lambda drvr: self.is_kernel_running() # ) From d30b82e4a920edcfa4bb06aa2874363f145798ff Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 6 Sep 2022 15:53:34 -0400 Subject: [PATCH 004/131] Added refactors from team session. --- nbclassic/tests/end_to_end/test_playwright.py | 10 +++++- nbclassic/tests/end_to_end/utils.py | 33 +++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index fa3f46181..ecf5d8831 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -14,12 +14,20 @@ def test_execute_code(notebook): # def clear_outputs(): # return notebook.evaluate( # "Jupyter.notebook.clear_all_output();") + page = notebook.page + page.pause() + page.reload() + title = page.title() + # notebook_a_tag = page.locator('a[href=\"http://localhost:8888/a@b/notebooks/Untitled.ipynb\"]') + notebook_a_tag = page.locator('#notebook_list > div:nth-child(4) > div > a') + new_page = notebook_a_tag.click() + page.goto('http://localhost:8888/a@b/notebooks/Untitled.ipynb') # Execute cell with Javascript API notebook.edit_cell(index=0, content='a=10; print(a)') notebook.evaluate("Jupyter.notebook.get_cell(0).execute();") outputs = notebook.wait_for_cell_output(0) - assert outputs[0].text == '10' + assert outputs.inner_text().strip() == '10' # # Execute cell with Shift-Enter # notebook.edit_cell(index=0, content='a=11; print(a)') diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index b850cc4ab..306ad1e12 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -156,7 +156,7 @@ def cells(self): """Gets all cells once they are visible. """ - return self.page.query_selector_all("cell") + return self.page.query_selector_all(".cell") @property def current_index(self): @@ -174,15 +174,16 @@ def disable_autosave_and_onbeforeunload(self): This is most easily done by using js directly. """ - self.page.evaluate("window.onbeforeunload = null;") - self.page.evaluate("Jupyter.notebook.set_autosave_interval(0)") + self.evaluate("window.onbeforeunload = null;") + # Refactor this call, we can't call Jupyter.notebook on the /tree page during construction + # self.page.evaluate("Jupyter.notebook.set_autosave_interval(0)") 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(" + self.evaluate(" () => { return Jupyter.notebook.handle_command_mode(" "Jupyter.notebook.get_cell(" - "Jupyter.notebook.get_edit_index()))") + "Jupyter.notebook.get_edit_index())) }") def focus_cell(self, index=0): cell = self.cells[index] @@ -242,8 +243,8 @@ def get_cells_contents(self): 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].query_selector(output) # Find cell child elements + def get_cell_output(self, index=0, output='.output_subarea'): + return self.cells[index].as_element().query_selector(output) # Find cell child elements def wait_for_cell_output(self, index=0, timeout=10): # return WebDriverWait(self.browser, timeout).until( @@ -277,9 +278,9 @@ def edit_cell(self, cell=None, index=0, content="", render=False): for line_no, line in enumerate(content.splitlines()): if line_no != 0: - self.page.type("Enter") - self.page.type("Enter") - self.page.type(line) + self.page.keyboard.press("Enter") + self.page.keyboard.press("Enter") + self.page.keyboard.type(line) if render: self.execute_cell(self.current_index) @@ -334,13 +335,13 @@ 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()" + return self.evaluate( + "() => { 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) + self.evaluate(JS) @classmethod def new_notebook(cls, page, kernel_name='kernel-python3'): @@ -396,7 +397,11 @@ def shift(browser, k): def cmdtrl(page, key): """Send key combination Ctrl+(key) or Command+(key) for MacOS""" - page.press("Meta+{}".format(key)) if os.uname()[0] == "Darwin" else page.press("Control+{}".format(key)) + print(f"@@@@ key: {key}") + if os.uname()[0] == "Darwin": + page.keyboard.press("Meta+{}".format(key)) + else: + page.keyboard.press("Control+{}".format(key)) def alt(browser, k): From 09437c59248cad5c130b26221c18cd31ebe8c380 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 7 Sep 2022 14:09:50 -0400 Subject: [PATCH 005/131] WIP NotebookFrontend refactor --- nbclassic/tests/end_to_end/conftest.py | 40 +++--- nbclassic/tests/end_to_end/test_playwright.py | 30 +++-- nbclassic/tests/end_to_end/utils.py | 116 +++++++++++++----- 3 files changed, 127 insertions(+), 59 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index da330c7df..576f32aa1 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -5,6 +5,7 @@ from os.path import join as pjoin from subprocess import Popen from tempfile import mkstemp +from types import SimpleNamespace from urllib.parse import urljoin import pytest @@ -14,7 +15,7 @@ import nbformat from nbformat.v4 import new_notebook, new_code_cell -from .utils import NotebookFrontend +from .utils import NotebookFrontend, BROWSER, TREE_PAGE, SERVER_INFO def _wait_for_server(proc, info_file_path): @@ -99,20 +100,19 @@ def notebook_server(): @pytest.fixture(scope='session') -def playwright_driver(playwright): - # TODO: Fix - # if os.environ.get('SAUCE_USERNAME'): +def playwright_browser(playwright): + # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix # driver = make_sauce_driver() if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': - driver = playwright.chromium.launch() + browser = playwright.chromium.launch() else: - driver = playwright.firefox.launch() - driver = driver.new_context().new_page() + browser = playwright.firefox.launch() + browser_context = browser.new_context() - yield driver + yield browser_context - # # Teardown - # driver.quit() + # Teardown + browser.close() # @pytest.fixture(scope='module') @@ -130,16 +130,24 @@ def playwright_driver(playwright): @pytest.fixture(scope='module') -def authenticated_browser(playwright_driver, notebook_server): - playwright_driver.jupyter_server_info = notebook_server - playwright_driver.goto("{url}?token={token}".format(**notebook_server)) - return playwright_driver +def authenticated_browser_data(playwright_browser, notebook_server): + playwright_browser.jupyter_server_info = notebook_server + tree_page = playwright_browser.new_page() + tree_page.goto("{url}?token={token}".format(**notebook_server)) + + auth_browser_data = { + BROWSER: playwright_browser, + TREE_PAGE: tree_page, + SERVER_INFO: notebook_server, + } + + return auth_browser_data @pytest.fixture -def notebook(authenticated_browser): +def notebook_frontend(authenticated_browser_data): # tree_wh = authenticated_browser.current_window_handle - yield NotebookFrontend.new_notebook(authenticated_browser) + yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data) # authenticated_browser.switch_to.window(tree_wh) diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index ecf5d8831..201b53324 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -5,28 +5,32 @@ # TODO: Remove # from selenium.webdriver.common.keys import Keys -# from .utils import shift, cmdtrl +from .utils import shift, cmdtrl, TREE_PAGE, EDITOR_PAGE -def test_execute_code(notebook): +def test_execute_code(notebook_frontend): # browser = notebook.browser # def clear_outputs(): # return notebook.evaluate( # "Jupyter.notebook.clear_all_output();") - page = notebook.page - page.pause() - page.reload() - title = page.title() - # notebook_a_tag = page.locator('a[href=\"http://localhost:8888/a@b/notebooks/Untitled.ipynb\"]') - notebook_a_tag = page.locator('#notebook_list > div:nth-child(4) > div > a') - new_page = notebook_a_tag.click() - page.goto('http://localhost:8888/a@b/notebooks/Untitled.ipynb') + + # page = notebook.page + # page.pause() + # page.reload() + # title = page.title() + # # notebook_a_tag = page.locator('a[href=\"http://localhost:8888/a@b/notebooks/Untitled.ipynb\"]') + # notebook_a_tag = page.locator('#notebook_list > div:nth-child(4) > div > a') + # new_page = notebook_a_tag.click() + # page.goto('http://localhost:8888/a@b/notebooks/Untitled.ipynb') + # print(f'@@@ PAGECELLS :: {page.query_selector_all(".cell")}') + # print(f'@@@ PAGECELLS :: {page.url}') + # page.pause() # Execute cell with Javascript API - notebook.edit_cell(index=0, content='a=10; print(a)') - notebook.evaluate("Jupyter.notebook.get_cell(0).execute();") - outputs = notebook.wait_for_cell_output(0) + 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) assert outputs.inner_text().strip() == '10' # # Execute cell with Shift-Enter diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 306ad1e12..a95de4455 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -14,6 +14,13 @@ # from selenium.webdriver.remote.webelement import WebElement +# Key constants for browser_data +BROWSER = 'BROWSER' +TREE_PAGE = 'TREE_PAGE' +EDITOR_PAGE = 'EDITOR_PAGE' +SERVER_INFO = 'SERVER_INFO' + + 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( @@ -102,11 +109,22 @@ def __init__(self, message=""): class NotebookFrontend: - def __init__(self, page): - self.page = page + TREE_PAGE = TREE_PAGE + EDITOR_PAGE = EDITOR_PAGE + + def __init__(self, browser_data): + # 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() + self.page = self.tree_page # TODO remove/refactor this away + + # Do some needed frontend setup self._wait_for_start() - self.disable_autosave_and_onbeforeunload() - # self.current_cell = None # Defined/used below + self.disable_autosave_and_onbeforeunload() # TODO fix/refactor + self.current_cell = None # Defined/used below # TODO refactor/remove def __len__(self): return len(self.cells) @@ -131,7 +149,7 @@ def __iter__(self): def _wait_for_start(self): """Wait until the notebook interface is loaded and the kernel started""" # wait_for_selector(self.browser, '.cell') - self.page.locator('.cell') + self.tree_page.locator('.cell') # TODO: Refactor/fix time.sleep(10) # begin = datetime.datetime.now() @@ -149,14 +167,14 @@ def _wait_for_start(self): @property def body(self): - return self.page.locator("body") + return self.editor_page.locator("body") @property def cells(self): """Gets all cells once they are visible. """ - return self.page.query_selector_all(".cell") + return self.editor_page.query_selector_all(".cell") @property def current_index(self): @@ -165,16 +183,22 @@ def current_index(self): def index(self, cell): return self.cells.index(cell) - # TODO: Do we need to wrap in an anonymous function? - def evaluate(self, text): - return self.page.evaluate(text) + def evaluate(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!') + + return specified_page.evaluate(text) 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;") + self.evaluate("window.onbeforeunload = null;", page=TREE_PAGE) # Refactor this call, we can't call Jupyter.notebook on the /tree page during construction # self.page.evaluate("Jupyter.notebook.set_autosave_interval(0)") @@ -183,7 +207,7 @@ def to_command_mode(self): self.body.press('Escape') self.evaluate(" () => { return Jupyter.notebook.handle_command_mode(" "Jupyter.notebook.get_cell(" - "Jupyter.notebook.get_edit_index())) }") + "Jupyter.notebook.get_edit_index())) }", page=EDITOR_PAGE) def focus_cell(self, index=0): cell = self.cells[index] @@ -264,6 +288,7 @@ 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) + # TODO refactor this, it's terrible 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 """ @@ -273,14 +298,14 @@ def edit_cell(self, cell=None, index=0, content="", render=False): # Select & delete anything already in the cell self.current_cell.press('Enter') - cmdtrl(self.page, 'a') # TODO: FIX + cmdtrl(self.editor_page, 'a') # TODO: FIX self.current_cell.press('Delete') for line_no, line in enumerate(content.splitlines()): if line_no != 0: - self.page.keyboard.press("Enter") - self.page.keyboard.press("Enter") - self.page.keyboard.type(line) + self.editor_page.keyboard.press("Enter") + self.editor_page.keyboard.press("Enter") + self.editor_page.keyboard.type(line) if render: self.execute_cell(self.current_index) @@ -343,24 +368,55 @@ def clear_cell_output(self, index): JS = f'Jupyter.notebook.clear_output({index})' self.evaluate(JS) + def _open_notebook_editor_page(self): + tree_page = self.tree_page + + # 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() + tree_page.pause() + + # Grab the new editor page (was opened by the previous click) + open_pages = [pg for pg in self._browser_data[BROWSER].pages] + editor_pages = [pg for pg in open_pages if '/notebooks/' in pg.url] + print(f'@@@ ::: {open_pages}') + if not editor_pages: + raise Exception('Error, could not find open editor page!') + editor_page = editor_pages[0] # TODO, extra checks here? + + return editor_page + + # TODO: Refactor/consider removing this @classmethod - def new_notebook(cls, page, kernel_name='kernel-python3'): + def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3'): + browser = browser_data[BROWSER] + tree_page = browser_data[TREE_PAGE] + server_info = browser_data[SERVER_INFO] + # with new_window(page): - select_kernel(page, kernel_name=kernel_name) - return cls(page) + # select_kernel(tree_page, kernel_name=kernel_name) # TODO this is terrible, remove it + # tree_page.pause() + instance = cls(browser_data) + return instance -def select_kernel(page, 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 = page.locator('#new-dropdown-button') - new_button.click() - kernel_selector = f'#{kernel_name} a' - # kernel = wait_for_selector(page, kernel_selector, single=True) - kernel = page.locator(kernel_selector) - kernel.click() + +# # TODO: refactor/remove this +# def select_kernel(page, 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 = page.locator('#new-dropdown-button') +# new_button.click() +# kernel_selector = f'#{kernel_name} a' +# # kernel = wait_for_selector(page, kernel_selector, single=True) +# kernel = page.locator(kernel_selector) +# kernel.click() # @contextmanager From 8bb241b5546557040bb02be178939971ddf63c67 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 7 Sep 2022 14:15:29 -0400 Subject: [PATCH 006/131] WIP working test in debug mode. --- nbclassic/tests/end_to_end/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a95de4455..c208c367c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -274,6 +274,7 @@ def wait_for_cell_output(self, index=0, timeout=10): # return WebDriverWait(self.browser, timeout).until( # lambda b: self.get_cell_output(index) # ) + self.tree_page.pause() return self.get_cell_output() def set_cell_metadata(self, index, key, value): From 1a043667c04739c4602b4f0cae75b97b03a6193c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 7 Sep 2022 15:12:59 -0400 Subject: [PATCH 007/131] WIP working in debug mode, fix editor_page wait. --- nbclassic/tests/end_to_end/conftest.py | 4 +-- nbclassic/tests/end_to_end/utils.py | 41 ++++++++++++++++++++------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 576f32aa1..270c3a5a1 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -111,8 +111,8 @@ def playwright_browser(playwright): yield browser_context - # Teardown - browser.close() + # # Teardown + # browser.close() # @pytest.fixture(scope='module') diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index c208c367c..924876b29 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -379,15 +379,38 @@ def _open_notebook_editor_page(self): kernel_selector = f'#{kernel_name} a' new_notebook_element = tree_page.locator(kernel_selector) new_notebook_element.click() - tree_page.pause() - - # Grab the new editor page (was opened by the previous click) - open_pages = [pg for pg in self._browser_data[BROWSER].pages] - editor_pages = [pg for pg in open_pages if '/notebooks/' in pg.url] - print(f'@@@ ::: {open_pages}') - if not editor_pages: - raise Exception('Error, could not find open editor page!') - editor_page = editor_pages[0] # TODO, extra checks here? + + # tree_page.pause() + + # # Wait for a new page to be created + # with self._browser_data[BROWSER].expect_event("page") as event: + # new_notebook_element.click() + # foo = event.value + # assert '/notebooks/' in foo.url + # editor_page = foo + + TIMEOUT = 10 + begin = datetime.datetime.now() + while (datetime.datetime.now() - begin).seconds < TIMEOUT: + open_pages = self._browser_data[BROWSER].pages + # if [pg for pg in open_pages if '/notebooks/' in pg.url]: + if len(open_pages) > 1: + editor_page = [pg for pg in open_pages if 'tree' not in pg.url][0] + print(f'@@@ !! :::: {editor_page}') + break + print(f'@@@ OPENPAGES ::: {open_pages}') + time.sleep(.1) + else: + raise Exception('Error waiting for editor page!') + + # # Grab the new editor page (was opened by the previous click) + # open_pages = [pg for pg in self._browser_data[BROWSER].pages] + # editor_pages = [pg for pg in open_pages if '/notebooks/' in pg.url] + # print(f'@@@ ::: {open_pages}') + # if not editor_pages: + # raise Exception('Error, could not find open editor page!') + + # editor_page = editor_pages[0] # TODO, extra checks here? return editor_page From 269fd615835c4d217173ae32f1388242725e6f08 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 7 Sep 2022 15:21:55 -0400 Subject: [PATCH 008/131] Working (ROUGH) POC test (todo remove explicit waits). --- nbclassic/tests/end_to_end/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 924876b29..33fbde019 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -274,7 +274,20 @@ def wait_for_cell_output(self, index=0, timeout=10): # return WebDriverWait(self.browser, timeout).until( # lambda b: self.get_cell_output(index) # ) - self.tree_page.pause() + # self.tree_page.pause() + + # TODO refactor/remove + TIMEOUT = 30 + begin = datetime.datetime.now() + while (datetime.datetime.now() - begin).seconds < TIMEOUT: + condition = self.editor_page.query_selector_all('.output_subarea') + if condition: + print(f'@@@ !! :::: {condition}') + break + time.sleep(.1) + else: + raise Exception('Error waiting for editor page!') + return self.get_cell_output() def set_cell_metadata(self, index, key, value): @@ -389,7 +402,7 @@ def _open_notebook_editor_page(self): # assert '/notebooks/' in foo.url # editor_page = foo - TIMEOUT = 10 + TIMEOUT = 30 begin = datetime.datetime.now() while (datetime.datetime.now() - begin).seconds < TIMEOUT: open_pages = self._browser_data[BROWSER].pages From 8f43e49ba6848711dca7aec6cd0691245e5a1a92 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 7 Sep 2022 15:26:58 -0400 Subject: [PATCH 009/131] Cleanup. --- nbclassic/tests/end_to_end/utils.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 33fbde019..de95c91fb 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -393,15 +393,7 @@ def _open_notebook_editor_page(self): new_notebook_element = tree_page.locator(kernel_selector) new_notebook_element.click() - # tree_page.pause() - - # # Wait for a new page to be created - # with self._browser_data[BROWSER].expect_event("page") as event: - # new_notebook_element.click() - # foo = event.value - # assert '/notebooks/' in foo.url - # editor_page = foo - + # TODO refactor/remove TIMEOUT = 30 begin = datetime.datetime.now() while (datetime.datetime.now() - begin).seconds < TIMEOUT: @@ -416,15 +408,6 @@ def _open_notebook_editor_page(self): else: raise Exception('Error waiting for editor page!') - # # Grab the new editor page (was opened by the previous click) - # open_pages = [pg for pg in self._browser_data[BROWSER].pages] - # editor_pages = [pg for pg in open_pages if '/notebooks/' in pg.url] - # print(f'@@@ ::: {open_pages}') - # if not editor_pages: - # raise Exception('Error, could not find open editor page!') - - # editor_page = editor_pages[0] # TODO, extra checks here? - return editor_page # TODO: Refactor/consider removing this From 88bf45b08e70a58b6fd83c908e03d47b999c64a9 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 12 Sep 2022 12:07:16 -0400 Subject: [PATCH 010/131] Added checks for frontend objects. --- nbclassic/tests/end_to_end/utils.py | 46 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index de95c91fb..c86b78443 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -151,7 +151,22 @@ def _wait_for_start(self): # wait_for_selector(self.browser, '.cell') self.tree_page.locator('.cell') # TODO: Refactor/fix - time.sleep(10) + # time.sleep(10) + + # TODO refactor/remove + TIMEOUT = 30 + begin = datetime.datetime.now() + while (datetime.datetime.now() - begin).seconds < TIMEOUT: + condition = (self.is_jupyter_defined() + and self.is_notebook_defined() + and self.is_kernel_running()) + if condition: + print(f'@@@ !! :::: {condition}') + break + time.sleep(.1) + else: + raise Exception('Error waiting for notebook/kernel startup!') + # begin = datetime.datetime.now() # while (datetime.datetime.now() - begin).seconds < 100: # while not self.is_kernel_running(): @@ -373,9 +388,36 @@ def run_all(self): def trigger_keydown(self, keys): trigger_keystrokes(self.body, keys) + 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() }" + "() => { return Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected() }", + page=EDITOR_PAGE ) def clear_cell_output(self, index): From 4765a4971d66ce5ccfd02c0fb876285ed415e825 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 12 Sep 2022 14:41:43 -0400 Subject: [PATCH 011/131] Added wait method. --- nbclassic/tests/end_to_end/utils.py | 73 +++++++++++++---------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index c86b78443..713653d3d 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -101,6 +101,12 @@ def multiple_found(driver): return wait.until(multiple_found) +class TimeoutError(Exception): + + def get_result(self): + return None if not self.args else self.args[0] + + class CellTypeError(ValueError): def __init__(self, message=""): @@ -150,35 +156,13 @@ def _wait_for_start(self): """Wait until the notebook interface is loaded and the kernel started""" # wait_for_selector(self.browser, '.cell') self.tree_page.locator('.cell') - # TODO: Refactor/fix - # time.sleep(10) - # TODO refactor/remove - TIMEOUT = 30 - begin = datetime.datetime.now() - while (datetime.datetime.now() - begin).seconds < TIMEOUT: - condition = (self.is_jupyter_defined() - and self.is_notebook_defined() - and self.is_kernel_running()) - if condition: - print(f'@@@ !! :::: {condition}') - break - time.sleep(.1) - else: - raise Exception('Error waiting for notebook/kernel startup!') - - # begin = datetime.datetime.now() - # while (datetime.datetime.now() - begin).seconds < 100: - # while not self.is_kernel_running(): - # print(self.is_kernel_running()) - # time.sleep(.1) - # else: - # print("Kernel running!") - # else: - # raise Exception('Kernel not running!') - # WebDriverWait(self.browser, 10).until( - # lambda drvr: self.is_kernel_running() - # ) + def check_is_kernel_running(): + return (self.is_jupyter_defined() + and self.is_notebook_defined() + and self.is_kernel_running()) + + self._wait_for_condition(check_is_kernel_running) @property def body(self): @@ -285,23 +269,30 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): def get_cell_output(self, index=0, output='.output_subarea'): return self.cells[index].as_element().query_selector(output) # Find cell child elements - def wait_for_cell_output(self, index=0, timeout=10): - # return WebDriverWait(self.browser, timeout).until( - # lambda b: self.get_cell_output(index) - # ) - # self.tree_page.pause() - + 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 - TIMEOUT = 30 + begin = datetime.datetime.now() - while (datetime.datetime.now() - begin).seconds < TIMEOUT: - condition = self.editor_page.query_selector_all('.output_subarea') + while (datetime.datetime.now() - begin).seconds < timeout: + condition = check_func() if condition: - print(f'@@@ !! :::: {condition}') - break - time.sleep(.1) + return condition + time.sleep(period) else: - raise Exception('Error waiting for editor page!') + try: + condition + except NameError: + raise TimeoutError() + raise TimeoutError(condition) + + def wait_for_cell_output(self, index=0, timeout=10): + # TODO refactor/remove + + def cell_output_check(): + return self.editor_page.query_selector_all('.output_subarea') + + self._wait_for_condition(cell_output_check) return self.get_cell_output() From e1abe465de97d6f4ec4a4b94515783c84238b3e0 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 12 Sep 2022 15:56:29 -0400 Subject: [PATCH 012/131] Added more test logic. --- nbclassic/tests/end_to_end/test_playwright.py | 38 +++++++------------ nbclassic/tests/end_to_end/utils.py | 20 +++++++++- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index 201b53324..08eb61d9c 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -9,23 +9,11 @@ def test_execute_code(notebook_frontend): - # browser = notebook.browser - - # def clear_outputs(): - # return notebook.evaluate( - # "Jupyter.notebook.clear_all_output();") - - # page = notebook.page - # page.pause() - # page.reload() - # title = page.title() - # # notebook_a_tag = page.locator('a[href=\"http://localhost:8888/a@b/notebooks/Untitled.ipynb\"]') - # notebook_a_tag = page.locator('#notebook_list > div:nth-child(4) > div > a') - # new_page = notebook_a_tag.click() - # page.goto('http://localhost:8888/a@b/notebooks/Untitled.ipynb') - # print(f'@@@ PAGECELLS :: {page.query_selector_all(".cell")}') - # print(f'@@@ PAGECELLS :: {page.url}') - # page.pause() + def clear_outputs(): + return notebook_frontend.evaluate( + "Jupyter.notebook.clear_all_output();", + page=EDITOR_PAGE + ) # Execute cell with Javascript API notebook_frontend.edit_cell(index=0, content='a=10; print(a)') @@ -33,14 +21,14 @@ def test_execute_code(notebook_frontend): outputs = notebook_frontend.wait_for_cell_output(0) assert outputs.inner_text().strip() == '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 Shift-Enter + notebook_frontend.edit_cell(index=0, content='a=11; print(a)') + clear_outputs() + notebook_frontend.press("Shift+Enter", EDITOR_PAGE) + outputs = notebook_frontend.wait_for_cell_output(0) + assert outputs.inner_text().strip() == '11' + notebook_frontend.delete_cell(index=1) + # # Execute cell with Ctrl-Enter # notebook.edit_cell(index=0, content='a=12; print(a)') # clear_outputs() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 713653d3d..145d367d2 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -182,6 +182,24 @@ def current_index(self): def index(self, cell): return self.cells.index(cell) + def press(self, keycode, 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.press(keycode) + + def type(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.type(text) + def evaluate(self, text, page): if page == TREE_PAGE: specified_page = self.tree_page @@ -355,7 +373,7 @@ def add_and_execute_cell(self, index=-1, cell_type="code", content=""): def delete_cell(self, index): self.focus_cell(index) self.to_command_mode() - self.current_cell.send_keys('dd') + self.current_cell.type('dd') def add_markdown_cell(self, index=-1, content="", render=True): self.add_cell(index, cell_type="markdown") From a3f7a884ebd61b600adf7223cb83bea32d3a5cd4 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 12 Sep 2022 16:11:11 -0400 Subject: [PATCH 013/131] Added more test logic. --- nbclassic/tests/end_to_end/test_playwright.py | 16 ++++++++-------- nbclassic/tests/end_to_end/utils.py | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py index 08eb61d9c..65106e0d4 100644 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ b/nbclassic/tests/end_to_end/test_playwright.py @@ -27,15 +27,15 @@ def clear_outputs(): notebook_frontend.press("Shift+Enter", EDITOR_PAGE) outputs = notebook_frontend.wait_for_cell_output(0) assert outputs.inner_text().strip() == '11' - notebook_frontend.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' - # + # TODO fix for platform-independent execute logic (mac uses meta+enter) + # Execute cell with Ctrl-Enter (or equivalent) + notebook_frontend.edit_cell(index=0, content='a=12; print(a)') + clear_outputs() + notebook_frontend.press("Control+Enter", EDITOR_PAGE) + outputs = notebook_frontend.wait_for_cell_output(0) + assert outputs.inner_text().strip() == '12' + # # Execute cell with toolbar button # notebook.edit_cell(index=0, content='a=13; print(a)') # clear_outputs() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 145d367d2..6fff4a663 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -524,7 +524,6 @@ def shift(browser, k): def cmdtrl(page, key): """Send key combination Ctrl+(key) or Command+(key) for MacOS""" - print(f"@@@@ key: {key}") if os.uname()[0] == "Darwin": page.keyboard.press("Meta+{}".format(key)) else: From 055c803f34be19f28a146f27fde88bb7af3e67bc Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 12 Sep 2022 17:10:39 -0400 Subject: [PATCH 014/131] Finished adding test logic, fixed wait_for_cell_output index bug --- .../tests/end_to_end/test_execute_code.py | 67 +++++++++++++++++ nbclassic/tests/end_to_end/test_playwright.py | 71 ------------------- nbclassic/tests/end_to_end/utils.py | 25 +++++-- 3 files changed, 87 insertions(+), 76 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_execute_code.py delete mode 100644 nbclassic/tests/end_to_end/test_playwright.py 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..916bbf507 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -0,0 +1,67 @@ +"""Proof of concept for playwright testing, uses a reimplementation of test_execute_code""" + + +from playwright.sync_api import Page, expect + +# TODO: Remove +# from selenium.webdriver.common.keys import Keys +from .utils import shift, cmdtrl, TREE_PAGE, 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) + assert outputs.inner_text().strip() == '10' # TODO fix/encapsulate inner_text + + # Execute cell with Shift-Enter + notebook_frontend.edit_cell(index=0, content='a=11; print(a)') + notebook_frontend.clear_all_output() + notebook_frontend.press("Shift+Enter", EDITOR_PAGE) + outputs = notebook_frontend.wait_for_cell_output(0) + assert outputs.inner_text().strip() == '11' + notebook_frontend.delete_cell(1) # Shift+Enter adds a cell + + # TODO fix for platform-independent execute logic (mac uses meta+enter) + # 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("Control+Enter", EDITOR_PAGE) + outputs = notebook_frontend.wait_for_cell_output(0) + assert outputs.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) + assert outputs.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) + outputs = notebook_frontend.wait_for_cell_output(0) + assert notebook_frontend.get_cell_output(1) is None + + # Execute a cell with stop_on_error=false + notebook_frontend.clear_all_output() + 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) + outputs = notebook_frontend.wait_for_cell_output(1) + assert outputs.inner_text().strip() == '14' diff --git a/nbclassic/tests/end_to_end/test_playwright.py b/nbclassic/tests/end_to_end/test_playwright.py deleted file mode 100644 index 65106e0d4..000000000 --- a/nbclassic/tests/end_to_end/test_playwright.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Proof of concept for playwright testing, uses a reimplementation of test_execute_code""" - - -from playwright.sync_api import Page, expect - -# TODO: Remove -# from selenium.webdriver.common.keys import Keys -from .utils import shift, cmdtrl, TREE_PAGE, EDITOR_PAGE - - -def test_execute_code(notebook_frontend): - def clear_outputs(): - return notebook_frontend.evaluate( - "Jupyter.notebook.clear_all_output();", - page=EDITOR_PAGE - ) - - # 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) - assert outputs.inner_text().strip() == '10' - - # Execute cell with Shift-Enter - notebook_frontend.edit_cell(index=0, content='a=11; print(a)') - clear_outputs() - notebook_frontend.press("Shift+Enter", EDITOR_PAGE) - outputs = notebook_frontend.wait_for_cell_output(0) - assert outputs.inner_text().strip() == '11' - - # TODO fix for platform-independent execute logic (mac uses meta+enter) - # Execute cell with Ctrl-Enter (or equivalent) - notebook_frontend.edit_cell(index=0, content='a=12; print(a)') - clear_outputs() - notebook_frontend.press("Control+Enter", EDITOR_PAGE) - outputs = notebook_frontend.wait_for_cell_output(0) - assert outputs.inner_text().strip() == '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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 6fff4a663..90a11f13c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -210,6 +210,25 @@ def evaluate(self, text, page): return specified_page.evaluate(text) + def clear_all_output(self): + return self.evaluate( + "Jupyter.notebook.clear_all_output();", + page=EDITOR_PAGE + ) + + def clear_cell_output(self, index): + JS = f'Jupyter.notebook.clear_output({index})' + self.evaluate(JS) + + def click_toolbar_execute_btn(self): + 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. @@ -312,7 +331,7 @@ def cell_output_check(): self._wait_for_condition(cell_output_check) - return self.get_cell_output() + 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}' @@ -429,10 +448,6 @@ def is_kernel_running(self): page=EDITOR_PAGE ) - def clear_cell_output(self, index): - JS = f'Jupyter.notebook.clear_output({index})' - self.evaluate(JS) - def _open_notebook_editor_page(self): tree_page = self.tree_page From 7372e9ff3913c68a94ce771c9632f3a898355959 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 13 Sep 2022 08:57:42 -0400 Subject: [PATCH 015/131] Small refactors/cleanup, disabled not-yet-used logic. --- nbclassic/tests/end_to_end/conftest.py | 6 +- .../tests/end_to_end/test_execute_code.py | 6 +- nbclassic/tests/end_to_end/utils.py | 539 +++++++++--------- 3 files changed, 267 insertions(+), 284 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 270c3a5a1..dc0a3e28d 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -101,7 +101,7 @@ def notebook_server(): @pytest.fixture(scope='session') def playwright_browser(playwright): - # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix + # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix this # driver = make_sauce_driver() if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': browser = playwright.chromium.launch() @@ -111,8 +111,8 @@ def playwright_browser(playwright): yield browser_context - # # Teardown - # browser.close() + # Teardown + browser.close() # @pytest.fixture(scope='module') diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 916bbf507..8d22f036d 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -1,11 +1,7 @@ """Proof of concept for playwright testing, uses a reimplementation of test_execute_code""" -from playwright.sync_api import Page, expect - -# TODO: Remove -# from selenium.webdriver.common.keys import Keys -from .utils import shift, cmdtrl, TREE_PAGE, EDITOR_PAGE +from .utils import TREE_PAGE, EDITOR_PAGE def test_execute_code(notebook_frontend): diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 90a11f13c..790c5fdf8 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -21,84 +21,84 @@ SERVER_INFO = 'SERVER_INFO' -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) +# 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 TimeoutError(Exception): @@ -132,25 +132,25 @@ def __init__(self, browser_data): self.disable_autosave_and_onbeforeunload() # TODO fix/refactor self.current_cell = None # Defined/used below # TODO refactor/remove - 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 __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""" @@ -214,11 +214,11 @@ def clear_all_output(self): return self.evaluate( "Jupyter.notebook.clear_all_output();", page=EDITOR_PAGE - ) + ) def clear_cell_output(self, index): JS = f'Jupyter.notebook.clear_output({index})' - self.evaluate(JS) + self.evaluate(JS, page=EDITOR_PAGE) def click_toolbar_execute_btn(self): execute_button = self.editor_page.locator( @@ -251,57 +251,57 @@ def focus_cell(self, index=0): 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 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].as_element().query_selector(output) # Find cell child elements @@ -317,11 +317,7 @@ def _wait_for_condition(self, check_func, timeout=30, period=.1): return condition time.sleep(period) else: - try: - condition - except NameError: - raise TimeoutError() - raise TimeoutError(condition) + raise TimeoutError() def wait_for_cell_output(self, index=0, timeout=10): # TODO refactor/remove @@ -333,17 +329,17 @@ def cell_output_check(): 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.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 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) # TODO refactor this, it's terrible def edit_cell(self, cell=None, index=0, content="", render=False): @@ -459,20 +455,11 @@ def _open_notebook_editor_page(self): new_notebook_element = tree_page.locator(kernel_selector) new_notebook_element.click() - # TODO refactor/remove - TIMEOUT = 30 - begin = datetime.datetime.now() - while (datetime.datetime.now() - begin).seconds < TIMEOUT: - open_pages = self._browser_data[BROWSER].pages - # if [pg for pg in open_pages if '/notebooks/' in pg.url]: - if len(open_pages) > 1: - editor_page = [pg for pg in open_pages if 'tree' not in pg.url][0] - print(f'@@@ !! :::: {editor_page}') - break - print(f'@@@ OPENPAGES ::: {open_pages}') - time.sleep(.1) - else: - raise Exception('Error waiting for editor page!') + def wait_for_new_page(): + return [pg for pg in self._browser_data[BROWSER].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 @@ -532,9 +519,9 @@ def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3'): # browser.switch_to.window(new_window_handles[0]) -def shift(browser, k): - """Send key combination Shift+(k)""" - trigger_keystrokes(browser, "shift-%s"%k) +# def shift(browser, k): +# """Send key combination Shift+(k)""" +# trigger_keystrokes(browser, "shift-%s"%k) def cmdtrl(page, key): @@ -545,96 +532,96 @@ def cmdtrl(page, key): page.keyboard.press("Control+{}".format(key)) -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 +# 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 From 2bc812768ec5d026beacff106a02d165d31ac523 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 13 Sep 2022 09:11:33 -0400 Subject: [PATCH 016/131] More code cleanup. --- nbclassic/tests/end_to_end/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 790c5fdf8..77c9c1541 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -125,7 +125,6 @@ def __init__(self, browser_data): # Define tree and editor attributes self.tree_page = browser_data[TREE_PAGE] self.editor_page = self._open_notebook_editor_page() - self.page = self.tree_page # TODO remove/refactor this away # Do some needed frontend setup self._wait_for_start() @@ -234,9 +233,8 @@ def disable_autosave_and_onbeforeunload(self): This is most easily done by using js directly. """ - self.evaluate("window.onbeforeunload = null;", page=TREE_PAGE) - # Refactor this call, we can't call Jupyter.notebook on the /tree page during construction - # self.page.evaluate("Jupyter.notebook.set_autosave_interval(0)") + 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""" From 099a1032a654e71c2d9e8a4a8ce821ae2a12a6d4 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 13 Sep 2022 12:00:45 -0400 Subject: [PATCH 017/131] Replaced cmdtrl func with encapsulated mod key method. --- .../tests/end_to_end/test_execute_code.py | 11 +- nbclassic/tests/end_to_end/utils.py | 112 ++++++++++-------- 2 files changed, 69 insertions(+), 54 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 8d22f036d..5bfa0573b 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -1,4 +1,4 @@ -"""Proof of concept for playwright testing, uses a reimplementation of test_execute_code""" +"""Test basic cell execution methods, related shortcuts, and error modes""" from .utils import TREE_PAGE, EDITOR_PAGE @@ -14,16 +14,19 @@ def test_execute_code(notebook_frontend): # Execute cell with Shift-Enter notebook_frontend.edit_cell(index=0, content='a=11; print(a)') notebook_frontend.clear_all_output() - notebook_frontend.press("Shift+Enter", EDITOR_PAGE) + notebook_frontend.press("Enter", EDITOR_PAGE, ["Shift"]) outputs = notebook_frontend.wait_for_cell_output(0) assert outputs.inner_text().strip() == '11' notebook_frontend.delete_cell(1) # Shift+Enter adds a cell - # TODO fix for platform-independent execute logic (mac uses meta+enter) # 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("Control+Enter", EDITOR_PAGE) + notebook_frontend.press( + "Enter", + EDITOR_PAGE, + modifiers=[notebook_frontend.get_platform_modifier_key()] + ) outputs = notebook_frontend.wait_for_cell_output(0) assert outputs.inner_text().strip() == '12' diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 77c9c1541..c64bc90e5 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -181,14 +181,26 @@ def current_index(self): def index(self, cell): return self.cells.index(cell) - def press(self, keycode, page): + def press(self, keycode, page, modifiers=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!') - specified_page.keyboard.press(keycode) + + mods = "" + if modifiers is not None: + mods = "+".join(m for m in modifiers) + + specified_page.keyboard.press(mods + "+" + keycode) + + 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 type(self, text, page): if page == TREE_PAGE: @@ -349,7 +361,7 @@ def edit_cell(self, cell=None, index=0, content="", render=False): # Select & delete anything already in the cell self.current_cell.press('Enter') - cmdtrl(self.editor_page, 'a') # TODO: FIX + self.press('a', EDITOR_PAGE, [self.get_platform_modifier_key()]) self.current_cell.press('Delete') for line_no, line in enumerate(content.splitlines()): @@ -360,55 +372,55 @@ def edit_cell(self, cell=None, index=0, content="", render=False): 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, ElementHandle): - 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 execute_cell(self, cell_or_index=None): + # if isinstance(cell_or_index, int): + # index = cell_or_index + # elif isinstance(cell_or_index, ElementHandle): + # 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=""): - 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_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 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 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 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_jupyter_defined(self): """Checks that the Jupyter object is defined on the frontend""" @@ -522,12 +534,12 @@ def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3'): # trigger_keystrokes(browser, "shift-%s"%k) -def cmdtrl(page, key): - """Send key combination Ctrl+(key) or Command+(key) for MacOS""" - if os.uname()[0] == "Darwin": - page.keyboard.press("Meta+{}".format(key)) - else: - page.keyboard.press("Control+{}".format(key)) +# def cmdtrl(page, key): +# """Send key combination Ctrl+(key) or Command+(key) for MacOS""" +# if os.uname()[0] == "Darwin": +# page.keyboard.press("Meta+{}".format(key)) +# else: +# page.keyboard.press("Control+{}".format(key)) # def alt(browser, k): From 3a1d6eeb04bc7dd4305c8c14de5c3f29fceaff23 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 14 Sep 2022 10:32:28 -0400 Subject: [PATCH 018/131] Cell output wait logic fix. --- nbclassic/tests/end_to_end/utils.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index c64bc90e5..2523b3330 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -19,6 +19,8 @@ TREE_PAGE = 'TREE_PAGE' EDITOR_PAGE = 'EDITOR_PAGE' SERVER_INFO = 'SERVER_INFO' +# Other constants +CELL_OUTPUT_SELECTOR = '.output_subarea' # def wait_for_selector(driver, selector, timeout=10, visible=False, single=False, wait_for_n=1, obscures=False): @@ -115,8 +117,10 @@ def __init__(self, message=""): class NotebookFrontend: + # 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): # Keep a reference to source data @@ -313,7 +317,7 @@ def focus_cell(self, index=0): # 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'): + def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): return self.cells[index].as_element().query_selector(output) # Find cell child elements def _wait_for_condition(self, check_func, timeout=30, period=.1): @@ -329,13 +333,17 @@ def _wait_for_condition(self, check_func, timeout=30, period=.1): else: raise TimeoutError() - def wait_for_cell_output(self, index=0, timeout=10): - # TODO refactor/remove - - def cell_output_check(): - return self.editor_page.query_selector_all('.output_subarea') - - self._wait_for_condition(cell_output_check) + def wait_for_cell_output(self, index=0, timeout=3): + 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) From be816bbd613a50a23e85e7b66ed3cba4759f5bb3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:02:23 -0400 Subject: [PATCH 019/131] Added WIP test_deletecell and related utils updates. --- .../end_to_end/manual_test_prototyper.py | 20 ++++++ nbclassic/tests/end_to_end/test_deletecell.py | 66 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 37 ++++++++--- 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 nbclassic/tests/end_to_end/manual_test_prototyper.py create mode 100644 nbclassic/tests/end_to_end/test_deletecell.py 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..97887a50c --- /dev/null +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -0,0 +1,20 @@ +"""Test basic cell execution methods, related shortcuts, and error modes""" + + +from .utils import TREE_PAGE, EDITOR_PAGE + + +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_notebook(cell_texts) + notebook_frontend._pause() 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..7ad5b439b --- /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 = 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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 2523b3330..a3a03d1d9 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -225,6 +225,9 @@ def evaluate(self, text, page): return specified_page.evaluate(text) + def _pause(self): + self.editor_page.pause() + def clear_all_output(self): return self.evaluate( "Jupyter.notebook.clear_all_output();", @@ -235,6 +238,21 @@ def clear_cell_output(self, index): 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_notebook(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): execute_button = self.editor_page.locator( "button[" @@ -390,14 +408,17 @@ def edit_cell(self, cell=None, index=0, content="", render=False): # self.focus_cell(index) # self.current_cell.press("Control+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_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) + # TODO fix this + if cell_type != 'code': + raise NotImplementedError('Error, non code cell_type is a TODO!') + # 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) From 5b5852417dc897af6933e9da2c12e3d896b327e3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:04:44 -0400 Subject: [PATCH 020/131] Renaming. --- nbclassic/tests/end_to_end/manual_test_prototyper.py | 2 +- nbclassic/tests/end_to_end/test_deletecell.py | 2 +- nbclassic/tests/end_to_end/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nbclassic/tests/end_to_end/manual_test_prototyper.py b/nbclassic/tests/end_to_end/manual_test_prototyper.py index 97887a50c..22965c737 100644 --- a/nbclassic/tests/end_to_end/manual_test_prototyper.py +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -16,5 +16,5 @@ def test_do_something(notebook_frontend): notebook_frontend.editor_page.pause() cell_texts = ['aa = 1', 'bb = 2', 'cc = 3'] a, b, c = cell_texts - notebook_frontend.populate_notebook(cell_texts) + notebook_frontend.populate(cell_texts) notebook_frontend._pause() diff --git a/nbclassic/tests/end_to_end/test_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py index 7ad5b439b..1c130232b 100644 --- a/nbclassic/tests/end_to_end/test_deletecell.py +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -19,7 +19,7 @@ def remove_all_cells(notebook): def test_delete_cells(notebook_frontend): a, b, c = INITIAL_CELLS - notebook = prefill_notebook(INITIAL_CELLS) + notebook.populate(INITIAL_CELLS) # Validate initial state assert notebook.get_cells_contents() == [a, b, c] diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a3a03d1d9..c3c09e5ba 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -244,7 +244,7 @@ def delete_all_cells(self): for _ in range(len(self.cells)): self.delete_cell(0) - def populate_notebook(self, cell_texts): + def populate(self, cell_texts): """Delete all cells, then add cells using the list of specified cell_texts""" self.delete_all_cells() From 837236041897cefddcccb46cb61b3c28c2f3746b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:12:21 -0400 Subject: [PATCH 021/131] Test/framework updates. --- .../tests/end_to_end/manual_test_prototyper.py | 1 + nbclassic/tests/end_to_end/test_deletecell.py | 5 +++-- nbclassic/tests/end_to_end/utils.py | 14 +++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/nbclassic/tests/end_to_end/manual_test_prototyper.py b/nbclassic/tests/end_to_end/manual_test_prototyper.py index 22965c737..44a53e273 100644 --- a/nbclassic/tests/end_to_end/manual_test_prototyper.py +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -17,4 +17,5 @@ def test_do_something(notebook_frontend): 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_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py index 1c130232b..8859ba826 100644 --- a/nbclassic/tests/end_to_end/test_deletecell.py +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -19,10 +19,11 @@ def remove_all_cells(notebook): def test_delete_cells(notebook_frontend): a, b, c = INITIAL_CELLS - notebook.populate(INITIAL_CELLS) + notebook_frontend.populate(INITIAL_CELLS) # Validate initial state - assert notebook.get_cells_contents() == [a, b, c] + assert notebook_frontend.get_cells_contents() == [a, b, c] + return for cell in range(0, 3): assert cell_is_deletable(notebook, cell) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index c3c09e5ba..003e059ab 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -327,13 +327,13 @@ def focus_cell(self, index=0): # # 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_cells_contents(self): + 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'): + return self.cells[index].query_selector(selector).text def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): return self.cells[index].as_element().query_selector(output) # Find cell child elements From a53cf8ed67b64f93b0d19412b737d9bcbdbcd4cd Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:17:26 -0400 Subject: [PATCH 022/131] Test updates. --- nbclassic/tests/end_to_end/test_deletecell.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py index 8859ba826..35aa1b298 100644 --- a/nbclassic/tests/end_to_end/test_deletecell.py +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -5,7 +5,7 @@ def cell_is_deletable(notebook, index): - JS = f'return Jupyter.notebook.get_cell({index}).is_deletable();' + JS = f'() => {{ return Jupyter.notebook.get_cell({index}).is_deletable(); }}' return notebook.evaluate(JS, EDITOR_PAGE) @@ -23,9 +23,10 @@ def test_delete_cells(notebook_frontend): # Validate initial state assert notebook_frontend.get_cells_contents() == [a, b, c] - return for cell in range(0, 3): - assert cell_is_deletable(notebook, cell) + assert cell_is_deletable(notebook_frontend, cell) + + return # TODO, finish remaining notebook.set_cell_metadata(0, 'deletable', 'false') notebook.set_cell_metadata(1, 'deletable', 0 From e1de729599eca7d8ec2c0c27de90a01e6d698b24 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:24:51 -0400 Subject: [PATCH 023/131] Test updates. --- nbclassic/tests/end_to_end/test_deletecell.py | 32 +++++++++---------- nbclassic/tests/end_to_end/utils.py | 8 ++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py index 35aa1b298..66b6f42af 100644 --- a/nbclassic/tests/end_to_end/test_deletecell.py +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -26,33 +26,33 @@ def test_delete_cells(notebook_frontend): for cell in range(0, 3): assert cell_is_deletable(notebook_frontend, cell) - return # TODO, finish remaining - - notebook.set_cell_metadata(0, 'deletable', 'false') - notebook.set_cell_metadata(1, 'deletable', 0 + notebook_frontend.set_cell_metadata(0, 'deletable', 'false') + notebook_frontend.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) + 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.delete_cell(0) - assert notebook.get_cells_contents() == [a, b, c] + notebook_frontend.delete_cell(0) + assert notebook_frontend.get_cells_contents() == [a, b, c] # Try to delete cell b (should succeed) - notebook.delete_cell(1) - assert notebook.get_cells_contents() == [a, c] + notebook_frontend.delete_cell(1) + assert notebook_frontend.get_cells_contents() == [a, c] # Try to delete cell c (should succeed) - notebook.delete_cell(1) - assert notebook.get_cells_contents() == [a] + notebook_frontend.delete_cell(1) + assert notebook_frontend.get_cells_contents() == [a] # Change the deletable state of cell a - notebook.set_cell_metadata(0, 'deletable', 'true') + notebook_frontend.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 + notebook_frontend.delete_cell(0) + assert len(notebook_frontend.cells) == 1 # it contains an empty cell + + return # TODO, finish remaining # Make sure copied cells are deletable notebook.edit_cell(index=0, content=a) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 003e059ab..d5b36182d 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -365,10 +365,10 @@ def wait_for_cell_output(self, index=0, timeout=3): 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.browser.execute_script(JS) - # + 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.browser.execute_script(JS) From d32d052beb58c4acd883d29fba91b5fe01013e60 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 10:37:44 -0400 Subject: [PATCH 024/131] Finished test_delete_cells() --- nbclassic/tests/end_to_end/test_deletecell.py | 24 +++++++++---------- nbclassic/tests/end_to_end/utils.py | 24 +++++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_deletecell.py b/nbclassic/tests/end_to_end/test_deletecell.py index 66b6f42af..72c01f4f4 100644 --- a/nbclassic/tests/end_to_end/test_deletecell.py +++ b/nbclassic/tests/end_to_end/test_deletecell.py @@ -52,17 +52,15 @@ def test_delete_cells(notebook_frontend): notebook_frontend.delete_cell(0) assert len(notebook_frontend.cells) == 1 # it contains an empty cell - return # TODO, finish remaining - # 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 + 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/utils.py b/nbclassic/tests/end_to_end/utils.py index d5b36182d..a20042a60 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -199,13 +199,6 @@ def press(self, keycode, page, modifiers=None): specified_page.keyboard.press(mods + "+" + keycode) - 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 type(self, text, page): if page == TREE_PAGE: specified_page = self.tree_page @@ -215,6 +208,23 @@ def type(self, text, page): raise Exception('Error, provide a valid page to evaluate from!') specified_page.keyboard.type(text) + def press_active(self, keycode, modifiers=None): + mods = "" + if modifiers is not None: + mods = "+".join(m for m in modifiers) + + self.current_cell.press(mods + "+" + keycode) + + def type_active(self, text): + self.current_cell.type(text) + + 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): if page == TREE_PAGE: specified_page = self.tree_page From b5bc97147505949b775b81eb86cf8da90b1d764d Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 13:15:55 -0400 Subject: [PATCH 025/131] Finished test_interrupt() --- nbclassic/tests/end_to_end/test_interrupt.py | 46 ++++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 45 ++++++++++++++----- 2 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_interrupt.py 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..d46398dcd --- /dev/null +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -0,0 +1,46 @@ +"""Test kernel interrupt""" + + +# from .utils import wait_for_selector +from .utils import TREE_PAGE, 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" + + 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_frontend.edit_cell(index=0, content=text) + + # return # TODO, finish remaining + + 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.inner_text() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a20042a60..bfad61b33 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -115,6 +115,10 @@ def __init__(self, message=""): self.message = message +class FrontendError(Exception): + pass + + class NotebookFrontend: # Some constants for users of the class @@ -196,8 +200,9 @@ def press(self, keycode, page, modifiers=None): mods = "" if modifiers is not None: mods = "+".join(m for m in modifiers) + mods += "+" - specified_page.keyboard.press(mods + "+" + keycode) + specified_page.keyboard.press(mods + keycode) def type(self, text, page): if page == TREE_PAGE: @@ -218,6 +223,26 @@ def press_active(self, keycode, modifiers=None): def type_active(self, text): self.current_cell.type(text) + def try_click_selector(self, selector, 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): + # 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) + def get_platform_modifier_key(self): """Jupyter Notebook uses different modifier keys on win (Control) vs mac (Meta)""" if os.uname()[0] == "Darwin": @@ -408,15 +433,15 @@ def edit_cell(self, cell=None, index=0, content="", render=False): 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, ElementHandle): - # 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 execute_cell(self, cell_or_index=None): + if isinstance(cell_or_index, int): + index = cell_or_index + elif isinstance(cell_or_index, ElementHandle): + 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 From 7f72539161f8aad6557454379de7d7fba2f9d0a5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 13:17:07 -0400 Subject: [PATCH 026/131] Minor cleanup. --- nbclassic/tests/end_to_end/test_interrupt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_interrupt.py b/nbclassic/tests/end_to_end/test_interrupt.py index d46398dcd..4659df4f6 100644 --- a/nbclassic/tests/end_to_end/test_interrupt.py +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -32,8 +32,6 @@ def test_interrupt(notebook_frontend): notebook_frontend.edit_cell(index=0, content=text) - # return # TODO, finish remaining - for interrupt_method in (interrupt_from_menu, interrupt_from_keyboard): notebook_frontend.clear_cell_output(0) notebook_frontend.to_command_mode() From ee196f41c88a10bb046cc40f8a1f45e7105ae954 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 13:20:59 -0400 Subject: [PATCH 027/131] Added test prototyper docs. --- nbclassic/tests/end_to_end/manual_test_prototyper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/manual_test_prototyper.py b/nbclassic/tests/end_to_end/manual_test_prototyper.py index 44a53e273..7121a4328 100644 --- a/nbclassic/tests/end_to_end/manual_test_prototyper.py +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -1,4 +1,11 @@ -"""Test basic cell execution methods, related shortcuts, and error modes""" +"""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 From 61b155ee7c0e14c241685f8f4acc7ea206baf4c3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 14:04:40 -0400 Subject: [PATCH 028/131] Revised docstring. --- nbclassic/tests/end_to_end/test_interrupt.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_interrupt.py b/nbclassic/tests/end_to_end/test_interrupt.py index 4659df4f6..2aac8b899 100644 --- a/nbclassic/tests/end_to_end/test_interrupt.py +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -19,11 +19,6 @@ def interrupt_from_keyboard(notebook_frontend): def test_interrupt(notebook_frontend): """ 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' From ba8d2b766a51fd68f30e7318ba63ee8c5b63a61b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 14:08:30 -0400 Subject: [PATCH 029/131] Fixed JS evaluate call in get_cell_contents() --- nbclassic/tests/end_to_end/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index bfad61b33..7b5311071 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -368,7 +368,7 @@ def get_cells_contents(self): return self.evaluate(JS, page=EDITOR_PAGE) def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): - return self.cells[index].query_selector(selector).text + return self.cells[index].query_selector(selector).inner_text() def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): return self.cells[index].as_element().query_selector(output) # Find cell child elements From a6a730e640c0108b4a5184d7752bde11ec4cf41b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 14:21:11 -0400 Subject: [PATCH 030/131] Updated cell data return format. --- .../tests/end_to_end/test_execute_code.py | 10 +++++----- nbclassic/tests/end_to_end/utils.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 5bfa0573b..436ca37bc 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -9,14 +9,14 @@ def test_execute_code(notebook_frontend): 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) - assert outputs.inner_text().strip() == '10' # TODO fix/encapsulate inner_text + assert outputs[notebook_frontend.CELL_TEXT].strip() == '10' # TODO fix/encapsulate inner_text # 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) - assert outputs.inner_text().strip() == '11' + assert outputs[notebook_frontend.CELL_TEXT].strip() == '11' notebook_frontend.delete_cell(1) # Shift+Enter adds a cell # Execute cell with Ctrl-Enter (or equivalent) @@ -28,14 +28,14 @@ def test_execute_code(notebook_frontend): modifiers=[notebook_frontend.get_platform_modifier_key()] ) outputs = notebook_frontend.wait_for_cell_output(0) - assert outputs.inner_text().strip() == '12' + assert outputs[notebook_frontend.CELL_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) - assert outputs.inner_text().strip() == '13' + assert outputs[notebook_frontend.CELL_TEXT].strip() == '13' notebook_frontend.delete_cell(1) # Toolbar execute button adds a cell # Set up two cells to test stopping on error @@ -63,4 +63,4 @@ def test_execute_code(notebook_frontend): cell1.execute(); """, page=EDITOR_PAGE) outputs = notebook_frontend.wait_for_cell_output(1) - assert outputs.inner_text().strip() == '14' + assert outputs[notebook_frontend.CELL_TEXT].strip() == '14' diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 7b5311071..33562a7e4 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -126,6 +126,13 @@ class NotebookFrontend: EDITOR_PAGE = EDITOR_PAGE CELL_OUTPUT_SELECTOR = CELL_OUTPUT_SELECTOR + CELL_INDEX = 'INDEX' + CELL_TEXT = 'TEXT' + _CELL_DATA_FORMAT = { + CELL_INDEX: None, # int + CELL_TEXT: None, # str + } + def __init__(self, browser_data): # Keep a reference to source data self._browser_data = browser_data @@ -371,7 +378,16 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): return self.cells[index].query_selector(selector).inner_text() def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): - return self.cells[index].as_element().query_selector(output) # Find cell child elements + cell = self.cells[index].as_element().query_selector(output) # Find cell child elements + + if cell is None: + return None + + cell_data = dict(self._CELL_DATA_FORMAT) + cell_data[self.CELL_INDEX] = index + cell_data[self.CELL_TEXT] = cell.inner_text() + + return cell_data 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""" From e8366dcdaeb3538d8615d771db53c4538bb5c5d5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 14:23:36 -0400 Subject: [PATCH 031/131] Updated test_interrupt(). --- nbclassic/tests/end_to_end/test_interrupt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_interrupt.py b/nbclassic/tests/end_to_end/test_interrupt.py index 2aac8b899..7c3f635d3 100644 --- a/nbclassic/tests/end_to_end/test_interrupt.py +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -36,4 +36,4 @@ def test_interrupt(notebook_frontend): # Wait for an output to appear output = notebook_frontend.wait_for_cell_output(0) - assert 'KeyboardInterrupt' in output.inner_text() + assert 'KeyboardInterrupt' in output[notebook_frontend.CELL_TEXT] From 5258f5e1ca2db2e3ef57f1d7464c27eba047e7a7 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 15 Sep 2022 15:22:32 -0400 Subject: [PATCH 032/131] Added test_find_and_replace() + framework updates. --- .../tests/end_to_end/test_find_and_replace.py | 20 +++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 22 +++++++++---------- 2 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_find_and_replace.py 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..7b0ec686b --- /dev/null +++ b/nbclassic/tests/end_to_end/test_find_and_replace.py @@ -0,0 +1,20 @@ +"""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" + + # replace the strings + notebook_frontend.find_and_replace(index=0, find_txt=find_str, replace_txt=replace_str) + + # check content of the cells + assert notebook_frontend.get_cells_contents() == [ + s.replace(find_str, replace_str) for s in INITIAL_CELLS + ] diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 33562a7e4..bfcf79207 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -330,17 +330,17 @@ def focus_cell(self, index=0): # 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 find_and_replace(self, index=0, find_txt='', replace_txt=''): + 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() + self.editor_page.locator('#findreplace_find_inp').type(find_txt) + self.editor_page.locator('#findreplace_replace_inp').type(replace_txt) + 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) From ebbd82781129994c520a484cbc5050cf5aca010b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 16 Sep 2022 09:21:37 -0400 Subject: [PATCH 033/131] Refactored to encapsulate playwright cell element objects. --- nbclassic/tests/end_to_end/utils.py | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index bfcf79207..e28e9bdcf 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -147,16 +147,16 @@ def __init__(self, browser_data): self.current_cell = None # Defined/used below # TODO refactor/remove # def __len__(self): - # return len(self.cells) + # return len(self._cells) # # def __getitem__(self, key): - # return self.cells[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 + # # includes: overwriting the entire self._cells object if you do # # self[:] = [] # # elif isinstance(key, slice): # # indices = (self.index(cell) for cell in self[key]) @@ -164,7 +164,7 @@ def __init__(self, browser_data): # # self.edit_cell(index=k, content=v, render=False) # # def __iter__(self): - # return (cell for cell in self.cells) + # return (cell for cell in self._cells) def _wait_for_start(self): """Wait until the notebook interface is loaded and the kernel started""" @@ -183,18 +183,31 @@ def body(self): return self.editor_page.locator("body") @property - def cells(self): + def _cells(self): """Gets all cells once they are visible. """ return self.editor_page.query_selector_all(".cell") + @property + def cells(self): + """Gets all cells once they are visible.""" + # self.cells is now a list of dicts containing info per-cell + # (self._cells returns cell objects, should not be used externally) + + cell_dicts = [ + {self.CELL_INDEX: index, self.CELL_TEXT: cell.inner_text()} + for index, cell in enumerate(self._cells) + ] + + return cell_dicts + @property def current_index(self): return self.index(self.current_cell) def index(self, cell): - return self.cells.index(cell) + return self._cells.index(cell) def press(self, keycode, page, modifiers=None): if page == TREE_PAGE: @@ -283,7 +296,7 @@ def clear_cell_output(self, index): def delete_all_cells(self): # Note: After deleting all cells, a single default cell will remain - for _ in range(len(self.cells)): + for _ in range(len(self._cells)): self.delete_cell(0) def populate(self, cell_texts): @@ -320,7 +333,7 @@ def to_command_mode(self): "Jupyter.notebook.get_edit_index())) }", page=EDITOR_PAGE) def focus_cell(self, index=0): - cell = self.cells[index] + cell = self._cells[index] cell.click() self.to_command_mode() self.current_cell = cell @@ -344,7 +357,7 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): # 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] + # cell = self._cells[index] # if cell_type == "markdown": # self.current_cell.send_keys("m") # elif cell_type == "raw": @@ -375,10 +388,10 @@ def get_cells_contents(self): return self.evaluate(JS, page=EDITOR_PAGE) def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): - return self.cells[index].query_selector(selector).inner_text() + return self._cells[index].query_selector(selector).inner_text() def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): - cell = self.cells[index].as_element().query_selector(output) # Find cell child elements + cell = self._cells[index].as_element().query_selector(output) # Find cell child elements if cell is None: return None @@ -403,11 +416,11 @@ def _wait_for_condition(self, check_func, timeout=30, period=.1): raise TimeoutError() def wait_for_cell_output(self, index=0, timeout=3): - if not self.cells: + if not self._cells: raise Exception('Error, no cells exist!') milliseconds_to_seconds = 1000 - cell = self.cells[index].as_element() + cell = self._cells[index].as_element() try: cell.wait_for_selector(CELL_OUTPUT_SELECTOR, timeout=timeout * milliseconds_to_seconds) except Exception: From d20d5b9a9fde681773411a35f6153f857cf326a1 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 16 Sep 2022 09:23:37 -0400 Subject: [PATCH 034/131] Comments. --- nbclassic/tests/end_to_end/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index e28e9bdcf..6e5eeed6d 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -195,6 +195,7 @@ def cells(self): # self.cells is now a list of dicts containing info per-cell # (self._cells returns cell objects, should not be used externally) + # This mirrors the self._CELL_DATA_FORMAT cell_dicts = [ {self.CELL_INDEX: index, self.CELL_TEXT: cell.inner_text()} for index, cell in enumerate(self._cells) From 28ec79563834f52513c123f85123319fc7302486 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 19 Sep 2022 16:10:28 -0400 Subject: [PATCH 035/131] Finished rough test_display_image (some TODOs). --- .../tests/end_to_end/test_display_image.py | 79 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 37 +++++++-- 2 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_display_image.py 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..d6eaec089 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_display_image.py @@ -0,0 +1,79 @@ +"""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) + # TODO refactor img element access/encapsulate + + # 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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 6e5eeed6d..594b1f9b9 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -284,6 +284,27 @@ def evaluate(self, text, page): def _pause(self): self.editor_page.pause() + def wait_for_tag(self, tag, page=None, cell_index=None): + 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 result + def clear_all_output(self): return self.evaluate( "Jupyter.notebook.clear_all_output();", @@ -498,14 +519,14 @@ def delete_cell(self, index): # 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 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 extend(self, values): # self.append(*values) # From 004cd9e7eea184d6d325d5e9edb78fc5d639a2a4 Mon Sep 17 00:00:00 2001 From: RRosio Date: Tue, 20 Sep 2022 11:08:00 -0700 Subject: [PATCH 036/131] WIP working test_buffering with reload from team session --- nbclassic/tests/end_to_end/conftest.py | 55 ++++++++++++++++++-- nbclassic/tests/end_to_end/test_buffering.py | 50 ++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 35 ++++++++----- 3 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_buffering.py diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index dc0a3e28d..f25466b2a 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -15,7 +15,7 @@ import nbformat from nbformat.v4 import new_notebook, new_code_cell -from .utils import NotebookFrontend, BROWSER, TREE_PAGE, SERVER_INFO +from .utils import NotebookFrontend, BROWSER, BROWSER_RAW, TREE_PAGE, SERVER_INFO def _wait_for_server(proc, info_file_path): @@ -107,9 +107,9 @@ def playwright_browser(playwright): browser = playwright.chromium.launch() else: browser = playwright.firefox.launch() - browser_context = browser.new_context() + # browser_context = browser.new_context() - yield browser_context + yield browser # Teardown browser.close() @@ -131,6 +131,8 @@ def playwright_browser(playwright): @pytest.fixture(scope='module') def authenticated_browser_data(playwright_browser, notebook_server): + browser_raw = playwright_browser + playwright_browser = browser_raw.new_context() playwright_browser.jupyter_server_info = notebook_server tree_page = playwright_browser.new_page() tree_page.goto("{url}?token={token}".format(**notebook_server)) @@ -139,6 +141,7 @@ def authenticated_browser_data(playwright_browser, notebook_server): BROWSER: playwright_browser, TREE_PAGE: tree_page, SERVER_INFO: notebook_server, + BROWSER_RAW: browser_raw, } return auth_browser_data @@ -167,3 +170,49 @@ def notebook_frontend(authenticated_browser_data): # return Notebook(selenium_driver) # # return inner + + +@pytest.fixture +def prefill_notebook(playwright_browser, notebook_server): + browser_raw = playwright_browser + playwright_browser = browser_raw.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 + playwright_browser.jupyter_server_info = notebook_server + # Open a new page in the browser and refer to it as the tree page + tree_page = playwright_browser.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: playwright_browser, + TREE_PAGE: tree_page, + SERVER_INFO: notebook_server, + BROWSER_RAW: browser_raw + } + + 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 \ No newline at end of file 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..bdac97d07 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_buffering.py @@ -0,0 +1,50 @@ +"""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[notebook_frontend.CELL_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) + notebook_frontend._pause() + # k == 2 + notebook_frontend.execute_cell(2) + notebook_frontend._pause() + # k == 6 + notebook_frontend.execute_cell(3) + notebook_frontend._pause() + # k == 7 + notebook_frontend.execute_cell(2) + notebook_frontend._pause() + notebook_frontend.execute_cell(4) + notebook_frontend._pause() + 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[notebook_frontend.CELL_TEXT].strip() == '7' diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 6e5eeed6d..9a9b8e5cb 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -19,6 +19,7 @@ TREE_PAGE = 'TREE_PAGE' EDITOR_PAGE = 'EDITOR_PAGE' SERVER_INFO = 'SERVER_INFO' +BROWSER_RAW = 'BROWSER_RAW' # Other constants CELL_OUTPUT_SELECTOR = '.output_subarea' @@ -133,13 +134,13 @@ class NotebookFrontend: CELL_TEXT: None, # str } - def __init__(self, browser_data): + def __init__(self, browser_data, existing_file_name=None): # 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() + self.editor_page = self._open_notebook_editor_page(existing_file_name) # Do some needed frontend setup self._wait_for_start() @@ -548,16 +549,24 @@ def is_kernel_running(self): page=EDITOR_PAGE ) - def _open_notebook_editor_page(self): - tree_page = self.tree_page + def wait_for_kernel_ready(self): + self.tree_page.locator(".kernel_idle_icon") - # 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 _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('div.list_item:nth-child(4) > div:nth-child(1) > a:nth-child(3)') + 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].pages if 'tree' not in pg.url] @@ -569,7 +578,7 @@ def wait_for_new_page(): # TODO: Refactor/consider removing this @classmethod - def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3'): + def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', existing_file_name=None): browser = browser_data[BROWSER] tree_page = browser_data[TREE_PAGE] server_info = browser_data[SERVER_INFO] @@ -577,7 +586,7 @@ def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3'): # with new_window(page): # select_kernel(tree_page, kernel_name=kernel_name) # TODO this is terrible, remove it # tree_page.pause() - instance = cls(browser_data) + instance = cls(browser_data, existing_file_name) return instance From ecebe2aa155567379bd562efac6c9db0423ea580 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 21 Sep 2022 09:47:02 -0400 Subject: [PATCH 037/131] Finished test_markdown. --- nbclassic/tests/end_to_end/conftest.py | 2 +- nbclassic/tests/end_to_end/test_markdown.py | 47 ++++++++++++++ nbclassic/tests/end_to_end/utils.py | 69 +++++++++++---------- 3 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_markdown.py diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index f25466b2a..6a44f2c9a 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -215,4 +215,4 @@ def inner(cells): 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 \ No newline at end of file + return inner diff --git a/nbclassic/tests/end_to_end/test_markdown.py b/nbclassic/tests/end_to_end/test_markdown.py new file mode 100644 index 000000000..4af11632e --- /dev/null +++ b/nbclassic/tests/end_to_end/test_markdown.py @@ -0,0 +1,47 @@ +"""Test markdown rendering""" + + +from nbformat.v4 import new_markdown_cell + +from .utils import TREE_PAGE, 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.query_selector(".text_cell_render") + for cell in nb._cells + if all([c in cell.get_attribute("class") for c in cl])] + return [x.inner_html().strip() + for x in rendered_cells + if x is not None] + + +def test_markdown_cell(prefill_notebook): + 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(notebook_frontend) == [ + '

Foo

', + '

Bar

', + '

Baz

', + '
x = 1
', + '
x = 1
', + '
' +
+        's = "$"\n' +
+        't = "$"
' + ] + + +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(); + """, page=EDITOR_PAGE) + assert notebook_frontend.get_cell_contents(1) == "#" * i + " " + notebook_frontend.delete_cell(1) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 46a7f1057..e79ad0e1f 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from os.path import join as pjoin -from playwright.sync_api import ElementHandle +from playwright.sync_api import ElementHandle, JSHandle # from selenium.webdriver import ActionChains # from selenium.webdriver.common.by import By @@ -377,32 +377,34 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): self.editor_page.locator('#findreplace_replace_inp').type(replace_txt) 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.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 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. + """ + # wait = WebDriverWait(self.browser, 10) + # element = wait.until(EC.staleness_of(cell)) + + cell.wait_for_element_state('hidden') + # def wait_for_element_availability(self, element): # _wait_for(self.browser, By.CLASS_NAME, element, visible=True) @@ -483,12 +485,13 @@ def edit_cell(self, cell=None, index=0, content="", render=False): self.editor_page.keyboard.press("Enter") self.editor_page.keyboard.type(line) if render: - self.execute_cell(self.current_index) + 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") @@ -504,8 +507,8 @@ def add_cell(self, index=-1, cell_type="code", content=""): self.edit_cell(index=index, content=content) # TODO fix this if cell_type != 'code': - raise NotImplementedError('Error, non code cell_type is a TODO!') - # self.convert_cell_type(index=new_index, cell_type=cell_type) + # raise NotImplementedError('Error, non code cell_type is a TODO!') + 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) @@ -516,9 +519,9 @@ def delete_cell(self, 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 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: From d01301289aff96f784472882d7d14d6062568b02 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 21 Sep 2022 10:25:31 -0400 Subject: [PATCH 038/131] Finished test_merge_cells --- .../tests/end_to_end/test_merge_cells.py | 40 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 10 ++--- 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_merge_cells.py 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/utils.py b/nbclassic/tests/end_to_end/utils.py index e79ad0e1f..bf0ff7cc5 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -361,11 +361,11 @@ def focus_cell(self, index=0): 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 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=''): self.focus_cell(index) From 6dd67e9776ac9e458145ce1a1343ef1b2cd9227a Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 21 Sep 2022 15:07:13 -0700 Subject: [PATCH 039/131] working test_clipboard_multiselect --- .../end_to_end/test_clipboard_multiselect.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_clipboard_multiselect.py 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..359dc7850 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_clipboard_multiselect.py @@ -0,0 +1,49 @@ +"""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'] \ No newline at end of file From 2830a0f4fb9aff3c2a7e149914aa178536536ca5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 09:44:40 -0400 Subject: [PATCH 040/131] Added test_multiselect --- .../tests/end_to_end/test_multiselect.py | 71 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 11 +++ 2 files changed, 82 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_multiselect.py 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..6a2a433da --- /dev/null +++ b/nbclassic/tests/end_to_end/test_multiselect.py @@ -0,0 +1,71 @@ +"""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 + + # TODO: refactor _locate and encapsulate playwright element access + # Check that only one cell is selected according to CSS classes as well + selected_css = notebook_frontend._locate( + '.cell.jupyter-soft-selected, .cell.selected', EDITOR_PAGE) + assert selected_css.count() == 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/utils.py b/nbclassic/tests/end_to_end/utils.py index bf0ff7cc5..dae82bb55 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -306,6 +306,17 @@ def wait_for_tag(self, tag, page=None, cell_index=None): return result + def _locate(self, selector, page): + 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): return self.evaluate( "Jupyter.notebook.clear_all_output();", From 92db77e50a48454f39aed5968eed09f69e15294e Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 09:58:37 -0400 Subject: [PATCH 041/131] Added test_move_multiselection --- .../end_to_end/test_move_multiselection.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_move_multiselection.py 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..aed440c8f --- /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_oder(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_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_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_oder("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_oder("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_oder('move up at top', ['1', '2', '3', '4', '5', '6']) From 3aa943019eea38b323e1c975b8951990e2b589f5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 10:13:09 -0400 Subject: [PATCH 042/131] Added test_multiselect_toggle --- .../end_to_end/test_multiselect_toggle.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_multiselect_toggle.py 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" From acb6e2d35d0b9b84c1a6a4976b2574c5e5166bea Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 10:34:35 -0400 Subject: [PATCH 043/131] Added test_prompt_numbers --- .../tests/end_to_end/test_prompt_numbers.py | 35 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 8 ++--- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_prompt_numbers.py diff --git a/nbclassic/tests/end_to_end/test_prompt_numbers.py b/nbclassic/tests/end_to_end/test_prompt_numbers.py new file mode 100644 index 000000000..a9a277a04 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_prompt_numbers.py @@ -0,0 +1,35 @@ +"""Test multiselect toggle + +TODO: This changes the In []: label preceding the cell, what's the purpose of this? +""" + + +def test_prompt_numbers(prefill_notebook): + notebook_frontend = prefill_notebook(['print("a")']) + + def get_prompt(): + return ( + notebook_frontend._cells[0].query_selector('.input') + .query_selector('.input_prompt') + .inner_html().strip() + ) + + def set_prompt(value): + notebook_frontend.set_cell_input_prompt(0, value) + + assert get_prompt() == "In [ ]:" + + set_prompt(2) + assert get_prompt() == "In [2]:" + + set_prompt(0) + assert get_prompt() == "In [0]:" + + set_prompt("'*'") + assert get_prompt() == "In [*]:" + + set_prompt("undefined") + assert get_prompt() == "In [ ]:" + + set_prompt("null") + assert get_prompt() == "In [ ]:" diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index dae82bb55..a2c22db94 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -472,10 +472,10 @@ def set_cell_metadata(self, index, key, value): # 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 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) # TODO refactor this, it's terrible def edit_cell(self, cell=None, index=0, content="", render=False): From f529dd942acbdc1376d3ffa99a03e3e50cdf7c9c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 10:53:49 -0400 Subject: [PATCH 044/131] Added test_dualmode_arrows --- .../tests/end_to_end/test_dualmode_arrows.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_arrows.py 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..c68bc8cce --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_arrows.py @@ -0,0 +1,107 @@ +"""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)] + # TODO^ Remove _locate/encapsulate element access + [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"] From 65cc783f93758bb2f935e836fd9376d690826e67 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 14:20:35 -0400 Subject: [PATCH 045/131] Added test_dualmode_cellmode --- .../end_to_end/test_dualmode_cellmode.py | 63 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 24 +++---- 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_cellmode.py 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/utils.py b/nbclassic/tests/end_to_end/utils.py index a2c22db94..a2b003a7d 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -469,9 +469,9 @@ 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.browser.execute_script(JS) + 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})' @@ -486,15 +486,17 @@ def edit_cell(self, cell=None, index=0, content="", render=False): self.focus_cell(index) # Select & delete anything already in the cell - self.current_cell.press('Enter') + self.press('Enter', EDITOR_PAGE) self.press('a', EDITOR_PAGE, [self.get_platform_modifier_key()]) - self.current_cell.press('Delete') - - for line_no, line in enumerate(content.splitlines()): - if line_no != 0: - self.editor_page.keyboard.press("Enter") - self.editor_page.keyboard.press("Enter") - self.editor_page.keyboard.type(line) + self.press('Delete', EDITOR_PAGE) + + self.type(content, page=EDITOR_PAGE) + # TODO cleanup + # for line_no, line in enumerate(content.splitlines()): + # if line_no != 0: + # self.editor_page.keyboard.press("Enter") + # self.editor_page.keyboard.press("Enter") + # self.editor_page.keyboard.type(line) if render: self.execute_cell(index) From f53174dd9127712a9096295a6bf1b51a23427204 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 22 Sep 2022 16:09:04 -0400 Subject: [PATCH 046/131] Added test_dualmode_clipboard --- .../end_to_end/test_dualmode_clipboard.py | 58 ++++++++ nbclassic/tests/end_to_end/utils.py | 138 +++++++++--------- 2 files changed, 127 insertions(+), 69 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_clipboard.py 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..a4ec01c80 --- /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/utils.py b/nbclassic/tests/end_to_end/utils.py index a2b003a7d..be906bcb5 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -706,72 +706,72 @@ def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', exist # 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 + + +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 From 32f9828e741bcd2ac34c33b656956b9bacce23c0 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 22 Sep 2022 18:58:37 -0700 Subject: [PATCH 047/131] working test undelete --- nbclassic/tests/end_to_end/test_undelete.py | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_undelete.py diff --git a/nbclassic/tests/end_to_end/test_undelete.py b/nbclassic/tests/end_to_end/test_undelete.py new file mode 100644 index 000000000..a38195622 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_undelete.py @@ -0,0 +1,92 @@ +from .utils import EDITOR_PAGE + + +def undelete(nb): + nb.evaluate('() => Jupyter.notebook.undelete_cell();', page=EDITOR_PAGE) + +INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")', 'print("d")'] + +def test_undelete_cells(prefill_notebook): + notebook = prefill_notebook(INITIAL_CELLS) + a, b, c, d = INITIAL_CELLS + + # Verify initial state + assert notebook.get_cells_contents() == [a, b, c, d] + + # Delete cells [1, 2] + notebook.focus_cell(1) + 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.press('d+d', EDITOR_PAGE) + assert notebook.get_cells_contents() == [a] + + # Undelete d + undelete(notebook) + assert notebook.get_cells_contents() == [a, d] + + # Undelete b, c + undelete(notebook) + assert notebook.get_cells_contents() == [a, b, c, d] + + # Nothing more to undelete + undelete(notebook) + assert notebook.get_cells_contents() == [a, b, c, d] + + # Delete first two cells and restore + notebook.focus_cell(0) + 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) + 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] + + # Merge cells [1, 2], restore the deleted one + bc = b + "\n\n" + c + notebook.focus_cell(1) + 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] + + # Merge cells [2, 3], restore the deleted one + cd = c + "\n\n" + d + notebook.focus_cell(-1) + 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] + + # Reset contents to [a, b, c, d] -------------------------------------- + notebook.edit_cell(index=1, content=b) + notebook.edit_cell(index=2, content=c) + assert notebook.get_cells_contents() == [a, b, c, d] + + # Merge cell below, restore the deleted one + ab = a + "\n\n" + b + notebook.focus_cell(0) + 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] + + # Merge cell above, restore the deleted one + cd = c + "\n\n" + d + notebook.focus_cell(-1) + 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] From 7c6a507ac2b3f8fe729d97f467d1a80c6894a235 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 23 Sep 2022 09:32:36 -0400 Subject: [PATCH 048/131] Added test_dualmode_insertcell --- .../end_to_end/test_dualmode_insertcell.py | 58 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_insertcell.py 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..2925d882f --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dualmode_insertcell.py @@ -0,0 +1,58 @@ +"""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") + + notebook_frontend.editor_page.pause() + # insert code cell above + notebook_frontend.press_active("a") + notebook_frontend.editor_page.pause() + 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/utils.py b/nbclassic/tests/end_to_end/utils.py index be906bcb5..7d5abafbc 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -239,8 +239,9 @@ def press_active(self, keycode, modifiers=None): mods = "" if modifiers is not None: mods = "+".join(m for m in modifiers) + mods += "+" - self.current_cell.press(mods + "+" + keycode) + self.current_cell.press(mods + keycode) def type_active(self, text): self.current_cell.type(text) From 40d1dd3527963068780a4f9b83d34284f4cfd5fe Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 23 Sep 2022 06:42:41 -0700 Subject: [PATCH 049/131] working test_shutdown --- nbclassic/tests/end_to_end/test_shutdown.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_shutdown.py 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..79fd66e99 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_shutdown.py @@ -0,0 +1,18 @@ +"""Tests shutdown of the Kernel.""" +from .utils import EDITOR_PAGE + +def test_shutdown(prefill_notebook): + # notebook_frontend.edit_cell(content="print(21)") + 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) + + # Wait until all shutdown modal elements disappear before trying to execute the cell + # notebook_frontend.editor_page.query_selector_all("//div[contains(@class,'modal')]") + + notebook_frontend.execute_cell(0) + + assert not notebook_frontend.is_kernel_running() + assert notebook_frontend.get_cell_output() == None \ No newline at end of file From 3f2581b31b57d941722d5603e10d3368aafb1b79 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 23 Sep 2022 11:18:05 -0400 Subject: [PATCH 050/131] Added test_dualmode_execute --- .../tests/end_to_end/test_dualmode_execute.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_execute.py 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..62c72c9f4 --- /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 where added + notebook_frontend.focus_cell(7) + validate_dualmode_state(notebook_frontend, 'command', 7) From 36d1483d72aadbaf655d0ff7a36a1c016f7aa43e Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 23 Sep 2022 13:19:54 -0400 Subject: [PATCH 051/131] Added test_dualmode_markdown --- .../end_to_end/test_dualmode_markdown.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_dualmode_markdown.py 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 From b9877be44b81a692d472c653df4477c2e3a3d3e3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 27 Sep 2022 10:47:56 -0400 Subject: [PATCH 052/131] Added test_kernel_menu --- .../tests/end_to_end/test_kernel_menu.py | 56 +++++++++++++ nbclassic/tests/end_to_end/utils.py | 78 +++++++++++++++++-- 2 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_kernel_menu.py 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..32f5eb07a --- /dev/null +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -0,0 +1,56 @@ +"""Test kernel menu""" + + +from .utils import TREE_PAGE, 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_state('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() + + # TODO refactor _wait_for_condition call + 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.wait_for_selector('.modal-backdrop', EDITOR_PAGE) + modal.wait_for_state('hidden') + kernel_menu.click() + + notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() + # wait_for_selector(browser, menu_item, visible=True, single=True).click() + # TODO refactor _wait_for_condition call + notebook_frontend._wait_for_condition(lambda: notebook_frontend.is_kernel_running()) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 7d5abafbc..01985f945 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -120,6 +120,58 @@ class FrontendError(Exception): pass +class FrontendElement: + + def __init__(self, item): + # item should be a JSHandle, locator or ElementHandle + self._raw = item + self._element = item + self._bool = True # Was the item created successfully? + + # We need either a locator or an ElementHandle for most ops, obtain it + if item is None: + self._bool = False + elif not isinstance(item, ElementHandle) and isinstance(item, JSHandle): + as_element = item.as_element() + if as_element: + self._element = as_element + else: + self._bool = False + + def __bool__(self): + """Returns True if construction succeeded""" + return self._bool + + def click(self): + return self._element.click() + + def get_inner_text(self): + return self._element.inner_text() + + def get_attribute(self, attribute): + return self._element.get_attribute(attribute) + + def locate(self, selector): + element = self._element + + if hasattr(element, 'locator'): + result = element.locator(selector) + elif hasattr(element, 'query_selector'): + result = element.query_selector(selector) + else: + result = None + + return FrontendElement(result) + + def wait_for_state(self, state): + if hasattr(self._element, 'wait_for_element_state'): + self._element.wait_for_element_state(state) + elif hasattr(self._element, 'wait_for'): + self._element.wait_for(state) + else: + raise Exception('Unable to wait for state!') + + class NotebookFrontend: # Some constants for users of the class @@ -257,14 +309,14 @@ def try_click_selector(self, selector, page): elem.click() - # def wait_for_selector(self, selector, 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) + def wait_for_selector(self, selector, 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 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)""" @@ -286,6 +338,16 @@ def evaluate(self, text, page): def _pause(self): self.editor_page.pause() + def locate(self, selector, 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 wait_for_tag(self, tag, page=None, cell_index=None): if cell_index is None and page is None: raise FrontendError('Provide a page or cell to wait from!') From b7eff8b0e6784d80ba2d698015c001cac68217ee Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 28 Sep 2022 11:16:12 -0400 Subject: [PATCH 053/131] Cleanup/refactors for test_execute_code --- nbclassic/tests/end_to_end/test_execute_code.py | 4 ++-- nbclassic/tests/end_to_end/utils.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 436ca37bc..d6cc20319 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -1,7 +1,7 @@ """Test basic cell execution methods, related shortcuts, and error modes""" -from .utils import TREE_PAGE, EDITOR_PAGE +from .utils import EDITOR_PAGE def test_execute_code(notebook_frontend): @@ -9,7 +9,7 @@ def test_execute_code(notebook_frontend): 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) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '10' # TODO fix/encapsulate inner_text + assert outputs[notebook_frontend.CELL_TEXT].strip() == '10' # Execute cell with Shift-Enter notebook_frontend.edit_cell(index=0, content='a=11; print(a)') diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 01985f945..27d357726 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -326,6 +326,12 @@ def get_platform_modifier_key(self): 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: @@ -370,6 +376,7 @@ def wait_for_tag(self, tag, page=None, cell_index=None): return result 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 @@ -381,6 +388,7 @@ def _locate(self, selector, page): return specified_page.locator(selector) def clear_all_output(self): + """Clear cell outputs""" return self.evaluate( "Jupyter.notebook.clear_all_output();", page=EDITOR_PAGE @@ -540,7 +548,6 @@ 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) - # TODO refactor this, it's terrible 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 """ @@ -554,12 +561,6 @@ def edit_cell(self, cell=None, index=0, content="", render=False): self.press('Delete', EDITOR_PAGE) self.type(content, page=EDITOR_PAGE) - # TODO cleanup - # for line_no, line in enumerate(content.splitlines()): - # if line_no != 0: - # self.editor_page.keyboard.press("Enter") - # self.editor_page.keyboard.press("Enter") - # self.editor_page.keyboard.type(line) if render: self.execute_cell(index) From 055a354c9c0c32efa4fa5fc4e7fee01bdc299fd7 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 28 Sep 2022 11:41:21 -0400 Subject: [PATCH 054/131] Refactored test_dualmode_arrows/utils and related code. --- nbclassic/tests/end_to_end/test_dualmode_arrows.py | 4 +--- nbclassic/tests/end_to_end/test_multiselect.py | 5 ++--- nbclassic/tests/end_to_end/utils.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dualmode_arrows.py b/nbclassic/tests/end_to_end/test_dualmode_arrows.py index c68bc8cce..0db68ad76 100644 --- a/nbclassic/tests/end_to_end/test_dualmode_arrows.py +++ b/nbclassic/tests/end_to_end/test_dualmode_arrows.py @@ -76,11 +76,9 @@ def test_dualmode_arrows(notebook_frontend): 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)] - # TODO^ Remove _locate/encapsulate element access + [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) diff --git a/nbclassic/tests/end_to_end/test_multiselect.py b/nbclassic/tests/end_to_end/test_multiselect.py index 6a2a433da..693983481 100644 --- a/nbclassic/tests/end_to_end/test_multiselect.py +++ b/nbclassic/tests/end_to_end/test_multiselect.py @@ -21,11 +21,10 @@ def n_selected_cells(): notebook_frontend.focus_cell(0) assert n_selected_cells() == 1 - # TODO: refactor _locate and encapsulate playwright element access # Check that only one cell is selected according to CSS classes as well - selected_css = notebook_frontend._locate( + selected_css = notebook_frontend.locate_all( '.cell.jupyter-soft-selected, .cell.selected', EDITOR_PAGE) - assert selected_css.count() == 1 + assert len(selected_css) == 1 # Extend the selection down one extend_selection_by(1) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 27d357726..586a99f8e 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -354,6 +354,19 @@ def locate(self, selector, page): return FrontendElement(specified_page.locator(selector)) + def locate_all(self, selector, 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 wait_for_tag(self, tag, page=None, cell_index=None): if cell_index is None and page is None: raise FrontendError('Provide a page or cell to wait from!') @@ -375,6 +388,7 @@ def wait_for_tag(self, tag, page=None, cell_index=None): return result + # TODO remove this def _locate(self, selector, page): """Find an frontend element by selector (Tag, CSS, XPath etc.)""" result = None From 495d66c11031446b2ba93dd40b17d9be162faf1d Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 28 Sep 2022 11:56:37 -0400 Subject: [PATCH 055/131] Minor cleanup test_dualmode_clipboard --- nbclassic/tests/end_to_end/test_dualmode_clipboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_dualmode_clipboard.py b/nbclassic/tests/end_to_end/test_dualmode_clipboard.py index a4ec01c80..0ea408e78 100644 --- a/nbclassic/tests/end_to_end/test_dualmode_clipboard.py +++ b/nbclassic/tests/end_to_end/test_dualmode_clipboard.py @@ -21,7 +21,7 @@ def test_dualmode_clipboard(prefill_notebook): 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 + assert len(notebook_frontend.cells) == num_cells - 1 # A cell was removed notebook_frontend.focus_cell(2) notebook_frontend.press("v", EDITOR_PAGE) # Paste From 7de2f957adc075e6eff9a1a81e9412e0a1ea3afc Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 28 Sep 2022 12:02:17 -0400 Subject: [PATCH 056/131] Minor cleanup test_dualmode_execute --- nbclassic/tests/end_to_end/test_dualmode_execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_dualmode_execute.py b/nbclassic/tests/end_to_end/test_dualmode_execute.py index 62c72c9f4..5dc3a9097 100644 --- a/nbclassic/tests/end_to_end/test_dualmode_execute.py +++ b/nbclassic/tests/end_to_end/test_dualmode_execute.py @@ -71,6 +71,6 @@ def test_dualmode_execute(prefill_notebook): 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 where added + assert len(notebook_frontend.cells) == 8 # Cells were added notebook_frontend.focus_cell(7) validate_dualmode_state(notebook_frontend, 'command', 7) From aafb1a385994ae2eaaa5ef036e84d3f984c95705 Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 28 Sep 2022 14:48:33 -0700 Subject: [PATCH 057/131] test display isolation and utils wait selector added --- .../end_to_end/test_display_isolation.py | 90 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 66 ++++++++++++-- 2 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 nbclassic/tests/end_to_end/test_display_isolation.py diff --git a/nbclassic/tests/end_to_end/test_display_isolation.py b/nbclassic/tests/end_to_end/test_display_isolation.py new file mode 100644 index 000000000..ae02b10ff --- /dev/null +++ b/nbclassic/tests/end_to_end/test_display_isolation.py @@ -0,0 +1,90 @@ +"""Test display isolation. + +An object whose metadata contains an "isolated" tag must be isolated +from the rest of the document. +""" + + +from .utils import EDITOR_PAGE, TREE_PAGE + + +def test_display_isolation(notebook_frontend): + import_ln = "from IPython.core.display import HTML, SVG, display, display_svg" + notebook_frontend.edit_cell(index=0, content=import_ln) + notebook_frontend.execute_cell(0) + try: + isolated_html(notebook_frontend) + isolated_svg(notebook_frontend) + finally: + # Ensure we switch from iframe back to default content even if test fails + notebook_frontend.editor_page.main_frame # TODO: + + +def isolated_html(notebook): + """Test HTML display isolation. + + HTML styling rendered without isolation will affect the whole + document, whereas styling applied with isolation will affect only + the local display object. + """ + red = 'rgb(255, 0, 0)' + blue = 'rgb(0, 0, 255)' + test_str = "
Should turn red from non-isolation
" + notebook.add_and_execute_cell(content=f"display(HTML({test_str!r}))") + non_isolated = ( + f"" + f"
Should be red
") + display_ni = f"display(HTML({non_isolated!r}), metadata={{'isolated':False}})" + notebook.add_and_execute_cell(content=display_ni) + isolated = ( + f"" + f"
Should be blue
") + display_i = f"display(HTML({isolated!r}), metadata={{'isolated':True}})" + notebook.add_and_execute_cell(content=display_i) + + # The non-isolated div will be in the body + 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.locate('#test', page=EDITOR_PAGE) + notebook.editor_page.pause() + assert test_div.get_computed_property('color') == red + + # The isolated div will be in an iframe, only that element will be blue + 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): + """Test that multiple isolated SVGs have different scopes. + + Asserts that there no CSS leaks between two isolated SVGs. + """ + yellow = "rgb(255, 255, 0)" + black = "rgb(0, 0, 0)" + svg_1_str = f"""s1 = ''''''""" + svg_2_str = """s2 = ''''''""" + + notebook.add_and_execute_cell(content=svg_1_str) + notebook.add_and_execute_cell(content=svg_2_str) + notebook.add_and_execute_cell( + content="display_svg(SVG(s1), metadata=dict(isolated=True))") + notebook.add_and_execute_cell( + content="display_svg(SVG(s2), metadata=dict(isolated=True))") + iframes = notebook.wait_for_tag("iframe", page=EDITOR_PAGE) + + # The first rectangle will be red + 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.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)): + notebook.delete_cell(1) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 586a99f8e..ea0d1023f 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -131,7 +131,9 @@ def __init__(self, item): # We need either a locator or an ElementHandle for most ops, obtain it if item is None: self._bool = False - elif not isinstance(item, ElementHandle) and isinstance(item, JSHandle): + 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 @@ -151,6 +153,14 @@ def get_inner_text(self): 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 locate(self, selector): element = self._element @@ -221,9 +231,6 @@ def __init__(self, browser_data, existing_file_name=None): def _wait_for_start(self): """Wait until the notebook interface is loaded and the kernel started""" - # wait_for_selector(self.browser, '.cell') - self.tree_page.locator('.cell') - def check_is_kernel_running(): return (self.is_jupyter_defined() and self.is_notebook_defined() @@ -367,6 +374,49 @@ def locate_all(self, selector, page): element_list = [FrontendElement(result.nth(index)) for index in range(result.count())] return element_list + def wait_for_frame(self, count=None, name=None, page=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 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): + 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): if cell_index is None and page is None: raise FrontendError('Provide a page or cell to wait from!') @@ -601,9 +651,9 @@ def add_cell(self, index=-1, cell_type="code", content=""): # raise NotImplementedError('Error, non code cell_type is a TODO!') 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 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) @@ -688,7 +738,7 @@ def wait_for_new_page(): new_pages = self._wait_for_condition(wait_for_new_page) editor_page = new_pages[0] - + editor_page.wait_for_selector('.cell') return editor_page # TODO: Refactor/consider removing this From fb83d090f9f045b60f473ddcb63c15ea64f8d072 Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 5 Oct 2022 10:26:39 -0700 Subject: [PATCH 058/131] updates to the scope of fixtures, remove wait for selector causing timeouts, updating the notebook selector when opening --- nbclassic/tests/end_to_end/conftest.py | 10 +++++----- nbclassic/tests/end_to_end/utils.py | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 6a44f2c9a..852949970 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -35,7 +35,7 @@ def _wait_for_server(proc, info_file_path): raise RuntimeError("Didn't find %s in 30 seconds", info_file_path) -@pytest.fixture(scope='session') +@pytest.fixture(scope='function') def notebook_server(): info = {} with TemporaryDirectory() as td: @@ -99,7 +99,7 @@ def notebook_server(): # return driver -@pytest.fixture(scope='session') +@pytest.fixture(scope='function') def playwright_browser(playwright): # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix this # driver = make_sauce_driver() @@ -129,7 +129,7 @@ def playwright_browser(playwright): # authenticated_browser.switch_to.window(tree_wh) -@pytest.fixture(scope='module') +@pytest.fixture(scope='function') def authenticated_browser_data(playwright_browser, notebook_server): browser_raw = playwright_browser playwright_browser = browser_raw.new_context() @@ -147,7 +147,7 @@ def authenticated_browser_data(playwright_browser, notebook_server): return auth_browser_data -@pytest.fixture +@pytest.fixture(scope='function') def notebook_frontend(authenticated_browser_data): # tree_wh = authenticated_browser.current_window_handle yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data) @@ -172,7 +172,7 @@ def notebook_frontend(authenticated_browser_data): # return inner -@pytest.fixture +@pytest.fixture(scope='function') def prefill_notebook(playwright_browser, notebook_server): browser_raw = playwright_browser playwright_browser = browser_raw.new_context() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index ea0d1023f..711d966dd 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -719,9 +719,9 @@ def wait_for_kernel_ready(self): 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('div.list_item:nth-child(4) > div:nth-child(1) > a:nth-child(3)') + 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: @@ -738,7 +738,6 @@ def wait_for_new_page(): new_pages = self._wait_for_condition(wait_for_new_page) editor_page = new_pages[0] - editor_page.wait_for_selector('.cell') return editor_page # TODO: Refactor/consider removing this From 6947005c31c4fd2db16c951b62cf8fdf8f063208 Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 5 Oct 2022 10:42:20 -0700 Subject: [PATCH 059/131] 3 updated tests --- .../tests/end_to_end/test_dashboard_nav.py | 87 +++++++++++++++++++ nbclassic/tests/end_to_end/test_save.py | 76 ++++++++++++++++ .../tests/end_to_end/test_save_readonly_as.py | 60 +++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_dashboard_nav.py create mode 100644 nbclassic/tests/end_to_end/test_save.py create mode 100644 nbclassic/tests/end_to_end/test_save_readonly_as.py 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..1692e2849 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -0,0 +1,87 @@ +import os +from tkinter.tix import NoteBook +from turtle import home +from .utils import EDITOR_PAGE, TREE_PAGE +from jupyter_server.utils import url_path_join +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.pages[0].url + + tree_url = url_path_join(browser.jupyter_server_info['url'], 'tree') + return True if tree_url in url else False + + +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.") + + browser.pages[0].wait_for_selector('.item_link') + + return [{ + 'link': a.get_attribute('href'), + 'label': a.inner_text(), + 'element': a, + } for a in browser.pages[0].query_selector_all('.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(notebook_frontend): + authenticated_browser = notebook_frontend.get_browser_context() + + home_page = notebook_frontend.get_browser_page(page=TREE_PAGE) + visited_dict = {} + + while True: + home_page.wait_for_selector('.item_link') + + # store the links to directories available in this current URL. URL is the key, the list of directory URLs is the value + items = visited_dict[home_page.url] = only_dir_links(authenticated_browser) + + try: + # Access the first link in the list + item = items[0] + + # Click on the link + item["element"].click() + # Make sure we navigate to that link + assert home_page.url == item['link'] + except IndexError: + break + + # Going back up the tree while we still have unvisited links + while visited_dict: + # Generate a list of directory links from the home URL + current_items = only_dir_links(authenticated_browser) + + # Save each link from the current_items list into this new variable + current_items_links = [item["link"] for item in current_items] + + # If the current URL we are at, is in the visited_dict, remove it + stored_items = visited_dict.pop(home_page.url) + + # Store that visted URL in the stored_items_links list + stored_items_links = [item["link"] for item in stored_items] + + assert stored_items_links == current_items_links + + home_page.go_back() 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..83792ed52 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_save.py @@ -0,0 +1,76 @@ +"""Test saving a notebook with escaped characters +""" + +from tkinter import E +from urllib.parse import quote +from .utils import EDITOR_PAGE + + +# TODO: TEST TIMESOUT WHEN NOT RUN IN DEBUG MODE: DUE TO wait_for_selector('.cell') CALL IN _open_notebook_editor_page + # WHEN IN DEBUG MODE, HAVE TO RELOAD THE PAGE FOR THE .cell SELECTOR TO BE PICKED UP + # determine when to wait for the selector..... + +# 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.editor_page.query_selector('#notebook_name').as_element().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.editor_page.query_selector_all('a.item_link') + for link in all_links: + href = link.as_element().get_attribute('href') + if escaped_name in href: + print("Opening", href) + href = href.split('/a@b/') + notebook_frontend.editor_page.goto(notebook_frontend._browser_data['SERVER_INFO']['url'] + href[1]) + notebook_frontend.editor_page.wait_for_selector('.cell') + 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() + print(f"the cell contents after delete_all_cells are: {notebook_frontend.get_cells_contents()}") + \ No newline at end of file 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..a891cf1fa --- /dev/null +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -0,0 +1,60 @@ +from os import rename +from tkinter import E +from webbrowser import get +from .utils import EDITOR_PAGE, TREE_PAGE +import time + + +# TODO: TEST TIMESOUT WHEN NOT RUN IN DEBUG MODE: DUE TO wait_for_selector('.cell') CALL IN _open_notebook_editor_page + # WHEN IN DEBUG MODE, HAVE TO RELOAD THE PAGE FOR THE .cell SELECTOR TO BE PICKED UP + # determine when to wait for the selector..... +def check_for_rename(nb, selector, page, new_name): + check_count = 0 + nb_name = nb.locate(selector, page) + while nb_name != new_name and check_count <= 5: + nb_name = nb.locate(selector, page) + check_count += 1 + return nb_name + +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): + notebook_frontend.edit_cell(index=0, content='a=10; print(a)') + notebook_frontend.wait_for_kernel_ready() + notebook_frontend.editor_page.wait_for_selector(".input") + + # 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) + notebook_frontend.editor_page.wait_for_selector('.save-message') + + inp = notebook_frontend.editor_page.wait_for_selector('//input[@data-testid="save-as"]') + inp.type('new_notebook.ipynb') + notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE) + + check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="new_notebook.ipynb") + + # notebook_frontend.editor_page.query_selector('#notebook_name') + # notebook_frontend.editor_page.query_selector('#notebook_name') + # notebook_frontend.editor_page.query_selector('#notebook_name') + # notebook_frontend.editor_page.query_selector('#notebook_name') + + # Test that the name changed + assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb" + + # Test that address bar was updated (TODO: get the base url) + print(f"editor_page dir: {dir(notebook_frontend.editor_page)}") + assert "new_notebook.ipynb" in notebook_frontend.editor_page.url From e1d69c0329be254d353f5c89132264940045f91d Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 09:09:19 -0400 Subject: [PATCH 060/131] Refactor test_dualmode_insertcell --- nbclassic/tests/end_to_end/test_dualmode_insertcell.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dualmode_insertcell.py b/nbclassic/tests/end_to_end/test_dualmode_insertcell.py index 2925d882f..05fa33215 100644 --- a/nbclassic/tests/end_to_end/test_dualmode_insertcell.py +++ b/nbclassic/tests/end_to_end/test_dualmode_insertcell.py @@ -14,10 +14,8 @@ def test_insert_cell(prefill_notebook): notebook_frontend.focus_cell(2) notebook_frontend.convert_cell_type(2, "markdown") - notebook_frontend.editor_page.pause() # insert code cell above notebook_frontend.press_active("a") - notebook_frontend.editor_page.pause() 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" From 8d81faa525c5a11cc910647a8139a09b9a5032a1 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 09:18:55 -0400 Subject: [PATCH 061/131] Refactor test_display_image and related utils --- nbclassic/tests/end_to_end/test_display_image.py | 3 ++- nbclassic/tests/end_to_end/utils.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_display_image.py b/nbclassic/tests/end_to_end/test_display_image.py index d6eaec089..93dba1224 100644 --- a/nbclassic/tests/end_to_end/test_display_image.py +++ b/nbclassic/tests/end_to_end/test_display_image.py @@ -2,6 +2,8 @@ The effect of shape metadata is validated, using Image(retina=True) """ + + import re @@ -36,7 +38,6 @@ def validate_img(notebook_frontend, cell_index, image_fmt, retina): # Find the image element that was just displayed img_element = notebook_frontend.wait_for_tag("img", cell_index=cell_index) - # TODO refactor img element access/encapsulate # Check image format src = img_element.get_attribute("src") diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 711d966dd..a2ff5881f 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -436,7 +436,7 @@ def wait_for_tag(self, tag, page=None, cell_index=None): if cell_index is not None: result = self._cells[cell_index].wait_for_selector(tag) - return result + return FrontendElement(result) # TODO remove this def _locate(self, selector, page): From b24505fa72ea3adb8f309c28f6490294c6858fb0 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 09:29:30 -0400 Subject: [PATCH 062/131] Refactor test_clipboard_multiselect --- nbclassic/tests/end_to_end/test_clipboard_multiselect.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_clipboard_multiselect.py b/nbclassic/tests/end_to_end/test_clipboard_multiselect.py index 359dc7850..a71b8e67a 100644 --- a/nbclassic/tests/end_to_end/test_clipboard_multiselect.py +++ b/nbclassic/tests/end_to_end/test_clipboard_multiselect.py @@ -9,7 +9,7 @@ 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 @@ -29,16 +29,15 @@ def test_clipboard_multiselect(prefill_notebook): 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) @@ -46,4 +45,4 @@ def test_clipboard_multiselect(prefill_notebook): 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'] \ No newline at end of file + assert notebook.get_cells_contents() == ['', '5a', '1', '2', '3', '1', '2', '3', '4'] From cd92f207a666ae79efc747e44e089bd433a096a9 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 10:40:52 -0400 Subject: [PATCH 063/131] Refactor test_move_multiselection --- nbclassic/tests/end_to_end/test_move_multiselection.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_move_multiselection.py b/nbclassic/tests/end_to_end/test_move_multiselection.py index aed440c8f..a4266892e 100644 --- a/nbclassic/tests/end_to_end/test_move_multiselection.py +++ b/nbclassic/tests/end_to_end/test_move_multiselection.py @@ -10,7 +10,7 @@ def test_move_multiselection(prefill_notebook): notebook_frontend = prefill_notebook(INITIAL_CELLS) - def assert_oder(pre_message, expected_state): + 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)}" @@ -22,7 +22,7 @@ def assert_oder(pre_message, expected_state): EDITOR_PAGE ) # Should not move up at top - assert_oder('move up at top', ['1', '2', '3', '4', '5', '6']) + 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( @@ -39,14 +39,14 @@ def assert_oder(pre_message, expected_state): ) # 3 times down should move the 3 selected cells to the bottom - assert_oder("move down to bottom", ['4', '5', '6', '1', '2', '3']) + 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_oder("move down to bottom", ['4', '5', '6', '1', '2', '3']) + assert_order("move down to bottom", ['4', '5', '6', '1', '2', '3']) notebook_frontend.evaluate( "Jupyter.notebook.move_selection_up();", @@ -62,4 +62,4 @@ def assert_oder(pre_message, expected_state): ) # Bring them back on top - assert_oder('move up at top', ['1', '2', '3', '4', '5', '6']) + assert_order('move up at top', ['1', '2', '3', '4', '5', '6']) From 17a4c3bffa3eb5eafa19938c56810e27eae3de08 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 08:52:54 -0700 Subject: [PATCH 064/131] refactored test dashboard nav and added utils functions --- .../tests/end_to_end/test_dashboard_nav.py | 83 +++++-------------- nbclassic/tests/end_to_end/utils.py | 23 +++++ 2 files changed, 46 insertions(+), 60 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dashboard_nav.py b/nbclassic/tests/end_to_end/test_dashboard_nav.py index 1692e2849..26b90872f 100644 --- a/nbclassic/tests/end_to_end/test_dashboard_nav.py +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -1,87 +1,50 @@ import os -from tkinter.tix import NoteBook -from turtle import home from .utils import EDITOR_PAGE, TREE_PAGE from jupyter_server.utils import url_path_join 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): +def url_in_tree(nb, url=None): if url is None: - url = browser.pages[0].url + url = nb.get_page_url(page=TREE_PAGE) - tree_url = url_path_join(browser.jupyter_server_info['url'], 'tree') + tree_url = url_path_join(nb.get_server_info(), 'tree') return True if tree_url in url else False - -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) +def get_list_items(nb): + """ + Gets list items from a directory listing page """ - 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.") - browser.pages[0].wait_for_selector('.item_link') + link_items = nb.locate_all('.item_link', page=TREE_PAGE) return [{ 'link': a.get_attribute('href'), - 'label': a.inner_text(), + 'label': a.get_inner_text(), 'element': a, - } for a in browser.pages[0].query_selector_all('.item_link')] + } for a in link_items if a.get_inner_text() != '..'] -def only_dir_links(browser): - """Return only links that point at other directories in the tree""" - items = get_list_items(browser) +def test_navigation(notebook_frontend): - return [i for i in items - if url_in_tree(browser, i['link']) and i['label'] != '..'] + link_elements = get_list_items(notebook_frontend) -def test_items(notebook_frontend): - authenticated_browser = notebook_frontend.get_browser_context() + def check_links(nb, list_of_link_elements): + if len(list_of_link_elements) < 1: + return False - home_page = notebook_frontend.get_browser_page(page=TREE_PAGE) - visited_dict = {} - - while True: - home_page.wait_for_selector('.item_link') - - # store the links to directories available in this current URL. URL is the key, the list of directory URLs is the value - items = visited_dict[home_page.url] = only_dir_links(authenticated_browser) - - try: - # Access the first link in the list - item = items[0] - - # Click on the link + for item in list_of_link_elements: item["element"].click() - # Make sure we navigate to that link - assert home_page.url == item['link'] - except IndexError: - break - - # Going back up the tree while we still have unvisited links - while visited_dict: - # Generate a list of directory links from the home URL - current_items = only_dir_links(authenticated_browser) - # Save each link from the current_items list into this new variable - current_items_links = [item["link"] for item in current_items] + assert url_in_tree(notebook_frontend) == True + assert item["link"] in nb.get_page_url(page=TREE_PAGE) - # If the current URL we are at, is in the visited_dict, remove it - stored_items = visited_dict.pop(home_page.url) + new_links = get_list_items(nb) + if len(new_links) > 0: + check_links(nb, new_links) - # Store that visted URL in the stored_items_links list - stored_items_links = [item["link"] for item in stored_items] + nb.go_back(page=TREE_PAGE) - assert stored_items_links == current_items_links + return - home_page.go_back() + check_links(notebook_frontend, link_elements) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a2ff5881f..0f8cc185b 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -740,6 +740,29 @@ def 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'] + # TODO: Refactor/consider removing this @classmethod def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', existing_file_name=None): From ad400cf9c0d491bc71aa731b0a527f8cd8b3fdbe Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 09:49:36 -0700 Subject: [PATCH 065/131] refactored test_save and navigat_to util function added --- nbclassic/tests/end_to_end/test_save.py | 21 ++++++++------------- nbclassic/tests/end_to_end/utils.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save.py b/nbclassic/tests/end_to_end/test_save.py index 83792ed52..29c8d32af 100644 --- a/nbclassic/tests/end_to_end/test_save.py +++ b/nbclassic/tests/end_to_end/test_save.py @@ -3,12 +3,7 @@ from tkinter import E from urllib.parse import quote -from .utils import EDITOR_PAGE - - -# TODO: TEST TIMESOUT WHEN NOT RUN IN DEBUG MODE: DUE TO wait_for_selector('.cell') CALL IN _open_notebook_editor_page - # WHEN IN DEBUG MODE, HAVE TO RELOAD THE PAGE FOR THE .cell SELECTOR TO BE PICKED UP - # determine when to wait for the selector..... +from .utils import EDITOR_PAGE, TREE_PAGE # TODO: REWORK from polling => async def check_display_name(nb, nbname): @@ -16,7 +11,7 @@ def check_display_name(nb, nbname): count_check = 0 while not display_updated and count_check < 5: - displayed_name = nb.editor_page.query_selector('#notebook_name').as_element().inner_text() + displayed_name = nb.locate('#notebook_name', page=EDITOR_PAGE).get_inner_text() if displayed_name + '.ipynb' == nbname: display_updated = True count_check += 1 @@ -54,14 +49,15 @@ def test_save(notebook_frontend): notebook_frontend.wait_for_selector('.item_link', page=EDITOR_PAGE) hrefs_nonmatch = [] - all_links = notebook_frontend.editor_page.query_selector_all('a.item_link') + all_links = notebook_frontend.locate_all('a.item_link', page=EDITOR_PAGE) + for link in all_links: - href = link.as_element().get_attribute('href') + href = link.get_attribute('href') + if escaped_name in href: - print("Opening", href) href = href.split('/a@b/') - notebook_frontend.editor_page.goto(notebook_frontend._browser_data['SERVER_INFO']['url'] + href[1]) - notebook_frontend.editor_page.wait_for_selector('.cell') + 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: @@ -72,5 +68,4 @@ def test_save(notebook_frontend): notebook_frontend.edit_cell(index=0, content="") notebook_frontend.delete_all_cells() - print(f"the cell contents after delete_all_cells are: {notebook_frontend.get_cells_contents()}") \ No newline at end of file diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 0f8cc185b..9904d2ac1 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -763,6 +763,16 @@ def go_back(self, page): 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 @classmethod def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', existing_file_name=None): From 351af2dfcefbabf004ca2ff631c5f9be9be517e5 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 10:08:16 -0700 Subject: [PATCH 066/131] test_buffering cleanup --- nbclassic/tests/end_to_end/test_buffering.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_buffering.py b/nbclassic/tests/end_to_end/test_buffering.py index bdac97d07..272832df4 100644 --- a/nbclassic/tests/end_to_end/test_buffering.py +++ b/nbclassic/tests/end_to_end/test_buffering.py @@ -31,18 +31,13 @@ def test_buffered_cells_execute_in_order(prefill_notebook): notebook_frontend.evaluate("() => IPython.notebook.kernel.stop_channels();", page=EDITOR_PAGE) # k == 1 notebook_frontend.execute_cell(1) - notebook_frontend._pause() # k == 2 notebook_frontend.execute_cell(2) - notebook_frontend._pause() # k == 6 notebook_frontend.execute_cell(3) - notebook_frontend._pause() # k == 7 notebook_frontend.execute_cell(2) - notebook_frontend._pause() notebook_frontend.execute_cell(4) - notebook_frontend._pause() notebook_frontend.evaluate("() => IPython.notebook.kernel.reconnect();", page=EDITOR_PAGE) notebook_frontend.wait_for_kernel_ready() From 016c7c5418555e6344dd2512de1bb22e66201e61 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 10:23:03 -0700 Subject: [PATCH 067/131] clean display isolation --- nbclassic/tests/end_to_end/test_display_isolation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_display_isolation.py b/nbclassic/tests/end_to_end/test_display_isolation.py index ae02b10ff..4a55185ac 100644 --- a/nbclassic/tests/end_to_end/test_display_isolation.py +++ b/nbclassic/tests/end_to_end/test_display_isolation.py @@ -48,7 +48,6 @@ def isolated_html(notebook): # The non-isolated styling will have affected the output of other cells test_div = notebook.locate('#test', page=EDITOR_PAGE) - notebook.editor_page.pause() assert test_div.get_computed_property('color') == red # The isolated div will be in an iframe, only that element will be blue From cd19fe1a9ed8fada990bdeb13ebe89085572c6cf Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 11:10:50 -0700 Subject: [PATCH 068/131] cleaup shutdown test and refactor test_save_readonly_as, added FrontendElement type method --- .../tests/end_to_end/test_save_readonly_as.py | 19 +++++-------------- nbclassic/tests/end_to_end/test_shutdown.py | 4 ---- nbclassic/tests/end_to_end/utils.py | 3 +++ 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save_readonly_as.py b/nbclassic/tests/end_to_end/test_save_readonly_as.py index a891cf1fa..3729f3c58 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -5,9 +5,6 @@ import time -# TODO: TEST TIMESOUT WHEN NOT RUN IN DEBUG MODE: DUE TO wait_for_selector('.cell') CALL IN _open_notebook_editor_page - # WHEN IN DEBUG MODE, HAVE TO RELOAD THE PAGE FOR THE .cell SELECTOR TO BE PICKED UP - # determine when to wait for the selector..... def check_for_rename(nb, selector, page, new_name): check_count = 0 nb_name = nb.locate(selector, page) @@ -31,7 +28,7 @@ def set_notebook_name(nb, name): def test_save_notebook_as(notebook_frontend): notebook_frontend.edit_cell(index=0, content='a=10; print(a)') notebook_frontend.wait_for_kernel_ready() - notebook_frontend.editor_page.wait_for_selector(".input") + notebook_frontend.wait_for_selector(".input", page=EDITOR_PAGE) # Set a name for comparison later set_notebook_name(notebook_frontend, name="nb1.ipynb") @@ -39,22 +36,16 @@ def test_save_notebook_as(notebook_frontend): # Wait for Save As modal, save save_as(notebook_frontend) - notebook_frontend.editor_page.wait_for_selector('.save-message') + notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE) - inp = notebook_frontend.editor_page.wait_for_selector('//input[@data-testid="save-as"]') + inp = notebook_frontend.wait_for_selector('//input[@data-testid="save-as"]', page=EDITOR_PAGE) inp.type('new_notebook.ipynb') notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE) check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="new_notebook.ipynb") - # notebook_frontend.editor_page.query_selector('#notebook_name') - # notebook_frontend.editor_page.query_selector('#notebook_name') - # notebook_frontend.editor_page.query_selector('#notebook_name') - # notebook_frontend.editor_page.query_selector('#notebook_name') - # Test that the name changed assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb" - # Test that address bar was updated (TODO: get the base url) - print(f"editor_page dir: {dir(notebook_frontend.editor_page)}") - assert "new_notebook.ipynb" in notebook_frontend.editor_page.url + # 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 index 79fd66e99..114907d4c 100644 --- a/nbclassic/tests/end_to_end/test_shutdown.py +++ b/nbclassic/tests/end_to_end/test_shutdown.py @@ -2,15 +2,11 @@ from .utils import EDITOR_PAGE def test_shutdown(prefill_notebook): - # notebook_frontend.edit_cell(content="print(21)") 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) - - # Wait until all shutdown modal elements disappear before trying to execute the cell - # notebook_frontend.editor_page.query_selector_all("//div[contains(@class,'modal')]") notebook_frontend.execute_cell(0) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 9904d2ac1..23dbb9ff7 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -173,6 +173,9 @@ def locate(self, selector): return FrontendElement(result) + def type(self, text): + return self._element.type(text) + def wait_for_state(self, state): if hasattr(self._element, 'wait_for_element_state'): self._element.wait_for_element_state(state) From 66e0925bd8bbe4ff864fd54de659b7457bb0fd0c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 14:10:53 -0400 Subject: [PATCH 069/131] Refactor test_kernel_menu --- nbclassic/tests/end_to_end/test_kernel_menu.py | 9 +++------ nbclassic/tests/end_to_end/utils.py | 8 ++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index 32f5eb07a..1be27f060 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -1,7 +1,7 @@ """Test kernel menu""" -from .utils import TREE_PAGE, EDITOR_PAGE +from .utils import EDITOR_PAGE restart_selectors = [ @@ -41,8 +41,7 @@ def test_menu_items(notebook_frontend): # Confirm shutdown notebook_frontend.wait_for_selector(confirm_selector, EDITOR_PAGE).click() - # TODO refactor _wait_for_condition call - notebook_frontend._wait_for_condition(lambda: not notebook_frontend.is_kernel_running()) + 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) @@ -51,6 +50,4 @@ def test_menu_items(notebook_frontend): kernel_menu.click() notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() - # wait_for_selector(browser, menu_item, visible=True, single=True).click() - # TODO refactor _wait_for_condition call - notebook_frontend._wait_for_condition(lambda: notebook_frontend.is_kernel_running()) + notebook_frontend.wait_for_condition(lambda: notebook_frontend.is_kernel_running()) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a2ff5881f..1894387d0 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -236,7 +236,7 @@ def check_is_kernel_running(): and self.is_notebook_defined() and self.is_kernel_running()) - self._wait_for_condition(check_is_kernel_running) + self.wait_for_condition(check_is_kernel_running) @property def body(self): @@ -391,7 +391,7 @@ def frame_wait(): frames = [f for f in specified_page.frames] return len(frames) >= count - self._wait_for_condition(frame_wait) + self.wait_for_condition(frame_wait) def locate_in_frame(self, selector, page, frame_name=None, frame_index=None): if frame_name is None and frame_index is None: @@ -573,7 +573,7 @@ def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): return cell_data - def _wait_for_condition(self, check_func, timeout=30, period=.1): + 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 @@ -736,7 +736,7 @@ def _open_notebook_editor_page(self, existing_file_name=None): def wait_for_new_page(): return [pg for pg in self._browser_data[BROWSER].pages if 'tree' not in pg.url] - new_pages = self._wait_for_condition(wait_for_new_page) + new_pages = self.wait_for_condition(wait_for_new_page) editor_page = new_pages[0] return editor_page From cca89fbcd740d56a0df5872ea8a585f6d9fdfef9 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 6 Oct 2022 11:43:17 -0700 Subject: [PATCH 070/131] refactored test_save_as_notebook recovered --- .../tests/end_to_end/test_save_as_notebook.py | 44 +++++++++++++++++++ nbclassic/tests/end_to_end/utils.py | 3 ++ 2 files changed, 47 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_save_as_notebook.py 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..e927c5012 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_save_as_notebook.py @@ -0,0 +1,44 @@ +from os import rename +from webbrowser import get +from .utils import EDITOR_PAGE, TREE_PAGE +import time + + +def check_for_rename(nb, selector, page, new_name): + check_count = 0 + nb_name = nb.locate(selector, page) + while nb_name != new_name and check_count <= 15: + nb_name = nb.locate(selector, page) + check_count += 1 + return nb_name + +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") + + check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="nb1.ipynb") + assert get_notebook_name(notebook_frontend) == "nb1.ipynb" + + # Wait for Save As modal, save + save_as(notebook_frontend) + save_message = notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE) + + inp = notebook_frontend.wait_for_selector('//input[@data-testid="save-as"]', page=EDITOR_PAGE) + inp.type('new_notebook.ipynb') + notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE) + + check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="new_notebook.ipynb") + + 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/utils.py b/nbclassic/tests/end_to_end/utils.py index 5667bc01d..71edd6c60 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -176,6 +176,9 @@ def locate(self, selector): def type(self, text): return self._element.type(text) + def press(self, key): + return self._element.press(key) + def wait_for_state(self, state): if hasattr(self._element, 'wait_for_element_state'): self._element.wait_for_element_state(state) From 6f60cac6e5cb2a8132a1c2a8aaf129a6642339ba Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 6 Oct 2022 14:52:41 -0400 Subject: [PATCH 071/131] WIP test_notifications --- .../tests/end_to_end/test_notifications.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 nbclassic/tests/end_to_end/test_notifications.py 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..156be5481 --- /dev/null +++ b/nbclassic/tests/end_to_end/test_notifications.py @@ -0,0 +1,121 @@ +"""Test the notification area and widgets""" + + +import pytest + +from .utils import EDITOR_PAGE +# from .utils import wait_for_selector, wait_for_script_to_return_true + + +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 correct" + assert widget_message(notebook_frontend, "test") == f"test {level}", f"{level}: message is correct" + + # 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 correct" + notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE)#, obscures=True) + assert widget_message(notebook_frontend, "test") == "", "timeout: message was 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.wait_for_selector("#notification_test", page=EDITOR_PAGE) + + assert widget_message(notebook_frontend, "test") == "test click", "callback: message is correct" + + notebook_frontend.browser.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", page=EDITOR_PAGE)#, obscures=True) + + assert widget_message(notebook_frontend, "test") == "", "callback: message was cleared" From 5822205bebf472eb79a7a3b927d9ba1bb25be341 Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 7 Oct 2022 07:49:48 -0700 Subject: [PATCH 072/131] working test_notifications with update to wait for selector function --- .../tests/end_to_end/test_notifications.py | 21 +++++++++++-------- nbclassic/tests/end_to_end/utils.py | 5 ++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_notifications.py b/nbclassic/tests/end_to_end/test_notifications.py index 156be5481..b446af239 100644 --- a/nbclassic/tests/end_to_end/test_notifications.py +++ b/nbclassic/tests/end_to_end/test_notifications.py @@ -77,8 +77,9 @@ def test_notification(notebook_frontend): ) notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE) - assert widget_has_class(notebook_frontend, "test", level), f"{level}: class is correct" - assert widget_message(notebook_frontend, "test") == f"test {level}", f"{level}: message is correct" + assert widget_has_class(notebook_frontend, "test", level), f"{level}: class is incorrect" + #notebook_frontend.editor_page.pause() + assert widget_message(notebook_frontend, "test") == f"test {level}", f"{level}: message is incorrect" # test message timeout notebook_frontend.evaluate( @@ -90,9 +91,10 @@ def test_notification(notebook_frontend): ) notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE) - assert widget_message(notebook_frontend, "test") == "test timeout", "timeout: message is correct" - notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE)#, obscures=True) - assert widget_message(notebook_frontend, "test") == "", "timeout: message was cleared" + 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( @@ -106,16 +108,17 @@ def test_notification(notebook_frontend): """, page=EDITOR_PAGE ) - notebook_frontend.wait_for_selector("#notification_test", 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.browser.locate("notification_test", page=EDITOR_PAGE).click() + 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", page=EDITOR_PAGE)#, obscures=True) + notebook_frontend.locate("#notification_test", page=EDITOR_PAGE)#, obscures=True) - assert widget_message(notebook_frontend, "test") == "", "callback: message was cleared" + 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/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 5667bc01d..d943d2fb6 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -319,13 +319,16 @@ def try_click_selector(self, selector, page): elem.click() - def wait_for_selector(self, selector, page): + def wait_for_selector(self, selector, page, state=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!') + 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): From 77a72f91168af1969f9b808edcf05e68f88bd74f Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 7 Oct 2022 08:10:27 -0700 Subject: [PATCH 073/131] cleanup test notifications --- nbclassic/tests/end_to_end/test_notifications.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_notifications.py b/nbclassic/tests/end_to_end/test_notifications.py index b446af239..bf8d9666d 100644 --- a/nbclassic/tests/end_to_end/test_notifications.py +++ b/nbclassic/tests/end_to_end/test_notifications.py @@ -2,9 +2,7 @@ import pytest - from .utils import EDITOR_PAGE -# from .utils import wait_for_selector, wait_for_script_to_return_true def get_widget(notebook, name): @@ -78,7 +76,6 @@ def test_notification(notebook_frontend): notebook_frontend.wait_for_selector("#notification_test", page=EDITOR_PAGE) assert widget_has_class(notebook_frontend, "test", level), f"{level}: class is incorrect" - #notebook_frontend.editor_page.pause() assert widget_message(notebook_frontend, "test") == f"test {level}", f"{level}: message is incorrect" # test message timeout @@ -90,7 +87,6 @@ def test_notification(notebook_frontend): 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') @@ -109,7 +105,6 @@ def test_notification(notebook_frontend): 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() @@ -118,7 +113,6 @@ def test_notification(notebook_frontend): '() => { return IPython.notification_area.widget("test")._clicked; }', page=EDITOR_PAGE ) ) - notebook_frontend.locate("#notification_test", page=EDITOR_PAGE)#, obscures=True) notebook_frontend.wait_for_selector("#notification_test", EDITOR_PAGE, state='hidden') assert widget_message(notebook_frontend, "test") == "", "callback: message was not cleared" From e3baacc8de6610563edca25fefaf35faaeb03ce0 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 7 Oct 2022 15:11:31 -0400 Subject: [PATCH 074/131] WIP added some utils docs. --- nbclassic/tests/end_to_end/utils.py | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 812b073a6..a653b96ea 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -121,9 +121,22 @@ class FrontendError(Exception): 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 + + Blah blah blah + + """ def __init__(self, item): - # item should be a JSHandle, locator or ElementHandle + # "item" should be a JSHandle, locator or ElementHandle self._raw = item self._element = item self._bool = True # Was the item created successfully? @@ -142,6 +155,7 @@ def __init__(self, item): def __bool__(self): """Returns True if construction succeeded""" + # We can debug on failures by deferring bad inits and testing for them here return self._bool def click(self): @@ -189,6 +203,26 @@ def wait_for_state(self, state): 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 + + Things to talk about + + - class design (editor_page, tree_page) + - Designed to support a full notebook application, + consisting of a single tree page and editor page + - Note, not designed around multi-notebook/editor page + usage scenarios... + - evaluate calls + - Possible future improvements, current limitations, etc + - Known bad things, blah blah + """ # Some constants for users of the class TREE_PAGE = TREE_PAGE @@ -203,6 +237,11 @@ class NotebookFrontend: } def __init__(self, browser_data, existing_file_name=None): + """Start the Notebook app via the web UI or from a file. + + :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 From 7f2c3a3bc996a0344467afec891237498ff323bd Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 10:39:29 -0400 Subject: [PATCH 075/131] The .cells prop now uses FrontendElement, removed/refactored cell info dicts. --- nbclassic/tests/end_to_end/test_buffering.py | 4 +-- .../tests/end_to_end/test_execute_code.py | 10 +++---- nbclassic/tests/end_to_end/test_interrupt.py | 5 ++-- nbclassic/tests/end_to_end/utils.py | 29 +++++++------------ 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_buffering.py b/nbclassic/tests/end_to_end/test_buffering.py index 272832df4..59085fddc 100644 --- a/nbclassic/tests/end_to_end/test_buffering.py +++ b/nbclassic/tests/end_to_end/test_buffering.py @@ -17,7 +17,7 @@ def test_kernels_buffer_without_conn(prefill_notebook): notebook_frontend.wait_for_kernel_ready() outputs = notebook_frontend.wait_for_cell_output(0) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '3' + assert outputs.get_inner_text().strip() == '3' def test_buffered_cells_execute_in_order(prefill_notebook): @@ -42,4 +42,4 @@ def test_buffered_cells_execute_in_order(prefill_notebook): notebook_frontend.wait_for_kernel_ready() outputs = notebook_frontend.wait_for_cell_output(4) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '7' + assert outputs.get_inner_text().strip() == '7' diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index d6cc20319..a8605430a 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -9,14 +9,14 @@ def test_execute_code(notebook_frontend): 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) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '10' + 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) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '11' + assert outputs.get_inner_text().strip() == '11' notebook_frontend.delete_cell(1) # Shift+Enter adds a cell # Execute cell with Ctrl-Enter (or equivalent) @@ -28,14 +28,14 @@ def test_execute_code(notebook_frontend): modifiers=[notebook_frontend.get_platform_modifier_key()] ) outputs = notebook_frontend.wait_for_cell_output(0) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '12' + 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) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '13' + 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 @@ -63,4 +63,4 @@ def test_execute_code(notebook_frontend): cell1.execute(); """, page=EDITOR_PAGE) outputs = notebook_frontend.wait_for_cell_output(1) - assert outputs[notebook_frontend.CELL_TEXT].strip() == '14' + assert outputs.get_inner_text().strip() == '14' diff --git a/nbclassic/tests/end_to_end/test_interrupt.py b/nbclassic/tests/end_to_end/test_interrupt.py index 7c3f635d3..8a16a2dcd 100644 --- a/nbclassic/tests/end_to_end/test_interrupt.py +++ b/nbclassic/tests/end_to_end/test_interrupt.py @@ -1,8 +1,7 @@ """Test kernel interrupt""" -# from .utils import wait_for_selector -from .utils import TREE_PAGE, EDITOR_PAGE +from .utils import EDITOR_PAGE def interrupt_from_menu(notebook_frontend): @@ -36,4 +35,4 @@ def test_interrupt(notebook_frontend): # Wait for an output to appear output = notebook_frontend.wait_for_cell_output(0) - assert 'KeyboardInterrupt' in output[notebook_frontend.CELL_TEXT] + assert 'KeyboardInterrupt' in output.get_inner_text() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 812b073a6..f5667b1f4 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -122,11 +122,12 @@ class FrontendError(Exception): class FrontendElement: - def __init__(self, item): + def __init__(self, item, user_data=None): # item should be a JSHandle, locator or ElementHandle 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: @@ -187,6 +188,9 @@ def wait_for_state(self, state): else: raise Exception('Unable to wait for state!') + def get_user_data(self): + return self._user_data + class NotebookFrontend: @@ -195,13 +199,6 @@ class NotebookFrontend: EDITOR_PAGE = EDITOR_PAGE CELL_OUTPUT_SELECTOR = CELL_OUTPUT_SELECTOR - CELL_INDEX = 'INDEX' - CELL_TEXT = 'TEXT' - _CELL_DATA_FORMAT = { - CELL_INDEX: None, # int - CELL_TEXT: None, # str - } - def __init__(self, browser_data, existing_file_name=None): # Keep a reference to source data self._browser_data = browser_data @@ -258,16 +255,12 @@ def _cells(self): @property def cells(self): """Gets all cells once they are visible.""" - # self.cells is now a list of dicts containing info per-cell - # (self._cells returns cell objects, should not be used externally) - - # This mirrors the self._CELL_DATA_FORMAT - cell_dicts = [ - {self.CELL_INDEX: index, self.CELL_TEXT: cell.inner_text()} + cells = [ + FrontendElement(cell, user_data={'index': index}) for index, cell in enumerate(self._cells) ] - return cell_dicts + return cells @property def current_index(self): @@ -576,11 +569,9 @@ def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): if cell is None: return None - cell_data = dict(self._CELL_DATA_FORMAT) - cell_data[self.CELL_INDEX] = index - cell_data[self.CELL_TEXT] = cell.inner_text() + element = FrontendElement(cell, user_data={'index': index}) - return cell_data + 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""" From a8d62dce09c8130226bc8466791f74ac8d0b3364 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 11:05:38 -0400 Subject: [PATCH 076/131] Cleanup/refactor to use FrontendElement. --- nbclassic/tests/end_to_end/test_markdown.py | 6 +++--- nbclassic/tests/end_to_end/utils.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_markdown.py b/nbclassic/tests/end_to_end/test_markdown.py index 4af11632e..7b3da0ef3 100644 --- a/nbclassic/tests/end_to_end/test_markdown.py +++ b/nbclassic/tests/end_to_end/test_markdown.py @@ -9,10 +9,10 @@ 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.query_selector(".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.inner_html().strip() + return [x.get_inner_html().strip() for x in rendered_cells if x is not None] diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index f5667b1f4..af777d61c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -151,6 +151,9 @@ def click(self): 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) From a9ca493908098bf3787cdd01b7028178468b3c62 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 11:07:51 -0400 Subject: [PATCH 077/131] Minor import cleanup --- nbclassic/tests/end_to_end/test_markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_markdown.py b/nbclassic/tests/end_to_end/test_markdown.py index 7b3da0ef3..fa501ad6b 100644 --- a/nbclassic/tests/end_to_end/test_markdown.py +++ b/nbclassic/tests/end_to_end/test_markdown.py @@ -3,7 +3,7 @@ from nbformat.v4 import new_markdown_cell -from .utils import TREE_PAGE, EDITOR_PAGE +from .utils import EDITOR_PAGE def get_rendered_contents(nb): From edb39e8f58e13b5f80e3bd0863543d0fa26e60f2 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 11:12:15 -0400 Subject: [PATCH 078/131] Cleanup/refactor to use FrontendElement. --- nbclassic/tests/end_to_end/test_prompt_numbers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_prompt_numbers.py b/nbclassic/tests/end_to_end/test_prompt_numbers.py index a9a277a04..fad9dd654 100644 --- a/nbclassic/tests/end_to_end/test_prompt_numbers.py +++ b/nbclassic/tests/end_to_end/test_prompt_numbers.py @@ -1,6 +1,7 @@ """Test multiselect toggle -TODO: This changes the In []: label preceding the cell, what's the purpose of this? +TODO: This changes the In []: label preceding the cell, + what's the purpose of this? Update the docstring """ @@ -9,9 +10,9 @@ def test_prompt_numbers(prefill_notebook): def get_prompt(): return ( - notebook_frontend._cells[0].query_selector('.input') - .query_selector('.input_prompt') - .inner_html().strip() + notebook_frontend.cells[0].locate('.input') + .locate('.input_prompt') + .get_inner_html().strip() ) def set_prompt(value): From e5f6ff41b7345624bfd89ee903fca0a1d72b074e Mon Sep 17 00:00:00 2001 From: RRosio Date: Tue, 11 Oct 2022 08:19:03 -0700 Subject: [PATCH 079/131] adding playwright tests workflow yaml --- .github/workflows/playwright.yml | 44 ++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..f26b13748 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,44 @@ +name: Playwright 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'] + 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 pytest-playwright + playwright install + pip install .[test] + + - name: Run Tests + run: | + pytest -sv nbclassic/tests/end_to_end diff --git a/setup.py b/setup.py index 9ac2a2c7b..227d6c872 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,7 @@ ], extras_require = { 'test': ['pytest', 'coverage', 'requests', 'testpath', - 'nbval', 'selenium', 'pytest', 'pytest-cov', 'pytest_tornasync'], + 'nbval', 'selenium', 'pytest-playwright', 'pytest-cov', 'pytest_tornasync'], 'docs': ['sphinx', 'nbsphinx', 'sphinxcontrib_github_alt', 'sphinx_rtd_theme', 'myst-parser'], 'test:sys_platform != "win32"': ['requests-unixsocket'], From 158f767de235ae9ec23a4aaaf7bf20736afa9ea6 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 11:37:07 -0400 Subject: [PATCH 080/131] Removed legacy/unused code. --- nbclassic/tests/end_to_end/conftest.py | 61 -------- nbclassic/tests/end_to_end/utils.py | 208 +------------------------ 2 files changed, 1 insertion(+), 268 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 852949970..d3a41d31d 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -5,13 +5,11 @@ from os.path import join as pjoin from subprocess import Popen from tempfile import mkstemp -from types import SimpleNamespace from urllib.parse import urljoin import pytest import requests from testpath.tempdir import TemporaryDirectory -# from selenium.webdriver import Firefox, Remote, Chrome import nbformat from nbformat.v4 import new_notebook, new_code_cell @@ -72,33 +70,6 @@ def notebook_server(): 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='function') def playwright_browser(playwright): # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix this @@ -115,20 +86,6 @@ def playwright_browser(playwright): browser.close() -# @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(scope='function') def authenticated_browser_data(playwright_browser, notebook_server): browser_raw = playwright_browser @@ -154,24 +111,6 @@ def notebook_frontend(authenticated_browser_data): # 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 - - @pytest.fixture(scope='function') def prefill_notebook(playwright_browser, notebook_server): browser_raw = playwright_browser diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index cb929b397..5a9ace4fc 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -1,18 +1,9 @@ import datetime import os import time -from contextlib import contextmanager -from os.path import join as pjoin from playwright.sync_api import ElementHandle, JSHandle -# 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 - # Key constants for browser_data BROWSER = 'BROWSER' @@ -24,86 +15,6 @@ CELL_OUTPUT_SELECTOR = '.output_subarea' -# 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 TimeoutError(Exception): def get_result(self): @@ -254,26 +165,6 @@ def __init__(self, browser_data, existing_file_name=None): self.disable_autosave_and_onbeforeunload() # TODO fix/refactor self.current_cell = None # Defined/used below # TODO refactor/remove - # 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""" def check_is_kernel_running(): @@ -714,16 +605,6 @@ def append(self, *values, cell_type="code"): 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_jupyter_defined(self): """Checks that the Jupyter object is defined on the frontend""" return self.evaluate( @@ -815,101 +696,14 @@ def navigate_to(self, page, partial_url): specified_page.goto(self._browser_data[SERVER_INFO]['url'] + partial_url) - # TODO: Refactor/consider removing this + # TODO: Refactor/consider removing this (legacy cruft) @classmethod def new_notebook_frontend(cls, browser_data, kernel_name='kernel-python3', existing_file_name=None): - browser = browser_data[BROWSER] - tree_page = browser_data[TREE_PAGE] - server_info = browser_data[SERVER_INFO] - - # with new_window(page): - # select_kernel(tree_page, kernel_name=kernel_name) # TODO this is terrible, remove it - # tree_page.pause() instance = cls(browser_data, existing_file_name) return instance -# # TODO: refactor/remove this -# def select_kernel(page, 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 = page.locator('#new-dropdown-button') -# new_button.click() -# kernel_selector = f'#{kernel_name} a' -# # kernel = wait_for_selector(page, kernel_selector, single=True) -# kernel = page.locator(kernel_selector) -# 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(page, key): -# """Send key combination Ctrl+(key) or Command+(key) for MacOS""" -# if os.uname()[0] == "Darwin": -# page.keyboard.press("Meta+{}".format(key)) -# else: -# page.keyboard.press("Control+{}".format(key)) - - -# 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. From 45a8a54039d1da6ba21680b0d62c9562a7bed3ca Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 12:30:16 -0400 Subject: [PATCH 081/131] Added docs. --- nbclassic/tests/end_to_end/utils.py | 81 ++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 5a9ace4fc..963ca390c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -42,12 +42,18 @@ class FrontendElement: details of the web library to individual tests) - Unifies disparate library syntax for common functionalities - Blah blah blah - + 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): - # "item" should be a JSHandle, locator or ElementHandle + """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? @@ -67,7 +73,7 @@ def __init__(self, item, user_data=None): def __bool__(self): """Returns True if construction succeeded""" - # We can debug on failures by deferring bad inits and testing for them here + # (Quick/dirty )We can debug on failures by deferring bad inits and testing for them here return self._bool def click(self): @@ -91,6 +97,7 @@ def evaluate(self, text): return self._element.evaluate(text) def locate(self, selector): + """Locate child elements with the given selector""" element = self._element if hasattr(element, 'locator'): @@ -103,12 +110,15 @@ def locate(self, selector): return FrontendElement(result) 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 wait_for_state(self, state): + """Used to check for hidden, etc.""" if hasattr(self._element, 'wait_for_element_state'): self._element.wait_for_element_state(state) elif hasattr(self._element, 'wait_for'): @@ -117,6 +127,7 @@ def wait_for_state(self, state): raise Exception('Unable to wait for state!') def get_user_data(self): + """Currently this is an unmanaged user data area, use it as you please""" return self._user_data @@ -130,7 +141,20 @@ class NotebookFrontend: on application features rather than implementation details for any given testing task - Things to talk about + NotebookFrontend holds a tree_page (Jupyter file browser), and + an 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. FrontendElement holds (private) handles to the underlying + browser/context. - class design (editor_page, tree_page) - Designed to support a full notebook application, @@ -150,6 +174,11 @@ class NotebookFrontend: 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 """ @@ -180,14 +209,12 @@ def body(self): @property def _cells(self): - """Gets all cells once they are visible. - - """ + """Return a list of the current Notebook cells.""" return self.editor_page.query_selector_all(".cell") @property def cells(self): - """Gets all cells once they are visible.""" + """User facing cell list, gives a list of FrontendElement's""" cells = [ FrontendElement(cell, user_data={'index': index}) for index, cell in enumerate(self._cells) @@ -203,6 +230,12 @@ 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: @@ -218,6 +251,7 @@ def press(self, keycode, page, modifiers=None): 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: @@ -227,6 +261,7 @@ def type(self, text, page): specified_page.keyboard.type(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) @@ -235,9 +270,11 @@ def press_active(self, keycode, modifiers=None): 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: @@ -249,6 +286,7 @@ def try_click_selector(self, selector, page): 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: @@ -257,7 +295,7 @@ def wait_for_selector(self, selector, page, state=None): 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): @@ -287,6 +325,7 @@ 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: @@ -297,6 +336,7 @@ def locate(self, selector, page): 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: @@ -310,6 +350,7 @@ def locate_all(self, selector, page): return element_list 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: @@ -329,6 +370,7 @@ def frame_wait(): 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: @@ -353,6 +395,7 @@ def locate_in_frame(self, selector, page, frame_name=None, frame_index=None): 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: @@ -387,13 +430,14 @@ def _locate(self, selector, page): return specified_page.locator(selector) def clear_all_output(self): - """Clear cell outputs""" + """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) @@ -413,6 +457,7 @@ def populate(self, 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=" @@ -437,6 +482,7 @@ def to_command_mode(self): "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() @@ -449,6 +495,7 @@ def select_cell_range(self, initial_index=0, final_index=0): 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) @@ -481,22 +528,19 @@ def _wait_for_stale_cell(self, cell): 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)) - cell.wait_for_element_state('hidden') - # def wait_for_element_availability(self, element): - # _wait_for(self.browser, By.CLASS_NAME, element, visible=True) - 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: @@ -520,6 +564,7 @@ def wait_for_condition(self, check_func, timeout=30, period=.1): raise TimeoutError() def wait_for_cell_output(self, index=0, timeout=3): + """Waits for the cell to finish executing and return the cell output""" if not self._cells: raise Exception('Error, no cells exist!') @@ -579,9 +624,7 @@ def add_cell(self, index=-1, cell_type="code", content=""): new_index = index + 1 if index >= 0 else index if content: self.edit_cell(index=index, content=content) - # TODO fix this if cell_type != 'code': - # raise NotImplementedError('Error, non code cell_type is a TODO!') self.convert_cell_type(index=new_index, cell_type=cell_type) def add_and_execute_cell(self, index=-1, cell_type="code", content=""): From 343bc994bc48680142d34f3039313d7579a78afa Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 12:39:05 -0400 Subject: [PATCH 082/131] Docs and related cleanup. --- nbclassic/tests/end_to_end/conftest.py | 3 +++ nbclassic/tests/end_to_end/utils.py | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index d3a41d31d..169abac10 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -1,3 +1,6 @@ +"""Fixtures for pytest/playwright end_to_end tests.""" + + import os import json import sys diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 963ca390c..a1baa9dc6 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -1,3 +1,14 @@ +"""Utility module for end to end testing. + +The primary utilities are: + * NotebookFrontend + * FrontendElement + +This module was converted and refactored from the older +selenium test suite. +""" + + import datetime import os import time @@ -797,20 +808,20 @@ def is_focused_on(index): # 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 + 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 + 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 + assert mode == keyboard_mode # keyboard mode is correct if mode == 'command': - assert is_focused_on(None) #no focused cells + assert is_focused_on(None) # no focused cells - assert is_only_cell_edit(None) #no cells in edit mode + 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_focused_on(index) # The specified cell is focused - assert is_only_cell_edit(index) #The specified cell is the only one in edit mode + assert is_only_cell_edit(index) # The specified cell is the only one in edit mode From cc9d8dba3c24beeaebefcbb2348e7022cb036720 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 12:41:43 -0400 Subject: [PATCH 083/131] Added minor docs updates to utils. --- nbclassic/tests/end_to_end/utils.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index a1baa9dc6..d177c5bed 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -1,4 +1,4 @@ -"""Utility module for end to end testing. +"""Utility module for end_to_end testing. The primary utilities are: * NotebookFrontend @@ -153,9 +153,9 @@ class NotebookFrontend: details for any given testing task NotebookFrontend holds a tree_page (Jupyter file browser), and - an 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. + 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. @@ -167,13 +167,8 @@ class NotebookFrontend: writers. FrontendElement holds (private) handles to the underlying browser/context. - - class design (editor_page, tree_page) - - Designed to support a full notebook application, - consisting of a single tree page and editor page - - Note, not designed around multi-notebook/editor page - usage scenarios... - - evaluate calls - - Possible future improvements, current limitations, etc + TODO: + Possible future improvements, current limitations, etc - Known bad things, blah blah """ From ae646d96a071bbda9be0467ebf45a176fa7c6ede Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 12:46:45 -0400 Subject: [PATCH 084/131] Added more docs. --- nbclassic/tests/end_to_end/conftest.py | 1 + nbclassic/tests/end_to_end/utils.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 169abac10..91edd3c13 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -97,6 +97,7 @@ def authenticated_browser_data(playwright_browser, notebook_server): tree_page = playwright_browser.new_page() tree_page.goto("{url}?token={token}".format(**notebook_server)) + # TODO: fix this mess, BROWSER_RAW naming etc. auth_browser_data = { BROWSER: playwright_browser, TREE_PAGE: tree_page, diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index d177c5bed..dd83f64e2 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -4,6 +4,12 @@ * 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. """ From abf8bc0de5aec18a36adeb68b6e024cffdae4470 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 14:23:50 -0400 Subject: [PATCH 085/131] Wait for start refactor WIP. --- nbclassic/tests/end_to_end/utils.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index dd83f64e2..dc018f14c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -209,9 +209,14 @@ def __init__(self, browser_data, existing_file_name=None): def _wait_for_start(self): """Wait until the notebook interface is loaded and the kernel started""" def check_is_kernel_running(): - return (self.is_jupyter_defined() - and self.is_notebook_defined() - and self.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) @@ -814,7 +819,7 @@ def is_focused_on(index): if mode != 'command' and mode != 'edit': raise Exception('An unknown mode was send: mode = "%s"'%mode) # An unknown mode is send - #validate mode + # validate mode assert mode == keyboard_mode # keyboard mode is correct if mode == 'command': From 3a6f99c7f01293c667fe1afd808706fceaf607e8 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 14:26:51 -0400 Subject: [PATCH 086/131] Disabled/moved selenium workflow file --- {.github/workflows => nbclassic/tests/selenium}/selenium.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github/workflows => nbclassic/tests/selenium}/selenium.yml (100%) diff --git a/.github/workflows/selenium.yml b/nbclassic/tests/selenium/selenium.yml similarity index 100% rename from .github/workflows/selenium.yml rename to nbclassic/tests/selenium/selenium.yml From 43577c8909cc66b7c98d4cde6adc11a2a0e69121 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 11 Oct 2022 16:27:41 -0400 Subject: [PATCH 087/131] Made Page objects private. --- .../end_to_end/test_display_isolation.py | 4 +- nbclassic/tests/end_to_end/utils.py | 84 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_display_isolation.py b/nbclassic/tests/end_to_end/test_display_isolation.py index 4a55185ac..59ea9203a 100644 --- a/nbclassic/tests/end_to_end/test_display_isolation.py +++ b/nbclassic/tests/end_to_end/test_display_isolation.py @@ -5,7 +5,7 @@ """ -from .utils import EDITOR_PAGE, TREE_PAGE +from .utils import EDITOR_PAGE def test_display_isolation(notebook_frontend): @@ -17,7 +17,7 @@ def test_display_isolation(notebook_frontend): isolated_svg(notebook_frontend) finally: # Ensure we switch from iframe back to default content even if test fails - notebook_frontend.editor_page.main_frame # TODO: + notebook_frontend._editor_page.main_frame # TODO: def isolated_html(notebook): diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index dd83f64e2..426dc1f50 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -198,8 +198,8 @@ def __init__(self, browser_data, existing_file_name=None): 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) + 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() @@ -217,12 +217,12 @@ def check_is_kernel_running(): @property def body(self): - return self.editor_page.locator("body") + 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") + return self._editor_page.query_selector_all(".cell") @property def cells(self): @@ -249,9 +249,9 @@ def press(self, keycode, page, modifiers=None): :param modifiers: A list of modifier keycode strings to press """ if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') @@ -265,9 +265,9 @@ def press(self, keycode, page, modifiers=None): 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') specified_page.keyboard.type(text) @@ -288,9 +288,9 @@ def type_active(self, 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') elem = specified_page.locator(selector) @@ -300,9 +300,9 @@ def try_click_selector(self, selector, page): 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') if state is not None: @@ -325,23 +325,23 @@ def evaluate(self, text, page): :return: The result of the evaluated JS """ if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.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() + 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to locate from!') @@ -350,9 +350,9 @@ def locate(self, selector, page): 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to locate from!') @@ -364,9 +364,9 @@ def locate_all(self, selector, page): 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to wait for frame from!') @@ -389,9 +389,9 @@ def locate_in_frame(self, selector, page, frame_name=None, frame_index=None): raise Exception('Error, provide only one either frame name or frame index!') if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to locate in frame from!') @@ -416,9 +416,9 @@ def wait_for_tag(self, tag, page=None, cell_index=None): result = None if page is not None: if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') @@ -433,9 +433,9 @@ 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 + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') @@ -470,7 +470,7 @@ def populate(self, cell_texts): def click_toolbar_execute_btn(self): """Mimics a user pressing the execute button in the UI""" - execute_button = self.editor_page.locator( + execute_button = self._editor_page.locator( "button[" "data-jupyter-action=" "'jupyter-notebook:run-cell-and-select-next'" @@ -511,11 +511,11 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): 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() - self.editor_page.locator('#findreplace_find_inp').type(find_txt) - self.editor_page.locator('#findreplace_replace_inp').type(replace_txt) - self.editor_page.locator('#findreplace_replaceall_btn').click() + self._editor_page.locator('#find-and-replace') + self._editor_page.locator('#findreplace_allcells_btn').click() + self._editor_page.locator('#findreplace_find_inp').type(find_txt) + self._editor_page.locator('#findreplace_replace_inp').type(replace_txt) + 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 @@ -693,15 +693,15 @@ def is_kernel_running(self): ) def wait_for_kernel_ready(self): - self.tree_page.locator(".kernel_idle_icon") + self._tree_page.locator(".kernel_idle_icon") def _open_notebook_editor_page(self, existing_file_name=None): - tree_page = self.tree_page + 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 + 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') @@ -720,9 +720,9 @@ def wait_for_new_page(): def get_page_url(self, page): if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') @@ -730,9 +730,9 @@ def get_page_url(self, page): def go_back(self, page): if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') @@ -743,9 +743,9 @@ def get_server_info(self): def navigate_to(self, page, partial_url): if page == TREE_PAGE: - specified_page = self.tree_page + specified_page = self._tree_page elif page == EDITOR_PAGE: - specified_page = self.editor_page + specified_page = self._editor_page else: raise Exception('Error, provide a valid page to evaluate from!') From c8506823f829aa63a7fcc8eec063a271846c4aeb Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 12 Oct 2022 10:06:13 -0400 Subject: [PATCH 088/131] Refactor fixture/browser object naming. --- nbclassic/tests/end_to_end/conftest.py | 32 +++++++++++--------------- nbclassic/tests/end_to_end/utils.py | 6 ++--- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 91edd3c13..18e00a641 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -16,7 +16,7 @@ import nbformat from nbformat.v4 import new_notebook, new_code_cell -from .utils import NotebookFrontend, BROWSER, BROWSER_RAW, TREE_PAGE, SERVER_INFO +from .utils import NotebookFrontend, BROWSER_CONTEXT, BROWSER_OBJ, TREE_PAGE, SERVER_INFO def _wait_for_server(proc, info_file_path): @@ -75,13 +75,10 @@ def notebook_server(): @pytest.fixture(scope='function') def playwright_browser(playwright): - # if os.environ.get('SAUCE_USERNAME'): # TODO: Fix this - # driver = make_sauce_driver() if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': browser = playwright.chromium.launch() else: browser = playwright.firefox.launch() - # browser_context = browser.new_context() yield browser @@ -91,18 +88,17 @@ def playwright_browser(playwright): @pytest.fixture(scope='function') def authenticated_browser_data(playwright_browser, notebook_server): - browser_raw = playwright_browser - playwright_browser = browser_raw.new_context() - playwright_browser.jupyter_server_info = notebook_server - tree_page = playwright_browser.new_page() + 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)) - # TODO: fix this mess, BROWSER_RAW naming etc. auth_browser_data = { - BROWSER: playwright_browser, + BROWSER_CONTEXT: browser_context, TREE_PAGE: tree_page, SERVER_INFO: notebook_server, - BROWSER_RAW: browser_raw, + BROWSER_OBJ: browser_obj, } return auth_browser_data @@ -110,15 +106,13 @@ def authenticated_browser_data(playwright_browser, notebook_server): @pytest.fixture(scope='function') def notebook_frontend(authenticated_browser_data): - # tree_wh = authenticated_browser.current_window_handle yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data) - # authenticated_browser.switch_to.window(tree_wh) @pytest.fixture(scope='function') def prefill_notebook(playwright_browser, notebook_server): - browser_raw = playwright_browser - playwright_browser = browser_raw.new_context() + browser_obj = playwright_browser + browser_context = browser_obj.new_context() # playwright_browser is the browser_context, # notebook_server is the server with directories @@ -141,18 +135,18 @@ def inner(cells): fname = os.path.basename(path) # Add the notebook server as a property of the playwright browser with the name jupyter_server_info - playwright_browser.jupyter_server_info = notebook_server + browser_context.jupyter_server_info = notebook_server # Open a new page in the browser and refer to it as the tree page - tree_page = playwright_browser.new_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: playwright_browser, + BROWSER_CONTEXT: browser_context, TREE_PAGE: tree_page, SERVER_INFO: notebook_server, - BROWSER_RAW: browser_raw + BROWSER_OBJ: browser_obj } return NotebookFrontend.new_notebook_frontend(auth_browser_data, existing_file_name=fname) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index d399fcfd9..0b338debb 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -23,11 +23,11 @@ # Key constants for browser_data -BROWSER = 'BROWSER' +BROWSER_CONTEXT = 'BROWSER_CONTEXT' TREE_PAGE = 'TREE_PAGE' EDITOR_PAGE = 'EDITOR_PAGE' SERVER_INFO = 'SERVER_INFO' -BROWSER_RAW = 'BROWSER_RAW' +BROWSER_OBJ = 'BROWSER_OBJ' # Other constants CELL_OUTPUT_SELECTOR = '.output_subarea' @@ -717,7 +717,7 @@ def _open_notebook_editor_page(self, existing_file_name=None): new_notebook_element.click() def wait_for_new_page(): - return [pg for pg in self._browser_data[BROWSER].pages if 'tree' not in pg.url] + 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] From 468bc7d14ad8970bac6edecde251f651f9fef7fc Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 12 Oct 2022 10:30:10 -0400 Subject: [PATCH 089/131] Minor docs updates/fixes. --- nbclassic/tests/end_to_end/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 0b338debb..0f48ba2c1 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -158,8 +158,8 @@ class NotebookFrontend: 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 + 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. @@ -170,7 +170,7 @@ class NotebookFrontend: Other design notes: This class works together with FrontendElement to abstract the testing library implementation away from test - writers. FrontendElement holds (private) handles to the underlying + writers. NotebookFrontend holds (private) handles to the underlying browser/context. TODO: @@ -203,7 +203,7 @@ def __init__(self, browser_data, existing_file_name=None): # Do some needed frontend setup self._wait_for_start() - self.disable_autosave_and_onbeforeunload() # TODO fix/refactor + self.disable_autosave_and_onbeforeunload() self.current_cell = None # Defined/used below # TODO refactor/remove def _wait_for_start(self): From 252b432db6c5468f9e83d255a40ae05469a2dd66 Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 12 Oct 2022 22:39:04 -0700 Subject: [PATCH 090/131] test dashboard nav updates for api timeout error --- nbclassic/tests/end_to_end/test_dashboard_nav.py | 4 ++-- nbclassic/tests/end_to_end/utils.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dashboard_nav.py b/nbclassic/tests/end_to_end/test_dashboard_nav.py index 26b90872f..d2183530a 100644 --- a/nbclassic/tests/end_to_end/test_dashboard_nav.py +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -22,7 +22,7 @@ def get_list_items(nb): 'link': a.get_attribute('href'), 'label': a.get_inner_text(), 'element': a, - } for a in link_items if a.get_inner_text() != '..'] + } for a in link_items if a.get_inner_text() != '..' and 'ipynb' not in a.get_inner_text()] def test_navigation(notebook_frontend): @@ -30,7 +30,7 @@ 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: + if not list_of_link_elements or len(list_of_link_elements) < 1: return False for item in list_of_link_elements: diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 0f48ba2c1..68161a8ce 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -20,6 +20,7 @@ import time from playwright.sync_api import ElementHandle, JSHandle +from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError # Key constants for browser_data @@ -96,8 +97,17 @@ def __bool__(self): def click(self): return self._element.click() + # DEBUG: Playwright Timeout Error when calling inner_text def get_inner_text(self): - return self._element.inner_text() + count = 0 + while count < 2: + try: + count += 1 + innerText = self._element.inner_text() + return innerText + except PlaywrightTimeoutError: + print('Timeout grabbing the inner text of an element') + return None def get_inner_html(self): return self._element.inner_html() From 3a2e6c7e5a31b2b7364620b67c516f7a6c431917 Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 09:23:04 -0700 Subject: [PATCH 091/131] Revert "test dashboard nav updates for api timeout error" This reverts commit 252b432db6c5468f9e83d255a40ae05469a2dd66. --- nbclassic/tests/end_to_end/test_dashboard_nav.py | 4 ++-- nbclassic/tests/end_to_end/utils.py | 12 +----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dashboard_nav.py b/nbclassic/tests/end_to_end/test_dashboard_nav.py index d2183530a..26b90872f 100644 --- a/nbclassic/tests/end_to_end/test_dashboard_nav.py +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -22,7 +22,7 @@ def get_list_items(nb): 'link': a.get_attribute('href'), 'label': a.get_inner_text(), 'element': a, - } for a in link_items if a.get_inner_text() != '..' and 'ipynb' not in a.get_inner_text()] + } for a in link_items if a.get_inner_text() != '..'] def test_navigation(notebook_frontend): @@ -30,7 +30,7 @@ def test_navigation(notebook_frontend): link_elements = get_list_items(notebook_frontend) def check_links(nb, list_of_link_elements): - if not list_of_link_elements or len(list_of_link_elements) < 1: + if len(list_of_link_elements) < 1: return False for item in list_of_link_elements: diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 68161a8ce..0f48ba2c1 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -20,7 +20,6 @@ import time from playwright.sync_api import ElementHandle, JSHandle -from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError # Key constants for browser_data @@ -97,17 +96,8 @@ def __bool__(self): def click(self): return self._element.click() - # DEBUG: Playwright Timeout Error when calling inner_text def get_inner_text(self): - count = 0 - while count < 2: - try: - count += 1 - innerText = self._element.inner_text() - return innerText - except PlaywrightTimeoutError: - print('Timeout grabbing the inner text of an element') - return None + return self._element.inner_text() def get_inner_html(self): return self._element.inner_html() From 2031387a2e3c17338e5d81a03211e306ab23f817 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 13 Oct 2022 14:10:34 -0700 Subject: [PATCH 092/131] fix for test_dashboard_nav and added utils functions --- .../tests/end_to_end/test_dashboard_nav.py | 9 +++-- nbclassic/tests/end_to_end/utils.py | 34 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_dashboard_nav.py b/nbclassic/tests/end_to_end/test_dashboard_nav.py index 26b90872f..558e04272 100644 --- a/nbclassic/tests/end_to_end/test_dashboard_nav.py +++ b/nbclassic/tests/end_to_end/test_dashboard_nav.py @@ -1,5 +1,8 @@ +"""Test navigation to directory links""" + + import os -from .utils import EDITOR_PAGE, TREE_PAGE +from .utils import TREE_PAGE from jupyter_server.utils import url_path_join pjoin = os.path.join @@ -11,12 +14,14 @@ def url_in_tree(nb, url=None): 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 """ - link_items = nb.locate_all('.item_link', page=TREE_PAGE) + notebook_list = nb.locate('#notebook_list', page=TREE_PAGE) + link_items = notebook_list.locate_all('.item_link') return [{ 'link': a.get_attribute('href'), diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 0f48ba2c1..cea102e97 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -113,6 +113,14 @@ def get_computed_property(self, prop_name): 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 locate(self, selector): """Locate child elements with the given selector""" element = self._element @@ -122,10 +130,27 @@ def locate(self, 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) @@ -134,15 +159,6 @@ def press(self, key): """Send a key press to the element""" return self._element.press(key) - def wait_for_state(self, state): - """Used to check for hidden, etc.""" - if hasattr(self._element, 'wait_for_element_state'): - self._element.wait_for_element_state(state) - elif hasattr(self._element, 'wait_for'): - self._element.wait_for(state) - else: - raise Exception('Unable to wait for state!') - def get_user_data(self): """Currently this is an unmanaged user data area, use it as you please""" return self._user_data From 9e04ca5cc3a7d7d1b30548d54a82897585de9717 Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 07:50:41 -0700 Subject: [PATCH 093/131] refactor test_save_readonly_as and test_save_notebook_as and added utils functions --- .../tests/end_to_end/test_save_as_notebook.py | 31 ++++++++---------- .../tests/end_to_end/test_save_readonly_as.py | 32 +++++++++---------- nbclassic/tests/end_to_end/utils.py | 25 +++++++++++++++ 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save_as_notebook.py b/nbclassic/tests/end_to_end/test_save_as_notebook.py index e927c5012..3c21b9901 100644 --- a/nbclassic/tests/end_to_end/test_save_as_notebook.py +++ b/nbclassic/tests/end_to_end/test_save_as_notebook.py @@ -1,16 +1,8 @@ -from os import rename -from webbrowser import get -from .utils import EDITOR_PAGE, TREE_PAGE -import time +"""Test readonly notebook saved and renamed""" -def check_for_rename(nb, selector, page, new_name): - check_count = 0 - nb_name = nb.locate(selector, page) - while nb_name != new_name and check_count <= 15: - nb_name = nb.locate(selector, page) - check_count += 1 - return nb_name +from .utils import EDITOR_PAGE + def save_as(nb): JS = '() => Jupyter.notebook.save_notebook_as()' @@ -24,21 +16,26 @@ 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") - check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_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) - save_message = notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE) + notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE) - inp = notebook_frontend.wait_for_selector('//input[@data-testid="save-as"]', page=EDITOR_PAGE) - inp.type('new_notebook.ipynb') - notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', 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') - check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="new_notebook.ipynb") + 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.wait_for('hidden') 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 index 3729f3c58..8bb0b2e8f 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -1,17 +1,8 @@ -from os import rename -from tkinter import E -from webbrowser import get -from .utils import EDITOR_PAGE, TREE_PAGE -import time +"""Test readonly notebook saved and renamed""" -def check_for_rename(nb, selector, page, new_name): - check_count = 0 - nb_name = nb.locate(selector, page) - while nb_name != new_name and check_count <= 5: - nb_name = nb.locate(selector, page) - check_count += 1 - return nb_name +from .utils import EDITOR_PAGE + def save_as(nb): JS = '() => Jupyter.notebook.save_notebook_as()' @@ -25,6 +16,7 @@ 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): notebook_frontend.edit_cell(index=0, content='a=10; print(a)') notebook_frontend.wait_for_kernel_ready() @@ -36,13 +28,21 @@ def test_save_notebook_as(notebook_frontend): # Wait for Save As modal, save save_as(notebook_frontend) - notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE) - inp = notebook_frontend.wait_for_selector('//input[@data-testid="save-as"]', page=EDITOR_PAGE) - inp.type('new_notebook.ipynb') + # 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') + + 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) - check_for_rename(notebook_frontend, '#notebook_name', page=EDITOR_PAGE, new_name="new_notebook.ipynb") + locator_element.wait_for('hidden') + + notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE) # Test that the name changed assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb" diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index cea102e97..7f311b0bb 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -20,6 +20,7 @@ import time from playwright.sync_api import ElementHandle, JSHandle +from playwright.sync_api import expect # Key constants for browser_data @@ -293,6 +294,17 @@ def type(self, text, page): 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 = "" @@ -382,6 +394,19 @@ def locate_all(self, selector, page): 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) + expect(locator).to_be_focused() + 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: From 9947aca4654e6cf3dd27ad7acc8b8272cbafe306 Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 07:51:04 -0700 Subject: [PATCH 094/131] update method name in test_kernel_menu --- nbclassic/tests/end_to_end/test_kernel_menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index 1be27f060..d03f76d62 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -25,7 +25,7 @@ def test_cancel_restart_or_shutdown(notebook_frontend): notebook_frontend.wait_for_selector(cancel_selector, EDITOR_PAGE).click() modal = notebook_frontend.wait_for_selector('.modal-backdrop', EDITOR_PAGE) - modal.wait_for_state('hidden') + modal.wait_for('hidden') assert notebook_frontend.is_kernel_running() @@ -46,7 +46,7 @@ def test_menu_items(notebook_frontend): # Restart # (can't click the menu while a modal dialog is fading out) modal = notebook_frontend.wait_for_selector('.modal-backdrop', EDITOR_PAGE) - modal.wait_for_state('hidden') + modal.wait_for('hidden') kernel_menu.click() notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() From e75e4b6b5095149343152a2ec69f3fa013e1499a Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 10:24:13 -0700 Subject: [PATCH 095/131] move/disabled flaky selenium tests --- .../workflows => nbclassic/tests/selenium}/flaky-selenium.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github/workflows => nbclassic/tests/selenium}/flaky-selenium.yml (100%) diff --git a/.github/workflows/flaky-selenium.yml b/nbclassic/tests/selenium/flaky-selenium.yml similarity index 100% rename from .github/workflows/flaky-selenium.yml rename to nbclassic/tests/selenium/flaky-selenium.yml From a4ac9161120c54388e5574539241907373dfc53c Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 10:26:14 -0700 Subject: [PATCH 096/131] update timeout for wait_for_cell_output --- nbclassic/tests/end_to_end/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 7f311b0bb..2dfbfccd9 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -621,7 +621,7 @@ def wait_for_condition(self, check_func, timeout=30, period=.1): else: raise TimeoutError() - def wait_for_cell_output(self, index=0, timeout=3): + 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!') From 2897524a5ce8fe4e2433a716bbb38ed374920800 Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 13:10:03 -0700 Subject: [PATCH 097/131] updated find_and_replace and focus locator in locate_and_focus --- nbclassic/tests/end_to_end/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 2dfbfccd9..080adcdb1 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -404,6 +404,7 @@ def locate_and_focus(self, selector, page): raise Exception('Error, provide a valid page to locate from!') locator = specified_page.locator(selector) + locator.focus() expect(locator).to_be_focused() return FrontendElement(locator) @@ -559,8 +560,10 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): self.press('f', EDITOR_PAGE) self._editor_page.locator('#find-and-replace') self._editor_page.locator('#findreplace_allcells_btn').click() - self._editor_page.locator('#findreplace_find_inp').type(find_txt) - self._editor_page.locator('#findreplace_replace_inp').type(replace_txt) + self.locate_and_focus('#findreplace_find_inp', page=EDITOR_PAGE) + self._editor_page.keyboard.insert_text(find_txt) + self.locate_and_focus('#findreplace_replace_inp', page=EDITOR_PAGE) + self._editor_page.keyboard.insert_text(replace_txt) self._editor_page.locator('#findreplace_replaceall_btn').click() def convert_cell_type(self, index=0, cell_type="code"): From 3ff08b10b8d750db849be16bf50c8fb1b9914efc Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 14 Oct 2022 14:11:12 -0700 Subject: [PATCH 098/131] flakyness updates --- nbclassic/tests/end_to_end/test_execute_code.py | 6 ++++++ nbclassic/tests/end_to_end/test_save_readonly_as.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index a8605430a..4d1278256 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -9,6 +9,7 @@ def test_execute_code(notebook_frontend): 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 @@ -16,6 +17,7 @@ def test_execute_code(notebook_frontend): 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 @@ -28,6 +30,7 @@ def test_execute_code(notebook_frontend): 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 @@ -35,6 +38,7 @@ def test_execute_code(notebook_frontend): 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 @@ -52,6 +56,7 @@ def test_execute_code(notebook_frontend): cell1.execute(); """, page=EDITOR_PAGE) outputs = notebook_frontend.wait_for_cell_output(0) + outputs.wait_for('visible') assert notebook_frontend.get_cell_output(1) is None # Execute a cell with stop_on_error=false @@ -63,4 +68,5 @@ def test_execute_code(notebook_frontend): cell1.execute(); """, page=EDITOR_PAGE) outputs = notebook_frontend.wait_for_cell_output(1) + outputs.wait_for('visible') assert outputs.get_inner_text().strip() == '14' diff --git a/nbclassic/tests/end_to_end/test_save_readonly_as.py b/nbclassic/tests/end_to_end/test_save_readonly_as.py index 8bb0b2e8f..83b62fe9d 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -36,9 +36,15 @@ def test_save_notebook_as(notebook_frontend): 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) - notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', 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.wait_for('hidden') From 9823f0581d53caf10d76881f1e7aeb85ec105c47 Mon Sep 17 00:00:00 2001 From: RRosio Date: Mon, 17 Oct 2022 11:10:59 -0700 Subject: [PATCH 099/131] exception handling for timeout error --- nbclassic/tests/end_to_end/test_save_readonly_as.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save_readonly_as.py b/nbclassic/tests/end_to_end/test_save_readonly_as.py index 83b62fe9d..1fc8a1e51 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -2,6 +2,7 @@ from .utils import EDITOR_PAGE +from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError def save_as(nb): @@ -17,7 +18,7 @@ def set_notebook_name(nb, name): nb.evaluate(JS, page=EDITOR_PAGE) -def test_save_notebook_as(notebook_frontend): +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) @@ -46,7 +47,11 @@ def test_save_notebook_as(notebook_frontend): save_btn.click() # notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE) - locator_element.wait_for('hidden') + try: + locator_element.wait_for('hidden') + except PlaywrightTimeoutError: + print("There was a timeout error with Playwright in test_save_readonly_as") + pass notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE) From c3bfc6551b8cbbe2613b27b4077af09db19d1123 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 15:15:55 -0400 Subject: [PATCH 100/131] Added retry script for CI runner py dependency install step. --- .github/workflows/playwright.yml | 5 +--- tools/install_pydeps.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tools/install_pydeps.py diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f26b13748..7df348bc1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -34,10 +34,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install -U pip setuptools wheel - pip install pytest-playwright - playwright install - pip install .[test] + python tools/install_pydeps.py - name: Run Tests run: | diff --git a/tools/install_pydeps.py b/tools/install_pydeps.py new file mode 100644 index 000000000..580499e8b --- /dev/null +++ b/tools/install_pydeps.py @@ -0,0 +1,43 @@ +"""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(): + attempt(step_arglist, max_attempts=3, name=step_name) + + +if __name__ == '__main__': + run() From b4a8565ac44a1554e2f6fa9343d6a0a1af6c0623 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 16:00:50 -0400 Subject: [PATCH 101/131] Refactor element locator. --- nbclassic/tests/end_to_end/test_kernel_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index d03f76d62..98198bcd2 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -45,7 +45,7 @@ def test_menu_items(notebook_frontend): # Restart # (can't click the menu while a modal dialog is fading out) - modal = notebook_frontend.wait_for_selector('.modal-backdrop', EDITOR_PAGE) + modal = notebook_frontend.locate('.modal-backdrop', EDITOR_PAGE) modal.wait_for('hidden') kernel_menu.click() From b45fab69ba1b8a56397fa96db6b1888b390110cc Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 16:15:04 -0400 Subject: [PATCH 102/131] Refactor modal check method. --- nbclassic/tests/end_to_end/test_kernel_menu.py | 3 +-- nbclassic/tests/end_to_end/utils.py | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index 98198bcd2..45d34f785 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -45,8 +45,7 @@ def test_menu_items(notebook_frontend): # Restart # (can't click the menu while a modal dialog is fading out) - modal = notebook_frontend.locate('.modal-backdrop', EDITOR_PAGE) - modal.wait_for('hidden') + modal = notebook_frontend.locate('.modal-backdrop', EDITOR_PAGE).expect_to_not_be_visible() kernel_menu.click() notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 080adcdb1..60ae46fc7 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -164,6 +164,12 @@ def get_user_data(self): """Currently this is an unmanaged user data area, use it as you please""" return self._user_data + def expect_to_not_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 + class NotebookFrontend: """Performs high level Notebook tasks for automated testing. From 5df05f297bb6c179b3d0af734ec4ae6f934c6bac Mon Sep 17 00:00:00 2001 From: RRosio Date: Mon, 17 Oct 2022 13:15:31 -0700 Subject: [PATCH 103/131] save_as_notebook: wrapped wait_for in try except block --- nbclassic/tests/end_to_end/test_save_as_notebook.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_save_as_notebook.py b/nbclassic/tests/end_to_end/test_save_as_notebook.py index 3c21b9901..cb693ef41 100644 --- a/nbclassic/tests/end_to_end/test_save_as_notebook.py +++ b/nbclassic/tests/end_to_end/test_save_as_notebook.py @@ -2,6 +2,7 @@ from .utils import EDITOR_PAGE +from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError def save_as(nb): @@ -35,7 +36,11 @@ def test_save_notebook_as(notebook_frontend): 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.wait_for('hidden') + try: + locator_element.wait_for('hidden') + except PlaywrightTimeoutError: + print("There was a timeout error with Playwright in test_save_notebook_as") + pass assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb" assert "new_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE) From 4ccc21f043fd3512239cd0064f8b107dbbe2d72d Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 17:05:11 -0400 Subject: [PATCH 104/131] Flakiness updates for text_execute_code and test_kernel_menu + utils --- nbclassic/tests/end_to_end/test_execute_code.py | 5 ++--- nbclassic/tests/end_to_end/test_kernel_menu.py | 2 +- nbclassic/tests/end_to_end/utils.py | 11 +++-------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 4d1278256..4c48c7afe 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -55,9 +55,8 @@ def test_execute_code(notebook_frontend): cell0.execute(); cell1.execute(); """, page=EDITOR_PAGE) - outputs = notebook_frontend.wait_for_cell_output(0) - outputs.wait_for('visible') - assert notebook_frontend.get_cell_output(1) is None + outputs = notebook_frontend.get_cell_output(0) + outputs.expect_not_to_be_visible() # Execute a cell with stop_on_error=false notebook_frontend.clear_all_output() diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index 45d34f785..66677dcfe 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -45,7 +45,7 @@ def test_menu_items(notebook_frontend): # Restart # (can't click the menu while a modal dialog is fading out) - modal = notebook_frontend.locate('.modal-backdrop', EDITOR_PAGE).expect_to_not_be_visible() + 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() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 60ae46fc7..4d9926013 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -164,7 +164,7 @@ def get_user_data(self): """Currently this is an unmanaged user data area, use it as you please""" return self._user_data - def expect_to_not_be_visible(self): + def expect_not_to_be_visible(self): try: expect(self._element).not_to_be_visible() except ValueError as err: @@ -608,14 +608,9 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): 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 + cell = self._cells[index].as_element().locator(output) # Find cell child elements - if cell is None: - return None - - element = FrontendElement(cell, user_data={'index': index}) - - return element + return FrontendElement(cell, user_data={'index': index}) 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""" From 0a735efb27527b156e204230fd85df5d2cee807c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 17:15:17 -0400 Subject: [PATCH 105/131] Fix get_cell_output refactor --- nbclassic/tests/end_to_end/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 4d9926013..6371b919e 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -608,7 +608,7 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): 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().locator(output) # Find cell child elements + cell = self._cells[index].locate(output) # Find cell child elements return FrontendElement(cell, user_data={'index': index}) From 1bb8ebf190658ab1bcc55c693dc3bc2e066b8f55 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 17:40:34 -0400 Subject: [PATCH 106/131] Updated get_cell_output --- nbclassic/tests/end_to_end/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 6371b919e..2548c045e 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -13,8 +13,7 @@ This module was converted and refactored from the older selenium test suite. """ - - +import copy import datetime import os import time @@ -88,6 +87,12 @@ def __init__(self, item, user_data=None): self._element = as_element else: self._bool = False + if isinstance(item, ElementHandle): + 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""" @@ -608,7 +613,7 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): """Get the cell output for a given cell""" - cell = self._cells[index].locate(output) # Find cell child elements + cell = self.cells[index].locate(output) # Find cell child elements return FrontendElement(cell, user_data={'index': index}) From 2508fa7ee409ba3ea5487ee1bab2afbf49579108 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 17:42:51 -0400 Subject: [PATCH 107/131] Fix wrong classname in FrontendElement --- nbclassic/tests/end_to_end/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 2548c045e..263dc66be 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -87,7 +87,7 @@ def __init__(self, item, user_data=None): self._element = as_element else: self._bool = False - if isinstance(item, ElementHandle): + if isinstance(item, FrontendElement): self._raw = item self._element = item._element self._bool = item._bool From 033a10fa9d552b0e7aad5d19311fef82355b44e9 Mon Sep 17 00:00:00 2001 From: RRosio Date: Mon, 17 Oct 2022 14:45:07 -0700 Subject: [PATCH 108/131] updates from wait_for to expect not to be visible --- nbclassic/tests/end_to_end/test_save_as_notebook.py | 6 +----- nbclassic/tests/end_to_end/test_save_readonly_as.py | 6 +----- nbclassic/tests/end_to_end/utils.py | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save_as_notebook.py b/nbclassic/tests/end_to_end/test_save_as_notebook.py index cb693ef41..f90787812 100644 --- a/nbclassic/tests/end_to_end/test_save_as_notebook.py +++ b/nbclassic/tests/end_to_end/test_save_as_notebook.py @@ -36,11 +36,7 @@ def test_save_notebook_as(notebook_frontend): 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) - try: - locator_element.wait_for('hidden') - except PlaywrightTimeoutError: - print("There was a timeout error with Playwright in test_save_notebook_as") - pass + 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 index 1fc8a1e51..2e0094303 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -47,11 +47,7 @@ def test_save_readonly_as(notebook_frontend): save_btn.click() # notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE) - try: - locator_element.wait_for('hidden') - except PlaywrightTimeoutError: - print("There was a timeout error with Playwright in test_save_readonly_as") - pass + locator_element.expect_not_to_be_visible() notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 4d9926013..ac9735c1c 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -743,7 +743,7 @@ def is_kernel_running(self): ) def wait_for_kernel_ready(self): - self._tree_page.locator(".kernel_idle_icon") + self._tree_page.wait_for_selector(".kernel_idle_icon") def _open_notebook_editor_page(self, existing_file_name=None): tree_page = self._tree_page From 8f90d6eecbac9e2c866d792ee242d7fdc6108dd1 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 17:50:29 -0400 Subject: [PATCH 109/131] Refactor test_shutdown --- nbclassic/tests/end_to_end/test_shutdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/test_shutdown.py b/nbclassic/tests/end_to_end/test_shutdown.py index 114907d4c..6c2a5010d 100644 --- a/nbclassic/tests/end_to_end/test_shutdown.py +++ b/nbclassic/tests/end_to_end/test_shutdown.py @@ -11,4 +11,4 @@ def test_shutdown(prefill_notebook): notebook_frontend.execute_cell(0) assert not notebook_frontend.is_kernel_running() - assert notebook_frontend.get_cell_output() == None \ No newline at end of file + assert not notebook_frontend.get_cell_output() From 5e1704b27b98fb688dc165d361e8bddfd17f12f5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 17 Oct 2022 18:30:54 -0400 Subject: [PATCH 110/131] Reverted utils edits --- nbclassic/tests/end_to_end/test_execute_code.py | 3 +-- nbclassic/tests/end_to_end/utils.py | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 4c48c7afe..e63909cb2 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -55,8 +55,7 @@ def test_execute_code(notebook_frontend): cell0.execute(); cell1.execute(); """, page=EDITOR_PAGE) - outputs = notebook_frontend.get_cell_output(0) - outputs.expect_not_to_be_visible() + assert notebook_frontend.get_cell_output(0) is None # Execute a cell with stop_on_error=false notebook_frontend.clear_all_output() diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 263dc66be..cc86f472d 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -613,9 +613,14 @@ def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): def get_cell_output(self, index=0, output=CELL_OUTPUT_SELECTOR): """Get the cell output for a given cell""" - cell = self.cells[index].locate(output) # Find cell child elements + cell = self._cells[index].as_element().query_selector(output) # Find cell child elements - return FrontendElement(cell, user_data={'index': index}) + 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""" From 157c007b09944bcfb13da3403215134ea89abda1 Mon Sep 17 00:00:00 2001 From: RRosio Date: Mon, 17 Oct 2022 18:27:50 -0700 Subject: [PATCH 111/131] update editor page wait for selector --- nbclassic/tests/end_to_end/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index cb211ed0f..14995bb1a 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -753,7 +753,7 @@ def is_kernel_running(self): ) def wait_for_kernel_ready(self): - self._tree_page.wait_for_selector(".kernel_idle_icon") + self._editor_page.wait_for_selector(".kernel_idle_icon") def _open_notebook_editor_page(self, existing_file_name=None): tree_page = self._tree_page From 31504236e4a7e27609a9033095bc64ff4fb81345 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 18 Oct 2022 08:52:02 -0400 Subject: [PATCH 112/131] Dummy branch for single test runs --- .github/workflows/check-release.yml | 56 ------------ .github/workflows/docs.yml | 52 ----------- .github/workflows/downstream.yml | 32 ------- .github/workflows/enforce-label.yml | 11 --- .github/workflows/js.yml | 67 --------------- .github/workflows/playwright.yml | 10 ++- .github/workflows/pythonpackage.yml | 128 ---------------------------- 7 files changed, 7 insertions(+), 349 deletions(-) delete mode 100644 .github/workflows/check-release.yml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/downstream.yml delete mode 100644 .github/workflows/enforce-label.yml delete mode 100644 .github/workflows/js.yml delete mode 100644 .github/workflows/pythonpackage.yml diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml deleted file mode 100644 index 3a4fa8008..000000000 --- a/.github/workflows/check-release.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Check Release -on: - push: - branches: ["master"] - pull_request: - branches: ["*"] - -jobs: - check_release: - runs-on: ubuntu-latest - strategy: - matrix: - group: [check_release, link_check] - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Install Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - architecture: "x64" - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: Cache pip - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}-pip- - - name: Cache checked links - if: ${{ matrix.group == 'link_check' }} - uses: actions/cache@v2 - with: - path: ~/.cache/pytest-link-check - key: ${{ runner.os }}-linkcheck-${{ hashFiles('**/*.md', '**/*.rst') }}-md-links - restore-keys: | - ${{ runner.os }}-linkcheck- - - name: Upgrade packaging dependencies - run: | - pip install --upgrade pip setuptools wheel --user - - name: Install Dependencies - run: | - pip install -e . - - name: Check Release - if: ${{ matrix.group == 'check_release' }} - uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Run Link Check - if: ${{ matrix.group == 'link_check' }} - uses: jupyter-server/jupyter_releaser/.github/actions/check-links@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 20f4e96b5..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Docs Tests -on: - push: - branches: '*' - pull_request: - branches: '*' -jobs: - build: - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu] - python-version: [ '3.7' ] - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Install Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x64' - - name: Upgrade packaging dependencies - run: | - pip install --upgrade pip setuptools wheel - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: Cache pip - uses: actions/cache@v1 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python-version }}- - ${{ runner.os }}-pip- - - name: Install the Python dependencies - run: | - pip install -e .[test] codecov - pip install -r docs/doc-requirements.txt - wget https://github.com/jgm/pandoc/releases/download/1.19.1/pandoc-1.19.1-1-amd64.deb && sudo dpkg -i pandoc-1.19.1-1-amd64.deb - - name: List installed packages - run: | - pip freeze - pip check - - name: Run tests on documentation - run: | - EXIT_STATUS=0 - make -C docs/ html || EXIT_STATUS=$? - cd docs/source && pytest --nbval --current-env .. || EXIT_STATUS=$? - exit $EXIT_STATUS diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml deleted file mode 100644 index c58d21adb..000000000 --- a/.github/workflows/downstream.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Test Downstream - -on: - push: - branches: "*" - pull_request: - branches: "*" - -jobs: - downstream: - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - - name: Test jupyterlab_server - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 - with: - package_name: jupyterlab_server - - - name: Test jupyterlab - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 - with: - package_name: jupyterlab - package_spec: "\".[test]\"" - test_command: "python -m jupyterlab.browser_check --no-browser-test" - diff --git a/.github/workflows/enforce-label.yml b/.github/workflows/enforce-label.yml deleted file mode 100644 index 354a0468d..000000000 --- a/.github/workflows/enforce-label.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Enforce PR label - -on: - pull_request: - types: [labeled, unlabeled, opened, edited, synchronize] -jobs: - enforce-label: - runs-on: ubuntu-latest - steps: - - name: enforce-triage-label - uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml deleted file mode 100644 index b215521dd..000000000 --- a/.github/workflows/js.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Linux JS Tests - -on: - push: - branches: '*' - pull_request: - branches: '*' - -jobs: - build: - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos] - group: [notebook, base, services] - exclude: - - group: services - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: '12.x' - - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Cache pip on Linux - uses: actions/cache@v1 - if: startsWith(runner.os, 'Linux') - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.python }}-${{ hashFiles('**/requirements.txt', 'setup.py') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python }} - - - name: Temporary workaround for sanitizer loading in JS Tests - run: | - cp tools/security_deprecated.js nbclassic/static/base/js/security.js - - - name: Install dependencies - run: | - pip install --upgrade pip - pip install --upgrade setuptools wheel - npm install - npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7 - pip install .[test] - - - name: Run Tests - run: | - python -m nbclassic.jstest ${{ matrix.group }} diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7df348bc1..d28e05891 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos] - python-version: [ '3.7', '3.8', '3.9', '3.10'] + python-version: [ '3.7', '3.8'] steps: - name: Checkout uses: actions/checkout@v2 @@ -36,6 +36,10 @@ jobs: run: | python tools/install_pydeps.py - - name: Run Tests + - name: Run Test A run: | - pytest -sv nbclassic/tests/end_to_end + pytest -sv nbclassic/tests/end_to_end/test_execute_code.py + + - name: Run Test B + run: | + pytest -sv nbclassic/tests/end_to_end/test_buffering.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml deleted file mode 100644 index eac1ce0b9..000000000 --- a/.github/workflows/pythonpackage.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Testing nbclassic - -on: - push: - branches: - - master - pull_request: - -jobs: - build: - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos, windows] - python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"] - exclude: - - os: windows - python-version: pypy-3.8 - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Install pip dependencies - run: | - pip install -v -e ".[test]" pytest-cov - - name: Check pip environment - run: | - pip freeze - pip check - - name: Run the help command - run: | - jupyter nbclassic -h - - name: Test with pytest and coverage - if: ${{ matrix.python-version != 'pypy-3.8' }} - run: | - python -m pytest -vv --cov=nbclassic --cov-report term-missing:skip-covered || python -m pytest -vv --cov=nbclassic --cov-report term-missing:skip-covered - - name: Run the tests on pypy - if: ${{ matrix.python-version == 'pypy-3.8' }} - run: | - python -m pytest -vv || python -m pytest -vv -lf - - name: Test Running Server - if: startsWith(runner.os, 'Linux') - run: | - jupyter nbclassic --no-browser & - TASK_PID=$! - # Make sure the task is running - ps -p $TASK_PID || exit 1 - sleep 5 - kill $TASK_PID - wait $TASK_PID - -# test_miniumum_versions: -# name: Test Minimum Versions -# timeout-minutes: 20 -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v2 -# - name: Base Setup -# uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 -# with: -# python_version: "3.7" -# - name: Install miniumum versions -# uses: jupyterlab/maintainer-tools/.github/actions/install-minimums@v1 -# - name: Run the unit tests -# run: pytest -vv || pytest -vv --lf - - test_prereleases: - name: Test Prereleases - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Install the Python dependencies - run: | - pip install --pre -e ".[test]" - - name: List installed packages - run: | - pip freeze - pip check - - name: Run the tests - run: | - pytest -vv || pytest -vv --lf - - make_sdist: - name: Make SDist - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v2 - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Build SDist - run: | - pip install build - python -m build --sdist - - uses: actions/upload-artifact@v2 - with: - name: "sdist" - path: dist/*.tar.gz - - test_sdist: - runs-on: ubuntu-latest - needs: [make_sdist] - name: Install from SDist and Test - timeout-minutes: 20 - steps: - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Download sdist - uses: actions/download-artifact@v2 - - name: Install From SDist - run: | - set -ex - cd sdist - mkdir test - tar --strip-components=1 -zxvf *.tar.gz -C ./test - cd test - pip install -e .[test] - pip install pytest-github-actions-annotate-failures - - name: Run Test - run: | - cd sdist/test - pytest -vv || pytest -vv --lf From 770b14bab24e6a33247faa984eb383bd921e49b3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 18 Oct 2022 11:40:13 -0400 Subject: [PATCH 113/131] Combine single test runs into one step --- .github/workflows/playwright.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d28e05891..a88b63513 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,10 +36,7 @@ jobs: run: | python tools/install_pydeps.py - - name: Run Test A + - name: Run Individual Tests run: | pytest -sv nbclassic/tests/end_to_end/test_execute_code.py - - - name: Run Test B - run: | pytest -sv nbclassic/tests/end_to_end/test_buffering.py From e225f09a514d90d449bfaeb68548eecdb37c2ccf Mon Sep 17 00:00:00 2001 From: RRosio Date: Tue, 18 Oct 2022 11:43:47 -0700 Subject: [PATCH 114/131] testing full test suit runs #1 --- .github/workflows/playwright.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a88b63513..ea38690f8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -38,5 +38,4 @@ jobs: - name: Run Individual Tests run: | - pytest -sv nbclassic/tests/end_to_end/test_execute_code.py - pytest -sv nbclassic/tests/end_to_end/test_buffering.py + pytest -sv nbclassic/tests/end_to_end From 05526c4fea1df72b5d19e5c74f2607a1ee345e31 Mon Sep 17 00:00:00 2001 From: RRosio Date: Tue, 18 Oct 2022 11:48:58 -0700 Subject: [PATCH 115/131] macos dbg2 --- .github/workflows/playwright.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index ea38690f8..b26c58809 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,8 +11,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu, macos] - python-version: [ '3.7', '3.8'] + os: [macos] + python-version: [ '3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout uses: actions/checkout@v2 From 14f04c9d45e17b45182789c094ee8bc28ba689de Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 19 Oct 2022 12:48:31 -0400 Subject: [PATCH 116/131] Added state checks at various points in the test. --- nbclassic/tests/end_to_end/test_execute_code.py | 17 +++++++++++++++-- nbclassic/tests/end_to_end/utils.py | 9 +++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index e63909cb2..387bc9aee 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -1,7 +1,10 @@ """Test basic cell execution methods, related shortcuts, and error modes""" -from .utils import EDITOR_PAGE +import re +import time + +from .utils import CELL_OUTPUT_SELECTOR, EDITOR_PAGE def test_execute_code(notebook_frontend): @@ -58,13 +61,23 @@ def test_execute_code(notebook_frontend): 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, 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) + """, 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') outputs = notebook_frontend.wait_for_cell_output(1) outputs.wait_for('visible') assert outputs.get_inner_text().strip() == '14' diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 14995bb1a..2957f25b2 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -175,6 +175,15 @@ def expect_not_to_be_visible(self): 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. From e8f16dc6c940f7bec8a06314953dab50f4e17ef2 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 19 Oct 2022 13:48:55 -0400 Subject: [PATCH 117/131] Added extra checks for cell output. --- .../tests/end_to_end/test_execute_code.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_execute_code.py b/nbclassic/tests/end_to_end/test_execute_code.py index 387bc9aee..e5a0f0ef2 100644 --- a/nbclassic/tests/end_to_end/test_execute_code.py +++ b/nbclassic/tests/end_to_end/test_execute_code.py @@ -1,9 +1,6 @@ """Test basic cell execution methods, related shortcuts, and error modes""" -import re -import time - from .utils import CELL_OUTPUT_SELECTOR, EDITOR_PAGE @@ -66,18 +63,28 @@ def test_execute_code(notebook_frontend): 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, period=5) + 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(); + 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) - outputs.wait_for('visible') + assert outputs is not None assert outputs.get_inner_text().strip() == '14' From 81da4b68473d89e4aa6e93f385f312a5555637ce Mon Sep 17 00:00:00 2001 From: RRosio Date: Wed, 19 Oct 2022 11:02:18 -0700 Subject: [PATCH 118/131] update save_readonly_as --- nbclassic/tests/end_to_end/test_save_readonly_as.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_save_readonly_as.py b/nbclassic/tests/end_to_end/test_save_readonly_as.py index 2e0094303..6b6bde96d 100644 --- a/nbclassic/tests/end_to_end/test_save_readonly_as.py +++ b/nbclassic/tests/end_to_end/test_save_readonly_as.py @@ -47,12 +47,15 @@ def test_save_readonly_as(notebook_frontend): 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() + # 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) + # notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE) - # Test that the name changed - assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb" + # # 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) From e264193332039a3e14c7eaa4966093fabe4d58f2 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Wed, 19 Oct 2022 21:11:43 -0400 Subject: [PATCH 119/131] Refactor find/replace to use .value in eval call. --- .../tests/end_to_end/test_find_and_replace.py | 10 +++-- nbclassic/tests/end_to_end/utils.py | 43 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/nbclassic/tests/end_to_end/test_find_and_replace.py b/nbclassic/tests/end_to_end/test_find_and_replace.py index 7b0ec686b..787c9c19e 100644 --- a/nbclassic/tests/end_to_end/test_find_and_replace.py +++ b/nbclassic/tests/end_to_end/test_find_and_replace.py @@ -11,10 +11,14 @@ def test_find_and_replace(notebook_frontend): 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 - assert notebook_frontend.get_cells_contents() == [ - s.replace(find_str, replace_str) for s in INITIAL_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/utils.py b/nbclassic/tests/end_to_end/utils.py index 2957f25b2..175fb6dc7 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -17,6 +17,7 @@ import datetime import os import time +import traceback from playwright.sync_api import ElementHandle, JSHandle from playwright.sync_api import expect @@ -127,6 +128,9 @@ def wait_for(self, 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 @@ -425,7 +429,11 @@ def locate_and_focus(self, selector, page): locator = specified_page.locator(selector) locator.focus() - expect(locator).to_be_focused() + self.wait_for_condition( + lambda: expect(locator).to_be_focused(timeout=1000), + timeout=120, + period=5 + ) return FrontendElement(locator) def wait_for_frame(self, count=None, name=None, page=None): @@ -580,10 +588,22 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): self.press('f', EDITOR_PAGE) self._editor_page.locator('#find-and-replace') self._editor_page.locator('#findreplace_allcells_btn').click() - self.locate_and_focus('#findreplace_find_inp', page=EDITOR_PAGE) - self._editor_page.keyboard.insert_text(find_txt) - self.locate_and_focus('#findreplace_replace_inp', page=EDITOR_PAGE) - self._editor_page.keyboard.insert_text(replace_txt) + 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"): @@ -637,10 +657,15 @@ def wait_for_condition(self, check_func, timeout=30, period=.1): begin = datetime.datetime.now() while (datetime.datetime.now() - begin).seconds < timeout: - condition = check_func() - if condition: - return condition - time.sleep(period) + 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() From 103f5ca24fc7df7369da073ceeb64ccca5b23df7 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 09:13:59 -0400 Subject: [PATCH 120/131] Added retry to browser fixture --- nbclassic/tests/end_to_end/conftest.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/nbclassic/tests/end_to_end/conftest.py b/nbclassic/tests/end_to_end/conftest.py index 18e00a641..a45309ffa 100644 --- a/nbclassic/tests/end_to_end/conftest.py +++ b/nbclassic/tests/end_to_end/conftest.py @@ -1,6 +1,7 @@ """Fixtures for pytest/playwright end_to_end tests.""" +import datetime import os import json import sys @@ -75,10 +76,16 @@ def notebook_server(): @pytest.fixture(scope='function') def playwright_browser(playwright): - if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome': - browser = playwright.chromium.launch() - else: - browser = playwright.firefox.launch() + 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 From 146b0724c7fa8a52493d65ce23900dd1bf5e3111 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 13:27:52 -0400 Subject: [PATCH 121/131] Removed wait from locate and focus method. --- nbclassic/tests/end_to_end/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index 175fb6dc7..c50a022ae 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -429,11 +429,6 @@ def locate_and_focus(self, selector, page): locator = specified_page.locator(selector) locator.focus() - self.wait_for_condition( - lambda: expect(locator).to_be_focused(timeout=1000), - timeout=120, - period=5 - ) return FrontendElement(locator) def wait_for_frame(self, count=None, name=None, page=None): From 94be6048a44fd8396734d7d666254733132764a3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 13:34:38 -0400 Subject: [PATCH 122/131] Temp workflow edits for debugging (add workflows back later) --- .github/workflows/check-release.yml | 57 ----------------------------- .github/workflows/playwright.yml | 4 +- 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/check-release.yml diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml deleted file mode 100644 index 3b2a1ed3e..000000000 --- a/.github/workflows/check-release.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Check Release -on: - push: - branches: ["master"] - pull_request: - branches: ["*"] - -jobs: - check_release: - runs-on: ubuntu-latest - strategy: - matrix: - group: [check_release, link_check] - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Install Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - architecture: "x64" - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: Cache pip - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}-pip- - - name: Cache checked links - if: ${{ matrix.group == 'link_check' }} - uses: actions/cache@v2 - with: - path: ~/.cache/pytest-link-check - key: ${{ runner.os }}-linkcheck-${{ hashFiles('**/*.md', '**/*.rst') }}-md-links - restore-keys: | - ${{ runner.os }}-linkcheck- - - name: Upgrade packaging dependencies - run: | - pip install --upgrade pip setuptools wheel --user - - name: Install Dependencies - run: | - pip install -e . - - name: Check Release - if: ${{ matrix.group == 'check_release' }} - uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 - with: - version_spec: 100.100.100 - token: ${{ secrets.GITHUB_TOKEN }} - - name: Run Link Check - if: ${{ matrix.group == 'link_check' }} - uses: jupyter-server/jupyter_releaser/.github/actions/check-links@v1 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index b26c58809..25df0a14f 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,6 +36,6 @@ jobs: run: | python tools/install_pydeps.py - - name: Run Individual Tests + - name: Run Suite X Times run: | - pytest -sv nbclassic/tests/end_to_end + python tools/runsuite_repeat.py From 2ac94b8342b5538afff789c88890155ea811e9b0 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 13:40:01 -0400 Subject: [PATCH 123/131] Fixed temp workflow run files. --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 25df0a14f..67cdb338a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos] + os: [ubuntu] python-version: [ '3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout From b8a909dfd1cc605a8fd612c04f380d4e38f6eebd Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 13:46:37 -0400 Subject: [PATCH 124/131] Fixed temp workflow edits. --- tools/runsuite_repeat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tools/runsuite_repeat.py diff --git a/tools/runsuite_repeat.py b/tools/runsuite_repeat.py new file mode 100644 index 000000000..81b77c65a --- /dev/null +++ b/tools/runsuite_repeat.py @@ -0,0 +1,15 @@ +"""CI/CD debug script""" + + +import subprocess +import time + + +def run(): + for stepnum in range(10): + proc = subprocess.run('pytest -sv nbclassic/tests/end_to_end') + print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> {"Success" if proc.returncode == 0 else proc.returncode}\n') + + +if __name__ == '__main__': + run() From b5cf67a99b436453fcbf3db2b605a42ff113c36c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 13:59:12 -0400 Subject: [PATCH 125/131] Temp workflow edits --- tools/runsuite_repeat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/runsuite_repeat.py b/tools/runsuite_repeat.py index 81b77c65a..536166124 100644 --- a/tools/runsuite_repeat.py +++ b/tools/runsuite_repeat.py @@ -1,12 +1,14 @@ """CI/CD debug script""" +import os import subprocess import time def run(): for stepnum in range(10): + print(f'[RUNSUITE_REPEAT] {os.getcwd()} :: {[os.path.exists("nbclassic"), os.path.exists("nbclassic/tests"), os.path.exists("nbclassic/tests/end_to_end"), os.path.exists("tools/runsuite_repeat.py")]}') proc = subprocess.run('pytest -sv nbclassic/tests/end_to_end') print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> {"Success" if proc.returncode == 0 else proc.returncode}\n') From d677d6658eef6cc6f2b75f1f6e129a665a20b070 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 14:03:19 -0400 Subject: [PATCH 126/131] More workflow edits. --- tools/runsuite_repeat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/runsuite_repeat.py b/tools/runsuite_repeat.py index 536166124..55443cec8 100644 --- a/tools/runsuite_repeat.py +++ b/tools/runsuite_repeat.py @@ -9,7 +9,11 @@ def run(): for stepnum in range(10): print(f'[RUNSUITE_REPEAT] {os.getcwd()} :: {[os.path.exists("nbclassic"), os.path.exists("nbclassic/tests"), os.path.exists("nbclassic/tests/end_to_end"), os.path.exists("tools/runsuite_repeat.py")]}') - proc = subprocess.run('pytest -sv nbclassic/tests/end_to_end') + try: + proc = subprocess.run('pytest -sv tests/end_to_end') + except Exception: + print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> Exception') + continue print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> {"Success" if proc.returncode == 0 else proc.returncode}\n') From d3bcdc253bde154296261ca2283811f7aa84e70a Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 20 Oct 2022 15:58:17 -0400 Subject: [PATCH 127/131] Revised timeout on test_kernel_menu, WIP workflow edits. --- .github/workflows/playwright.yml | 4 ++-- nbclassic/tests/end_to_end/test_kernel_menu.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 67cdb338a..3abd806d7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,6 +36,6 @@ jobs: run: | python tools/install_pydeps.py - - name: Run Suite X Times + - name: Run Playwright Tests run: | - python tools/runsuite_repeat.py + pytest -sv nbclassic/tests/end_to_end diff --git a/nbclassic/tests/end_to_end/test_kernel_menu.py b/nbclassic/tests/end_to_end/test_kernel_menu.py index 66677dcfe..ded44d468 100644 --- a/nbclassic/tests/end_to_end/test_kernel_menu.py +++ b/nbclassic/tests/end_to_end/test_kernel_menu.py @@ -49,4 +49,8 @@ def test_menu_items(notebook_frontend): kernel_menu.click() notebook_frontend.wait_for_selector(menu_item, EDITOR_PAGE).click() - notebook_frontend.wait_for_condition(lambda: notebook_frontend.is_kernel_running()) + notebook_frontend.wait_for_condition( + lambda: notebook_frontend.is_kernel_running(), + timeout=120, + period=5 + ) From 3525185963e5ab0bd178665708b2651866dd5d9d Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 21 Oct 2022 10:09:54 -0400 Subject: [PATCH 128/131] Updated workflow files and the test prototyper. --- .github/workflows/check-release.yml | 57 ++++++++ .github/workflows/docs.yml | 52 +++++++ .github/workflows/downstream.yml | 32 +++++ .github/workflows/enforce-label.yml | 11 ++ .github/workflows/js.yml | 67 +++++++++ .github/workflows/pythonpackage.yml | 128 ++++++++++++++++++ .../end_to_end/manual_test_prototyper.py | 32 +++-- tools/runsuite_repeat.py | 7 +- 8 files changed, 367 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/check-release.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/downstream.yml create mode 100644 .github/workflows/enforce-label.yml create mode 100644 .github/workflows/js.yml create mode 100644 .github/workflows/pythonpackage.yml diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml new file mode 100644 index 000000000..3b2a1ed3e --- /dev/null +++ b/.github/workflows/check-release.yml @@ -0,0 +1,57 @@ +name: Check Release +on: + push: + branches: ["master"] + pull_request: + branches: ["*"] + +jobs: + check_release: + runs-on: ubuntu-latest + strategy: + matrix: + group: [check_release, link_check] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + architecture: "x64" + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Cache pip + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}-pip- + - name: Cache checked links + if: ${{ matrix.group == 'link_check' }} + uses: actions/cache@v2 + with: + path: ~/.cache/pytest-link-check + key: ${{ runner.os }}-linkcheck-${{ hashFiles('**/*.md', '**/*.rst') }}-md-links + restore-keys: | + ${{ runner.os }}-linkcheck- + - name: Upgrade packaging dependencies + run: | + pip install --upgrade pip setuptools wheel --user + - name: Install Dependencies + run: | + pip install -e . + - name: Check Release + if: ${{ matrix.group == 'check_release' }} + uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 + with: + version_spec: 100.100.100 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Run Link Check + if: ${{ matrix.group == 'link_check' }} + uses: jupyter-server/jupyter_releaser/.github/actions/check-links@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..20f4e96b5 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,52 @@ +name: Docs Tests +on: + push: + branches: '*' + pull_request: + branches: '*' +jobs: + build: + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu] + python-version: [ '3.7' ] + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Upgrade packaging dependencies + run: | + pip install --upgrade pip setuptools wheel + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Cache pip + uses: actions/cache@v1 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + - name: Install the Python dependencies + run: | + pip install -e .[test] codecov + pip install -r docs/doc-requirements.txt + wget https://github.com/jgm/pandoc/releases/download/1.19.1/pandoc-1.19.1-1-amd64.deb && sudo dpkg -i pandoc-1.19.1-1-amd64.deb + - name: List installed packages + run: | + pip freeze + pip check + - name: Run tests on documentation + run: | + EXIT_STATUS=0 + make -C docs/ html || EXIT_STATUS=$? + cd docs/source && pytest --nbval --current-env .. || EXIT_STATUS=$? + exit $EXIT_STATUS diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml new file mode 100644 index 000000000..c58d21adb --- /dev/null +++ b/.github/workflows/downstream.yml @@ -0,0 +1,32 @@ +name: Test Downstream + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + downstream: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Test jupyterlab_server + uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 + with: + package_name: jupyterlab_server + + - name: Test jupyterlab + uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 + with: + package_name: jupyterlab + package_spec: "\".[test]\"" + test_command: "python -m jupyterlab.browser_check --no-browser-test" + diff --git a/.github/workflows/enforce-label.yml b/.github/workflows/enforce-label.yml new file mode 100644 index 000000000..354a0468d --- /dev/null +++ b/.github/workflows/enforce-label.yml @@ -0,0 +1,11 @@ +name: Enforce PR label + +on: + pull_request: + types: [labeled, unlabeled, opened, edited, synchronize] +jobs: + enforce-label: + runs-on: ubuntu-latest + steps: + - name: enforce-triage-label + uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml new file mode 100644 index 000000000..b215521dd --- /dev/null +++ b/.github/workflows/js.yml @@ -0,0 +1,67 @@ +name: Linux JS Tests + +on: + push: + branches: '*' + pull_request: + branches: '*' + +jobs: + build: + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos] + group: [notebook, base, services] + exclude: + - group: services + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Set up Node + uses: actions/setup-node@v1 + with: + node-version: '12.x' + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Cache pip on Linux + uses: actions/cache@v1 + if: startsWith(runner.os, 'Linux') + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python }}-${{ hashFiles('**/requirements.txt', 'setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python }} + + - name: Temporary workaround for sanitizer loading in JS Tests + run: | + cp tools/security_deprecated.js nbclassic/static/base/js/security.js + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install --upgrade setuptools wheel + npm install + npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7 + pip install .[test] + + - name: Run Tests + run: | + python -m nbclassic.jstest ${{ matrix.group }} diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 000000000..eac1ce0b9 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,128 @@ +name: Testing nbclassic + +on: + push: + branches: + - master + pull_request: + +jobs: + build: + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"] + exclude: + - os: windows + python-version: pypy-3.8 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install pip dependencies + run: | + pip install -v -e ".[test]" pytest-cov + - name: Check pip environment + run: | + pip freeze + pip check + - name: Run the help command + run: | + jupyter nbclassic -h + - name: Test with pytest and coverage + if: ${{ matrix.python-version != 'pypy-3.8' }} + run: | + python -m pytest -vv --cov=nbclassic --cov-report term-missing:skip-covered || python -m pytest -vv --cov=nbclassic --cov-report term-missing:skip-covered + - name: Run the tests on pypy + if: ${{ matrix.python-version == 'pypy-3.8' }} + run: | + python -m pytest -vv || python -m pytest -vv -lf + - name: Test Running Server + if: startsWith(runner.os, 'Linux') + run: | + jupyter nbclassic --no-browser & + TASK_PID=$! + # Make sure the task is running + ps -p $TASK_PID || exit 1 + sleep 5 + kill $TASK_PID + wait $TASK_PID + +# test_miniumum_versions: +# name: Test Minimum Versions +# timeout-minutes: 20 +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - name: Base Setup +# uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 +# with: +# python_version: "3.7" +# - name: Install miniumum versions +# uses: jupyterlab/maintainer-tools/.github/actions/install-minimums@v1 +# - name: Run the unit tests +# run: pytest -vv || pytest -vv --lf + + test_prereleases: + name: Test Prereleases + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Install the Python dependencies + run: | + pip install --pre -e ".[test]" + - name: List installed packages + run: | + pip freeze + pip check + - name: Run the tests + run: | + pytest -vv || pytest -vv --lf + + make_sdist: + name: Make SDist + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v2 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Build SDist + run: | + pip install build + python -m build --sdist + - uses: actions/upload-artifact@v2 + with: + name: "sdist" + path: dist/*.tar.gz + + test_sdist: + runs-on: ubuntu-latest + needs: [make_sdist] + name: Install from SDist and Test + timeout-minutes: 20 + steps: + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Download sdist + uses: actions/download-artifact@v2 + - name: Install From SDist + run: | + set -ex + cd sdist + mkdir test + tar --strip-components=1 -zxvf *.tar.gz -C ./test + cd test + pip install -e .[test] + pip install pytest-github-actions-annotate-failures + - name: Run Test + run: | + cd sdist/test + pytest -vv || pytest -vv --lf diff --git a/nbclassic/tests/end_to_end/manual_test_prototyper.py b/nbclassic/tests/end_to_end/manual_test_prototyper.py index 7121a4328..8ac816efa 100644 --- a/nbclassic/tests/end_to_end/manual_test_prototyper.py +++ b/nbclassic/tests/end_to_end/manual_test_prototyper.py @@ -11,18 +11,20 @@ from .utils import TREE_PAGE, EDITOR_PAGE -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() +# # 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/tools/runsuite_repeat.py b/tools/runsuite_repeat.py index 55443cec8..b8b4b821d 100644 --- a/tools/runsuite_repeat.py +++ b/tools/runsuite_repeat.py @@ -8,13 +8,12 @@ def run(): for stepnum in range(10): - print(f'[RUNSUITE_REPEAT] {os.getcwd()} :: {[os.path.exists("nbclassic"), os.path.exists("nbclassic/tests"), os.path.exists("nbclassic/tests/end_to_end"), os.path.exists("tools/runsuite_repeat.py")]}') try: - proc = subprocess.run('pytest -sv tests/end_to_end') + proc = subprocess.run(['pytest', '-sv', 'nbclassic/tests/end_to_end']) except Exception: - print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> Exception') + print(f'\n[RUNSUITE_REPEAT] Exception -> Run {stepnum}\n') continue - print(f'\n[RUNSUITE_REPEAT] Run {stepnum} -> {"Success" if proc.returncode == 0 else proc.returncode}\n') + print(f'\n[RUNSUITE_REPEAT] {"Success" if proc.returncode == 0 else proc.returncode} -> Run {stepnum}\n') if __name__ == '__main__': From e668ffd6d05a8a66982c9538ec4e308f643f7ba5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 21 Oct 2022 11:30:36 -0400 Subject: [PATCH 129/131] Removed selenium tests. --- nbclassic/tests/selenium/__init__.py | 0 nbclassic/tests/selenium/conftest.py | 143 ------ nbclassic/tests/selenium/flaky-selenium.yml | 53 -- nbclassic/tests/selenium/quick_selenium.py | 53 -- nbclassic/tests/selenium/selenium.yml | 53 -- nbclassic/tests/selenium/test_buffering.py | 50 -- .../selenium/test_clipboard_multiselect.py | 27 - .../tests/selenium/test_dashboard_nav.py | 65 --- nbclassic/tests/selenium/test_deletecell.py | 57 --- .../tests/selenium/test_display_image.py | 65 --- .../tests/selenium/test_display_isolation.py | 94 ---- .../tests/selenium/test_dualmode_arrows.py | 103 ---- .../tests/selenium/test_dualmode_cellmode.py | 58 --- .../tests/selenium/test_dualmode_clipboard.py | 54 -- .../tests/selenium/test_dualmode_execute.py | 74 --- .../selenium/test_dualmode_insertcell.py | 51 -- .../tests/selenium/test_dualmode_markdown.py | 53 -- nbclassic/tests/selenium/test_execute_code.py | 66 --- .../tests/selenium/test_find_and_replace.py | 16 - nbclassic/tests/selenium/test_interrupt.py | 36 -- nbclassic/tests/selenium/test_kernel_menu.py | 59 --- nbclassic/tests/selenium/test_markdown.py | 41 -- nbclassic/tests/selenium/test_merge_cells.py | 36 -- .../selenium/test_move_multiselection.py | 47 -- nbclassic/tests/selenium/test_multiselect.py | 63 --- .../tests/selenium/test_multiselect_toggle.py | 43 -- .../tests/selenium/test_notifications.py | 103 ---- .../tests/selenium/test_prompt_numbers.py | 29 -- nbclassic/tests/selenium/test_save.py | 64 --- .../tests/selenium/test_save_as_notebook.py | 40 -- .../tests/selenium/test_save_readonly_as.py | 80 --- nbclassic/tests/selenium/test_shutdown.py | 15 - nbclassic/tests/selenium/test_undelete.py | 92 ---- nbclassic/tests/selenium/utils.py | 468 ------------------ 34 files changed, 2351 deletions(-) delete mode 100644 nbclassic/tests/selenium/__init__.py delete mode 100644 nbclassic/tests/selenium/conftest.py delete mode 100644 nbclassic/tests/selenium/flaky-selenium.yml delete mode 100644 nbclassic/tests/selenium/quick_selenium.py delete mode 100644 nbclassic/tests/selenium/selenium.yml delete mode 100644 nbclassic/tests/selenium/test_buffering.py delete mode 100644 nbclassic/tests/selenium/test_clipboard_multiselect.py delete mode 100644 nbclassic/tests/selenium/test_dashboard_nav.py delete mode 100644 nbclassic/tests/selenium/test_deletecell.py delete mode 100644 nbclassic/tests/selenium/test_display_image.py delete mode 100644 nbclassic/tests/selenium/test_display_isolation.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_arrows.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_cellmode.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_clipboard.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_execute.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_insertcell.py delete mode 100644 nbclassic/tests/selenium/test_dualmode_markdown.py delete mode 100644 nbclassic/tests/selenium/test_execute_code.py delete mode 100755 nbclassic/tests/selenium/test_find_and_replace.py delete mode 100644 nbclassic/tests/selenium/test_interrupt.py delete mode 100644 nbclassic/tests/selenium/test_kernel_menu.py delete mode 100644 nbclassic/tests/selenium/test_markdown.py delete mode 100644 nbclassic/tests/selenium/test_merge_cells.py delete mode 100644 nbclassic/tests/selenium/test_move_multiselection.py delete mode 100644 nbclassic/tests/selenium/test_multiselect.py delete mode 100644 nbclassic/tests/selenium/test_multiselect_toggle.py delete mode 100644 nbclassic/tests/selenium/test_notifications.py delete mode 100755 nbclassic/tests/selenium/test_prompt_numbers.py delete mode 100644 nbclassic/tests/selenium/test_save.py delete mode 100644 nbclassic/tests/selenium/test_save_as_notebook.py delete mode 100644 nbclassic/tests/selenium/test_save_readonly_as.py delete mode 100644 nbclassic/tests/selenium/test_shutdown.py delete mode 100644 nbclassic/tests/selenium/test_undelete.py delete mode 100644 nbclassic/tests/selenium/utils.py diff --git a/nbclassic/tests/selenium/__init__.py b/nbclassic/tests/selenium/__init__.py deleted file mode 100644 index e69de29bb..000000000 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/flaky-selenium.yml b/nbclassic/tests/selenium/flaky-selenium.yml deleted file mode 100644 index 2a418dc96..000000000 --- a/nbclassic/tests/selenium/flaky-selenium.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Flaky 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'] - exclude: - - os: ubuntu - python-version: '3.7' - - os: ubuntu - python-version: '3.9' - - os: macos - python-version: '3.8' - 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/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/selenium.yml b/nbclassic/tests/selenium/selenium.yml deleted file mode 100644 index 83086c427..000000000 --- a/nbclassic/tests/selenium/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/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_display_isolation.py b/nbclassic/tests/selenium/test_display_isolation.py deleted file mode 100644 index 51ca082bc..000000000 --- a/nbclassic/tests/selenium/test_display_isolation.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Test display isolation. - -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): - 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) - try: - isolated_html(notebook) - isolated_svg(notebook) - finally: - # Ensure we switch from iframe back to default content even if test fails - notebook.browser.switch_to.default_content() - - -def isolated_html(notebook): - """Test HTML display isolation. - - HTML styling rendered without isolation will affect the whole - document, whereas styling applied with isolation will affect only - the local display object. - """ - red = 'rgb(255, 0, 0)' - blue = 'rgb(0, 0, 255)' - test_str = "
Should turn red from non-isolation
" - notebook.add_and_execute_cell(content=f"display(HTML({test_str!r}))") - non_isolated = ( - f"" - f"
Should be red
") - display_ni = f"display(HTML({non_isolated!r}), metadata={{'isolated':False}})" - notebook.add_and_execute_cell(content=display_ni) - isolated = ( - f"" - f"
Should be blue
") - 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 - - # 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 - - # 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) - - -def isolated_svg(notebook): - """Test that multiple isolated SVGs have different scopes. - - Asserts that there no CSS leaks between two isolated SVGs. - """ - yellow = "rgb(255, 255, 0)" - black = "rgb(0, 0, 0)" - svg_1_str = f"""s1 = ''''''""" - svg_2_str = """s2 = ''''''""" - - notebook.add_and_execute_cell(content=svg_1_str) - notebook.add_and_execute_cell(content=svg_2_str) - notebook.add_and_execute_cell( - 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) - - # 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() - - # 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 - - # Clean up the svg test cells - for i in range(1, len(notebook.cells)): - notebook.delete_cell(1) 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_markdown.py b/nbclassic/tests/selenium/test_markdown.py deleted file mode 100644 index cae1a7a03..000000000 --- a/nbclassic/tests/selenium/test_markdown.py +++ /dev/null @@ -1,41 +0,0 @@ -from nbformat.v4 import new_markdown_cell - -def get_rendered_contents(nb): - cl = ["text_cell", "render"] - rendered_cells = [cell.find_element_by_class_name("text_cell_render") - for cell in nb.cells - if all([c in cell.get_attribute("class") for c in cl])] - return [x.get_attribute('innerHTML').strip() - for x in rendered_cells - if x is not None] - - -def test_markdown_cell(prefill_notebook): - nb = 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) == [ - '

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""" - 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) - 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_prompt_numbers.py b/nbclassic/tests/selenium/test_prompt_numbers.py deleted file mode 100755 index 38872b855..000000000 --- a/nbclassic/tests/selenium/test_prompt_numbers.py +++ /dev/null @@ -1,29 +0,0 @@ -def test_prompt_numbers(prefill_notebook): - notebook = 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() - ) - - def set_prompt(value): - notebook.set_cell_input_prompt(0, value) - - assert get_prompt() == "In [ ]:" - - set_prompt(2) - assert get_prompt() == "In [2]:" - - set_prompt(0) - assert get_prompt() == "In [0]:" - - set_prompt("'*'") - assert get_prompt() == "In [*]:" - - set_prompt("undefined") - assert get_prompt() == "In [ ]:" - - set_prompt("null") - assert get_prompt() == "In [ ]:" 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/test_undelete.py b/nbclassic/tests/selenium/test_undelete.py deleted file mode 100644 index d9823cda5..000000000 --- a/nbclassic/tests/selenium/test_undelete.py +++ /dev/null @@ -1,92 +0,0 @@ -from selenium.webdriver.common.keys import Keys -from .utils import shift - -def undelete(nb): - nb.browser.execute_script('Jupyter.notebook.undelete_cell();') - -INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")', 'print("d")'] - -def test_undelete_cells(prefill_notebook): - notebook = prefill_notebook(INITIAL_CELLS) - a, b, c, d = INITIAL_CELLS - - # Verify initial state - assert notebook.get_cells_contents() == [a, b, c, d] - - # Delete cells [1, 2] - notebook.focus_cell(1) - shift(notebook.browser, Keys.DOWN) - notebook.current_cell.send_keys('dd') - assert notebook.get_cells_contents() == [a, d] - - # Delete new cell 1 (which contains d) - notebook.focus_cell(1) - notebook.current_cell.send_keys('dd') - assert notebook.get_cells_contents() == [a] - - # Undelete d - undelete(notebook) - assert notebook.get_cells_contents() == [a, d] - - # Undelete b, c - undelete(notebook) - assert notebook.get_cells_contents() == [a, b, c, d] - - # Nothing more to undelete - undelete(notebook) - assert notebook.get_cells_contents() == [a, b, c, d] - - # Delete first two cells and restore - notebook.focus_cell(0) - shift(notebook.browser, Keys.DOWN) - notebook.current_cell.send_keys('dd') - 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') - assert notebook.get_cells_contents() == [a, b] - undelete(notebook) - assert notebook.get_cells_contents() == [a, b, c, d] - - # 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') - assert notebook.get_cells_contents() == [a, bc, d] - undelete(notebook) - assert notebook.get_cells_contents() == [a, bc, c, d] - - # 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') - assert notebook.get_cells_contents() == [a, bc, cd] - undelete(notebook) - assert notebook.get_cells_contents() == [a, bc, cd, d] - - # Reset contents to [a, b, c, d] -------------------------------------- - notebook.edit_cell(index=1, content=b) - notebook.edit_cell(index=2, content=c) - assert notebook.get_cells_contents() == [a, b, c, d] - - # 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();") - assert notebook.get_cells_contents() == [ab, c, d] - undelete(notebook) - assert notebook.get_cells_contents() == [ab, b, c, d] - - # 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();") - assert notebook.get_cells_contents() == [ab, b, cd] - undelete(notebook) - assert notebook.get_cells_contents() == [ab, b, c, cd] 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 From 328a8c70d3765842bdc61bb0fb71cec5bdf3ea2a Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 21 Oct 2022 12:27:03 -0400 Subject: [PATCH 130/131] Added macos back to action, added command printout to install script, cleanup. --- .github/workflows/playwright.yml | 2 +- nbclassic/tests/end_to_end/utils.py | 1 - tools/install_pydeps.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3abd806d7..764137dfb 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu] + os: [ubuntu, macos] python-version: [ '3.7', '3.8', '3.9', '3.10'] steps: - name: Checkout diff --git a/nbclassic/tests/end_to_end/utils.py b/nbclassic/tests/end_to_end/utils.py index c50a022ae..4ed698f4b 100644 --- a/nbclassic/tests/end_to_end/utils.py +++ b/nbclassic/tests/end_to_end/utils.py @@ -216,7 +216,6 @@ class NotebookFrontend: TODO: Possible future improvements, current limitations, etc - - Known bad things, blah blah """ # Some constants for users of the class diff --git a/tools/install_pydeps.py b/tools/install_pydeps.py index 580499e8b..b25dc5c55 100644 --- a/tools/install_pydeps.py +++ b/tools/install_pydeps.py @@ -36,6 +36,7 @@ def run(): } 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) From d3e5a81d2caf605319f315a2709af2ffe33b8e0e Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 21 Oct 2022 13:03:34 -0400 Subject: [PATCH 131/131] Removed selenium from setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 90d400cc5..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-playwright', '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'],