Skip to content

Commit cab2ea5

Browse files
authored
Merge pull request #170 from ericsnekbytes/playwright_conversion
Playwright Testing Conversion
2 parents 9a61b18 + d3e5a81 commit cab2ea5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2698
-2127
lines changed

.github/workflows/flaky-selenium.yml renamed to .github/workflows/playwright.yml

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Flaky Selenium Tests
1+
name: Playwright Tests
22

33
on:
44
push:
@@ -12,14 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
os: [ubuntu, macos]
15-
python-version: [ '3.7', '3.8', '3.9']
16-
exclude:
17-
- os: ubuntu
18-
python-version: '3.7'
19-
- os: ubuntu
20-
python-version: '3.9'
21-
- os: macos
22-
python-version: '3.8'
15+
python-version: [ '3.7', '3.8', '3.9', '3.10']
2316
steps:
2417
- name: Checkout
2518
uses: actions/checkout@v2
@@ -41,13 +34,8 @@ jobs:
4134
4235
- name: Install Python dependencies
4336
run: |
44-
python -m pip install -U pip setuptools wheel
45-
pip install --upgrade selenium
46-
pip install pytest
47-
pip install .[test]
37+
python tools/install_pydeps.py
4838
49-
- name: Run Tests
39+
- name: Run Playwright Tests
5040
run: |
51-
export JUPYTER_TEST_BROWSER=firefox
52-
export MOZ_HEADLESS=1
53-
pytest -sv nbclassic/tests/selenium
41+
pytest -sv nbclassic/tests/end_to_end

.github/workflows/selenium.yml

