Skip to content

Commit

Permalink
Merge pull request #170 from ericsnekbytes/playwright_conversion
Browse files Browse the repository at this point in the history
Playwright Testing Conversion
  • Loading branch information
echarles committed Oct 22, 2022
2 parents 9a61b18 + d3e5a81 commit cab2ea5
Show file tree
Hide file tree
Showing 64 changed files with 2,698 additions and 2,127 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Flaky Selenium Tests
name: Playwright Tests

on:
push:
Expand All @@ -12,14 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu, macos]
python-version: [ '3.7', '3.8', '3.9']
exclude:
- os: ubuntu
python-version: '3.7'
- os: ubuntu
python-version: '3.9'
- os: macos
python-version: '3.8'
python-version: [ '3.7', '3.8', '3.9', '3.10']
steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -41,13 +34,8 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install -U pip setuptools wheel
pip install --upgrade selenium
pip install pytest
pip install .[test]
python tools/install_pydeps.py
- name: Run Tests
- name: Run Playwright Tests
run: |
export JUPYTER_TEST_BROWSER=firefox
export MOZ_HEADLESS=1
pytest -sv nbclassic/tests/selenium
pytest -sv nbclassic/tests/end_to_end
53 changes: 0 additions & 53 deletions .github/workflows/selenium.yml

This file was deleted.

File renamed without changes.
162 changes: 162 additions & 0 deletions nbclassic/tests/end_to_end/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Fixtures for pytest/playwright end_to_end tests."""


import datetime
import os
import json
import sys
import time
from os.path import join as pjoin
from subprocess import Popen
from tempfile import mkstemp
from urllib.parse import urljoin

import pytest
import requests
from testpath.tempdir import TemporaryDirectory

import nbformat
from nbformat.v4 import new_notebook, new_code_cell
from .utils import NotebookFrontend, BROWSER_CONTEXT, BROWSER_OBJ, TREE_PAGE, SERVER_INFO


def _wait_for_server(proc, info_file_path):
"""Wait 30 seconds for the notebook server to start"""
for i in range(300):
if proc.poll() is not None:
raise RuntimeError("Notebook server failed to start")
if os.path.exists(info_file_path):
try:
with open(info_file_path) as f:
return json.load(f)
except ValueError:
# If the server is halfway through writing the file, we may
# get invalid JSON; it should be ready next iteration.
pass
time.sleep(0.1)
raise RuntimeError("Didn't find %s in 30 seconds", info_file_path)


@pytest.fixture(scope='function')
def notebook_server():
info = {}
with TemporaryDirectory() as td:
nbdir = info['nbdir'] = pjoin(td, 'notebooks')
os.makedirs(pjoin(nbdir, 'sub ∂ir1', 'sub ∂ir 1a'))
os.makedirs(pjoin(nbdir, 'sub ∂ir2', 'sub ∂ir 1b'))

info['extra_env'] = {
'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'),
'JUPYTER_RUNTIME_DIR': pjoin(td, 'jupyter_runtime'),
'IPYTHONDIR': pjoin(td, 'ipython'),
}
env = os.environ.copy()
env.update(info['extra_env'])

command = [sys.executable, '-m', 'nbclassic',
'--no-browser',
'--notebook-dir', nbdir,
# run with a base URL that would be escaped,
# to test that we don't double-escape URLs
'--ServerApp.base_url=/a@b/',
]
print("command=", command)
proc = info['popen'] = Popen(command, cwd=nbdir, env=env)
info_file_path = pjoin(td, 'jupyter_runtime',
f'jpserver-{proc.pid:d}.json')
info.update(_wait_for_server(proc, info_file_path))

print("Notebook server info:", info)
yield info

# Shut the server down
requests.post(urljoin(info['url'], 'api/shutdown'),
headers={'Authorization': 'token '+info['token']})


@pytest.fixture(scope='function')
def playwright_browser(playwright):
start = datetime.datetime.now()
while (datetime.datetime.now() - start).seconds < 30:
try:
if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome':
browser = playwright.chromium.launch()
else:
browser = playwright.firefox.launch()
break
except Exception:
time.sleep(.2)

yield browser

# Teardown
browser.close()


@pytest.fixture(scope='function')
def authenticated_browser_data(playwright_browser, notebook_server):
browser_obj = playwright_browser
browser_context = browser_obj.new_context()
browser_context.jupyter_server_info = notebook_server
tree_page = browser_context.new_page()
tree_page.goto("{url}?token={token}".format(**notebook_server))

auth_browser_data = {
BROWSER_CONTEXT: browser_context,
TREE_PAGE: tree_page,
SERVER_INFO: notebook_server,
BROWSER_OBJ: browser_obj,
}

return auth_browser_data


@pytest.fixture(scope='function')
def notebook_frontend(authenticated_browser_data):
yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data)


@pytest.fixture(scope='function')
def prefill_notebook(playwright_browser, notebook_server):
browser_obj = playwright_browser
browser_context = browser_obj.new_context()
# playwright_browser is the browser_context,
# notebook_server is the server with directories

# the return of function inner takes in a dictionary of strings to populate cells
def inner(cells):
cells = [new_code_cell(c) if isinstance(c, str) else c
for c in cells]
# new_notebook is an nbformat function that is imported so that it can create a
# notebook that is formatted as it needs to be
nb = new_notebook(cells=cells)

# Create temporary file directory and store it's reference as well as the path
fd, path = mkstemp(dir=notebook_server['nbdir'], suffix='.ipynb')

# Open the file and write the format onto the file
with open(fd, 'w', encoding='utf-8') as f:
nbformat.write(nb, f)

# Grab the name of the file
fname = os.path.basename(path)

# Add the notebook server as a property of the playwright browser with the name jupyter_server_info
browser_context.jupyter_server_info = notebook_server
# Open a new page in the browser and refer to it as the tree page
tree_page = browser_context.new_page()

# Navigate that page to the base URL page AKA the tree page
tree_page.goto("{url}?token={token}".format(**notebook_server))

auth_browser_data = {
BROWSER_CONTEXT: browser_context,
TREE_PAGE: tree_page,
SERVER_INFO: notebook_server,
BROWSER_OBJ: browser_obj
}

return NotebookFrontend.new_notebook_frontend(auth_browser_data, existing_file_name=fname)

# Return the function that will take in the dict of code strings
return inner
30 changes: 30 additions & 0 deletions nbclassic/tests/end_to_end/manual_test_prototyper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Test basic cell execution methods, related shortcuts, and error modes
Run this manually:
# Normal pytest run
pytest nbclassic/tests/end_to_end/test_interrupt.py
# with playwright debug (run and poke around in the web console)
PWDEBUG=1 pytest -s nbclassic/tests/end_to_end/test_interrupt.py
"""


from .utils import TREE_PAGE, EDITOR_PAGE


# # Use/uncomment this for manual test prototytping
# # (the test suite will run this if it's uncommented)
# def test_do_something(notebook_frontend):
# # Do something with the notebook_frontend here
# notebook_frontend.add_cell()
# notebook_frontend.add_cell()
# assert len(notebook_frontend.cells) == 3
#
# notebook_frontend.delete_all_cells()
# assert len(notebook_frontend.cells) == 1
#
# notebook_frontend.editor_page.pause()
# cell_texts = ['aa = 1', 'bb = 2', 'cc = 3']
# a, b, c = cell_texts
# notebook_frontend.populate(cell_texts)
# assert notebook_frontend.get_cells_contents() == [a, b, c]
# notebook_frontend._pause()
45 changes: 45 additions & 0 deletions nbclassic/tests/end_to_end/test_buffering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Tests buffering of execution requests."""


from .utils import TREE_PAGE, EDITOR_PAGE


def test_kernels_buffer_without_conn(prefill_notebook):
"""Test that execution request made while disconnected is buffered."""

notebook_frontend = prefill_notebook(["print(1 + 2)"])
notebook_frontend.wait_for_kernel_ready()

notebook_frontend.evaluate("() => { IPython.notebook.kernel.stop_channels }", page=EDITOR_PAGE)
notebook_frontend.execute_cell(0)

notebook_frontend.evaluate("() => { IPython.notebook.kernel.reconnect }", page=EDITOR_PAGE)
notebook_frontend.wait_for_kernel_ready()

outputs = notebook_frontend.wait_for_cell_output(0)
assert outputs.get_inner_text().strip() == '3'


def test_buffered_cells_execute_in_order(prefill_notebook):
"""Test that buffered requests execute in order."""

notebook_frontend = prefill_notebook(['', 'k=1', 'k+=1', 'k*=3', 'print(k)'])

# Repeated execution of cell queued up in the kernel executes
# each execution request in order.
notebook_frontend.wait_for_kernel_ready()
notebook_frontend.evaluate("() => IPython.notebook.kernel.stop_channels();", page=EDITOR_PAGE)
# k == 1
notebook_frontend.execute_cell(1)
# k == 2
notebook_frontend.execute_cell(2)
# k == 6
notebook_frontend.execute_cell(3)
# k == 7
notebook_frontend.execute_cell(2)
notebook_frontend.execute_cell(4)
notebook_frontend.evaluate("() => IPython.notebook.kernel.reconnect();", page=EDITOR_PAGE)
notebook_frontend.wait_for_kernel_ready()

outputs = notebook_frontend.wait_for_cell_output(4)
assert outputs.get_inner_text().strip() == '7'
48 changes: 48 additions & 0 deletions nbclassic/tests/end_to_end/test_clipboard_multiselect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests clipboard by copying, cutting and pasting multiple cells"""


from .utils import TREE_PAGE, EDITOR_PAGE


# Optionally perfom this test with Ctrl+c and Ctrl+v
def test_clipboard_multiselect(prefill_notebook):
notebook = prefill_notebook(['', '1', '2', '3', '4', '5a', '6b', '7c', '8d'])

assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '6b', '7c', '8d']

# Copy the first 3 cells
# Paste the values copied from the first three cells into the last 3 cells

# Selecting the fist 3 cells
notebook.select_cell_range(1, 3)

# Copy those selected cells
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
notebook.try_click_selector('//*[@id="copy_cell"]/a/span[1]', page=EDITOR_PAGE)

# Select the last 3 cells
notebook.select_cell_range(6, 8)

# Paste the cells in clipboard onto selected cells
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
notebook.try_click_selector('//*[@id="paste_cell_replace"]/a', page=EDITOR_PAGE)

assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '1', '2', '3']

# Select the last four cells, cut them and paste them below the first cell

# Select the last 4 cells
notebook.select_cell_range(5, 8)

# Click Edit button and the select cut button
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
notebook.try_click_selector('//*[@id="cut_cell"]/a', page=EDITOR_PAGE)

# Select the first cell
notebook.select_cell_range(0, 0)

# Paste the cells in our clipboard below this first cell we are focused at
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
notebook.try_click_selector('//*[@id="paste_cell_below"]/a/span[1]', page=EDITOR_PAGE)

assert notebook.get_cells_contents() == ['', '5a', '1', '2', '3', '1', '2', '3', '4']
Loading

0 comments on commit cab2ea5

Please sign in to comment.