Lines changed: 0 additions & 53 deletions
This file was deleted.
File renamed without changes.
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""Fixtures for pytest/playwright end_to_end tests."""
2+
3+
4+
import datetime
5+
import os
6+
import json
7+
import sys
8+
import time
9+
from os.path import join as pjoin
10+
from subprocess import Popen
11+
from tempfile import mkstemp
12+
from urllib.parse import urljoin
13+
14+
import pytest
15+
import requests
16+
from testpath.tempdir import TemporaryDirectory
17+
18+
import nbformat
19+
from nbformat.v4 import new_notebook, new_code_cell
20+
from .utils import NotebookFrontend, BROWSER_CONTEXT, BROWSER_OBJ, TREE_PAGE, SERVER_INFO
21+
22+
23+
def _wait_for_server(proc, info_file_path):
24+
"""Wait 30 seconds for the notebook server to start"""
25+
for i in range(300):
26+
if proc.poll() is not None:
27+
raise RuntimeError("Notebook server failed to start")
28+
if os.path.exists(info_file_path):
29+
try:
30+
with open(info_file_path) as f:
31+
return json.load(f)
32+
except ValueError:
33+
# If the server is halfway through writing the file, we may
34+
# get invalid JSON; it should be ready next iteration.
35+
pass
36+
time.sleep(0.1)
37+
raise RuntimeError("Didn't find %s in 30 seconds", info_file_path)
38+
39+
40+
@pytest.fixture(scope='function')
41+
def notebook_server():
42+
info = {}
43+
with TemporaryDirectory() as td:
44+
nbdir = info['nbdir'] = pjoin(td, 'notebooks')
45+
os.makedirs(pjoin(nbdir, 'sub ∂ir1', 'sub ∂ir 1a'))
46+
os.makedirs(pjoin(nbdir, 'sub ∂ir2', 'sub ∂ir 1b'))
47+
48+
info['extra_env'] = {
49+
'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'),
50+
'JUPYTER_RUNTIME_DIR': pjoin(td, 'jupyter_runtime'),
51+
'IPYTHONDIR': pjoin(td, 'ipython'),
52+
}
53+
env = os.environ.copy()
54+
env.update(info['extra_env'])
55+
56+
command = [sys.executable, '-m', 'nbclassic',
57+
'--no-browser',
58+
'--notebook-dir', nbdir,
59+
# run with a base URL that would be escaped,
60+
# to test that we don't double-escape URLs
61+
'--ServerApp.base_url=/a@b/',
62+
]
63+
print("command=", command)
64+
proc = info['popen'] = Popen(command, cwd=nbdir, env=env)
65+
info_file_path = pjoin(td, 'jupyter_runtime',
66+
f'jpserver-{proc.pid:d}.json')
67+
info.update(_wait_for_server(proc, info_file_path))
68+
69+
print("Notebook server info:", info)
70+
yield info
71+
72+
# Shut the server down
73+
requests.post(urljoin(info['url'], 'api/shutdown'),
74+
headers={'Authorization': 'token '+info['token']})
75+
76+
77+
@pytest.fixture(scope='function')
78+
def playwright_browser(playwright):
79+
start = datetime.datetime.now()
80+
while (datetime.datetime.now() - start).seconds < 30:
81+
try:
82+
if os.environ.get('JUPYTER_TEST_BROWSER') == 'chrome':
83+
browser = playwright.chromium.launch()
84+
else:
85+
browser = playwright.firefox.launch()
86+
break
87+
except Exception:
88+
time.sleep(.2)
89+
90+
yield browser
91+
92+
# Teardown
93+
browser.close()
94+
95+
96+
@pytest.fixture(scope='function')
97+
def authenticated_browser_data(playwright_browser, notebook_server):
98+
browser_obj = playwright_browser
99+
browser_context = browser_obj.new_context()
100+
browser_context.jupyter_server_info = notebook_server
101+
tree_page = browser_context.new_page()
102+
tree_page.goto("{url}?token={token}".format(**notebook_server))
103+
104+
auth_browser_data = {
105+
BROWSER_CONTEXT: browser_context,
106+
TREE_PAGE: tree_page,
107+
SERVER_INFO: notebook_server,
108+
BROWSER_OBJ: browser_obj,
109+
}
110+
111+
return auth_browser_data
112+
113+
114+
@pytest.fixture(scope='function')
115+
def notebook_frontend(authenticated_browser_data):
116+
yield NotebookFrontend.new_notebook_frontend(authenticated_browser_data)
117+
118+
119+
@pytest.fixture(scope='function')
120+
def prefill_notebook(playwright_browser, notebook_server):
121+
browser_obj = playwright_browser
122+
browser_context = browser_obj.new_context()
123+
# playwright_browser is the browser_context,
124+
# notebook_server is the server with directories
125+
126+
# the return of function inner takes in a dictionary of strings to populate cells
127+
def inner(cells):
128+
cells = [new_code_cell(c) if isinstance(c, str) else c
129+
for c in cells]
130+
# new_notebook is an nbformat function that is imported so that it can create a
131+
# notebook that is formatted as it needs to be
132+
nb = new_notebook(cells=cells)
133+
134+
# Create temporary file directory and store it's reference as well as the path
135+
fd, path = mkstemp(dir=notebook_server['nbdir'], suffix='.ipynb')
136+
137+
# Open the file and write the format onto the file
138+
with open(fd, 'w', encoding='utf-8') as f:
139+
nbformat.write(nb, f)
140+
141+
# Grab the name of the file
142+
fname = os.path.basename(path)
143+
144+
# Add the notebook server as a property of the playwright browser with the name jupyter_server_info
145+
browser_context.jupyter_server_info = notebook_server
146+
# Open a new page in the browser and refer to it as the tree page
147+
tree_page = browser_context.new_page()
148+
149+
# Navigate that page to the base URL page AKA the tree page
150+
tree_page.goto("{url}?token={token}".format(**notebook_server))
151+
152+
auth_browser_data = {
153+
BROWSER_CONTEXT: browser_context,
154+
TREE_PAGE: tree_page,
155+
SERVER_INFO: notebook_server,
156+
BROWSER_OBJ: browser_obj
157+
}
158+
159+
return NotebookFrontend.new_notebook_frontend(auth_browser_data, existing_file_name=fname)
160+
161+
# Return the function that will take in the dict of code strings
162+
return inner
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Test basic cell execution methods, related shortcuts, and error modes
2+
3+
Run this manually:
4+
# Normal pytest run
5+
pytest nbclassic/tests/end_to_end/test_interrupt.py
6+
# with playwright debug (run and poke around in the web console)
7+
PWDEBUG=1 pytest -s nbclassic/tests/end_to_end/test_interrupt.py
8+
"""
9+
10+
11+
from .utils import TREE_PAGE, EDITOR_PAGE
12+
13+
14+
# # Use/uncomment this for manual test prototytping
15+
# # (the test suite will run this if it's uncommented)
16+
# def test_do_something(notebook_frontend):
17+
# # Do something with the notebook_frontend here
18+
# notebook_frontend.add_cell()
19+
# notebook_frontend.add_cell()
20+
# assert len(notebook_frontend.cells) == 3
21+
#
22+
# notebook_frontend.delete_all_cells()
23+
# assert len(notebook_frontend.cells) == 1
24+
#
25+
# notebook_frontend.editor_page.pause()
26+
# cell_texts = ['aa = 1', 'bb = 2', 'cc = 3']
27+
# a, b, c = cell_texts
28+
# notebook_frontend.populate(cell_texts)
29+
# assert notebook_frontend.get_cells_contents() == [a, b, c]
30+
# notebook_frontend._pause()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests buffering of execution requests."""
2+
3+
4+
from .utils import TREE_PAGE, EDITOR_PAGE
5+
6+
7+
def test_kernels_buffer_without_conn(prefill_notebook):
8+
"""Test that execution request made while disconnected is buffered."""
9+
10+
notebook_frontend = prefill_notebook(["print(1 + 2)"])
11+
notebook_frontend.wait_for_kernel_ready()
12+
13+
notebook_frontend.evaluate("() => { IPython.notebook.kernel.stop_channels }", page=EDITOR_PAGE)
14+
notebook_frontend.execute_cell(0)
15+
16+
notebook_frontend.evaluate("() => { IPython.notebook.kernel.reconnect }", page=EDITOR_PAGE)
17+
notebook_frontend.wait_for_kernel_ready()
18+
19+
outputs = notebook_frontend.wait_for_cell_output(0)
20+
assert outputs.get_inner_text().strip() == '3'
21+
22+
23+
def test_buffered_cells_execute_in_order(prefill_notebook):
24+
"""Test that buffered requests execute in order."""
25+
26+
notebook_frontend = prefill_notebook(['', 'k=1', 'k+=1', 'k*=3', 'print(k)'])
27+
28+
# Repeated execution of cell queued up in the kernel executes
29+
# each execution request in order.
30+
notebook_frontend.wait_for_kernel_ready()
31+
notebook_frontend.evaluate("() => IPython.notebook.kernel.stop_channels();", page=EDITOR_PAGE)
32+
# k == 1
33+
notebook_frontend.execute_cell(1)
34+
# k == 2
35+
notebook_frontend.execute_cell(2)
36+
# k == 6
37+
notebook_frontend.execute_cell(3)
38+
# k == 7
39+
notebook_frontend.execute_cell(2)
40+
notebook_frontend.execute_cell(4)
41+
notebook_frontend.evaluate("() => IPython.notebook.kernel.reconnect();", page=EDITOR_PAGE)
42+
notebook_frontend.wait_for_kernel_ready()
43+
44+
outputs = notebook_frontend.wait_for_cell_output(4)
45+
assert outputs.get_inner_text().strip() == '7'
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Tests clipboard by copying, cutting and pasting multiple cells"""
2+
3+
4+
from .utils import TREE_PAGE, EDITOR_PAGE
5+
6+
7+
# Optionally perfom this test with Ctrl+c and Ctrl+v
8+
def test_clipboard_multiselect(prefill_notebook):
9+
notebook = prefill_notebook(['', '1', '2', '3', '4', '5a', '6b', '7c', '8d'])
10+
11+
assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '6b', '7c', '8d']
12+
13+
# Copy the first 3 cells
14+
# Paste the values copied from the first three cells into the last 3 cells
15+
16+
# Selecting the fist 3 cells
17+
notebook.select_cell_range(1, 3)
18+
19+
# Copy those selected cells
20+
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
21+
notebook.try_click_selector('//*[@id="copy_cell"]/a/span[1]', page=EDITOR_PAGE)
22+
23+
# Select the last 3 cells
24+
notebook.select_cell_range(6, 8)
25+
26+
# Paste the cells in clipboard onto selected cells
27+
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
28+
notebook.try_click_selector('//*[@id="paste_cell_replace"]/a', page=EDITOR_PAGE)
29+
30+
assert notebook.get_cells_contents() == ['', '1', '2', '3', '4', '5a', '1', '2', '3']
31+
32+
# Select the last four cells, cut them and paste them below the first cell
33+
34+
# Select the last 4 cells
35+
notebook.select_cell_range(5, 8)
36+
37+
# Click Edit button and the select cut button
38+
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
39+
notebook.try_click_selector('//*[@id="cut_cell"]/a', page=EDITOR_PAGE)
40+
41+
# Select the first cell
42+
notebook.select_cell_range(0, 0)
43+
44+
# Paste the cells in our clipboard below this first cell we are focused at
45+
notebook.try_click_selector('#editlink', page=EDITOR_PAGE)
46+
notebook.try_click_selector('//*[@id="paste_cell_below"]/a/span[1]', page=EDITOR_PAGE)
47+
48+
assert notebook.get_cells_contents() == ['', '5a', '1', '2', '3', '1', '2', '3', '4']

0 commit comments

Comments
 (0)