Skip to content

Commit 361eb1f

Browse files
CI Test Fixes Round 3 (#216)
* Refactors for saving and related utils updates. * Revised/fixed docstring.
1 parent e3cabee commit 361eb1f

File tree

3 files changed

+189
-35
lines changed

3 files changed

+189
-35
lines changed
Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,122 @@
1-
"""Test readonly notebook saved and renamed"""
1+
"""Test save-as functionality"""
22

33

4-
from .utils import EDITOR_PAGE
5-
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
4+
import traceback
5+
6+
from .utils import EDITOR_PAGE, EndToEndTimeout
67

78

89
def save_as(nb):
910
JS = '() => Jupyter.notebook.save_notebook_as()'
1011
return nb.evaluate(JS, page=EDITOR_PAGE)
1112

13+
1214
def get_notebook_name(nb):
1315
JS = '() => Jupyter.notebook.notebook_name'
1416
return nb.evaluate(JS, page=EDITOR_PAGE)
1517

18+
1619
def set_notebook_name(nb, name):
1720
JS = f'() => Jupyter.notebook.rename("{name}")'
1821
nb.evaluate(JS, page=EDITOR_PAGE)
1922

2023

21-
def test_save_notebook_as(notebook_frontend):
22-
set_notebook_name(notebook_frontend, name="nb1.ipynb")
24+
def test_save_as_nb(notebook_frontend):
25+
print('[Test] [test_save_readonly_as]')
26+
notebook_frontend.wait_for_kernel_ready()
27+
notebook_frontend.wait_for_selector(".input", page=EDITOR_PAGE)
2328

24-
notebook_frontend.locate('#notebook_name', page=EDITOR_PAGE)
25-
26-
assert get_notebook_name(notebook_frontend) == "nb1.ipynb"
29+
# Set a name for comparison later
30+
print('[Test] Set notebook name')
31+
set_notebook_name(notebook_frontend, name="nb1.ipynb")
32+
notebook_frontend.wait_for_condition(
33+
lambda: get_notebook_name(notebook_frontend) == 'nb1.ipynb',
34+
timeout=150,
35+
period=1
36+
)
2737

2838
# Wait for Save As modal, save
39+
print('[Test] Save')
2940
save_as(notebook_frontend)
30-
notebook_frontend.wait_for_selector('.save-message', page=EDITOR_PAGE)
3141

32-
# TODO: Add a function for locator assertions to FrontendElement
33-
locator_element = notebook_frontend.locate_and_focus('//input[@data-testid="save-as"]', page=EDITOR_PAGE)
34-
locator_element.wait_for('visible')
42+
# # Wait for modal to pop up
43+
print('[Test] Waiting for modal popup')
44+
notebook_frontend.wait_for_selector(".modal-footer", page=EDITOR_PAGE)
45+
dialog_element = notebook_frontend.locate(".modal-footer", page=EDITOR_PAGE)
46+
dialog_element.focus()
47+
48+
print('[Test] Focus the notebook name input field, then click and modify its .value')
49+
notebook_frontend.wait_for_selector('.modal-body .form-control', page=EDITOR_PAGE)
50+
name_input_element = notebook_frontend.locate('.modal-body .form-control', page=EDITOR_PAGE)
51+
name_input_element.focus()
52+
name_input_element.click()
53+
notebook_name = 'new_notebook.ipynb'
54+
55+
print('[Test] Begin attempts to fill the save dialog input and save the notebook')
56+
fill_attempts = 0
57+
58+
def attempt_form_fill_and_save():
59+
# Application behavior here is HIGHLY variable, we use this for repeated attempts
60+
# ....................
61+
# This may be a retry, check if the application state reflects a successful save operation
62+
nonlocal fill_attempts
63+
if fill_attempts and get_notebook_name(notebook_frontend) == "new_notebook.ipynb":
64+
print('[Test] Success from previous save attempt!')
65+
return True
66+
fill_attempts += 1
67+
print(f'[Test] Attempt form fill and save #{fill_attempts}')
68+
69+
# Make sure the save prompt is visible
70+
if not name_input_element.is_visible():
71+
save_as(notebook_frontend)
72+
name_input_element.wait_for('visible')
73+
74+
# Set the notebook name field in the save dialog
75+
print('[Test] Fill the input field')
76+
name_input_element.evaluate(f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}')
77+
notebook_frontend.wait_for_condition(
78+
lambda: name_input_element.evaluate(
79+
f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}') == 'new_notebook.ipynb',
80+
timeout=120,
81+
period=.25
82+
)
83+
# Show the input field value
84+
print('[Test] Name input field contents:')
85+
field_value = name_input_element.evaluate(f'(elem) => {{ return elem.value; }}')
86+
print('[Test] ' + field_value)
87+
if field_value != 'new_notebook.ipynb':
88+
return False
89+
90+
print('[Test] Locate and click the save button')
91+
save_element = dialog_element.locate('text=Save')
92+
save_element.wait_for('visible')
93+
save_element.focus()
94+
save_element.click()
95+
96+
# Application lag may cause the save dialog to linger,
97+
# if it's visible wait for it to disappear before proceeding
98+
if save_element.is_visible():
99+
print('[Test] Save element still visible after save, wait for hidden')
100+
try:
101+
save_element.expect_not_to_be_visible(timeout=120)
102+
except EndToEndTimeout as err:
103+
traceback.print_exc()
104+
print('[Test] Save button failed to hide...')
105+
106+
# Check if the save operation succeeded (by checking notebook name change)
107+
notebook_frontend.wait_for_condition(
108+
lambda: get_notebook_name(notebook_frontend) == "new_notebook.ipynb", timeout=120, period=5
109+
)
110+
print(f'[Test] Notebook name: {get_notebook_name(notebook_frontend)}')
111+
print('[Test] Notebook name was changed!')
112+
return True
35113

36-
notebook_frontend.insert_text('new_notebook.ipynb', page=EDITOR_PAGE)
37-
notebook_frontend.try_click_selector('//html//body//div[8]//div//div//div[3]//button[2]', page=EDITOR_PAGE)
38-
39-
locator_element.expect_not_to_be_visible()
114+
# Retry until timeout (wait_for_condition retries upon func exception)
115+
notebook_frontend.wait_for_condition(attempt_form_fill_and_save, timeout=900, period=1)
40116

41-
assert get_notebook_name(notebook_frontend) == "new_notebook.ipynb"
42-
assert "new_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE)
117+
print('[Test] Check notebook name in URL')
118+
notebook_frontend.wait_for_condition(
119+
lambda: notebook_name in notebook_frontend.get_page_url(page=EDITOR_PAGE),
120+
timeout=120,
121+
period=5
122+
)

nbclassic/tests/end_to_end/test_save_readonly_as.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
import traceback
55

6-
from .utils import EDITOR_PAGE, TimeoutError as TestingTimeout
7-
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
6+
from .utils import EDITOR_PAGE, EndToEndTimeout
87

98

109
def save_as(nb):
@@ -24,14 +23,39 @@ def set_notebook_name(nb, name):
2423

2524
def test_save_readonly_as(notebook_frontend):
2625
print('[Test] [test_save_readonly_as]')
27-
notebook_frontend.edit_cell(index=0, content='a=10; print(a)')
2826
notebook_frontend.wait_for_kernel_ready()
29-
notebook_frontend.wait_for_selector(".input", page=EDITOR_PAGE)
3027

31-
# Set a name for comparison later
32-
print('[Test] Set notebook name')
33-
set_notebook_name(notebook_frontend, name="nb1.ipynb")
34-
assert get_notebook_name(notebook_frontend) == "nb1.ipynb"
28+
print('[Test] Make notebook read-only')
29+
cell_text = (
30+
'import os\nimport stat\nos.chmod("'
31+
+ notebook_frontend.get_page_url(EDITOR_PAGE).split('?')[0].split('/')[-1]
32+
+ '", stat.S_IREAD)\nprint(0)'
33+
)
34+
notebook_frontend.edit_cell(index=0, content=cell_text)
35+
notebook_frontend.wait_for_condition(
36+
lambda: notebook_frontend.get_cell_contents(0).strip() == cell_text
37+
)
38+
notebook_frontend.evaluate("Jupyter.notebook.get_cell(0).execute();", page=EDITOR_PAGE)
39+
notebook_frontend.wait_for_cell_output(0)
40+
notebook_frontend.reload(EDITOR_PAGE)
41+
notebook_frontend.wait_for_kernel_ready()
42+
43+
print('[Test] Check that the notebook is read-only')
44+
notebook_frontend.wait_for_condition(
45+
lambda: notebook_frontend.evaluate('() => { return Jupyter.notebook.writable }', page=EDITOR_PAGE) is False,
46+
timeout=150,
47+
period=1
48+
)
49+
50+
# Add some content
51+
print('[Test] Add cell')
52+
test_content_0 = "print('a simple')\nprint('test script')"
53+
notebook_frontend.edit_cell(index=0, content=test_content_0)
54+
notebook_frontend.wait_for_condition(
55+
lambda: notebook_frontend.get_cell_contents(0).strip() == test_content_0,
56+
timeout=150,
57+
period=1
58+
)
3559

3660
# Wait for Save As modal, save
3761
print('[Test] Save')
@@ -57,7 +81,7 @@ def attempt_form_fill_and_save():
5781
# ....................
5882
# This may be a retry, check if the application state reflects a successful save operation
5983
nonlocal fill_attempts
60-
if fill_attempts and get_notebook_name(notebook_frontend) == "new_notebook.ipynb":
84+
if fill_attempts and get_notebook_name(notebook_frontend) == "writable_notebook.ipynb":
6185
print('[Test] Success from previous save attempt!')
6286
return True
6387
fill_attempts += 1
@@ -70,18 +94,18 @@ def attempt_form_fill_and_save():
7094

7195
# Set the notebook name field in the save dialog
7296
print('[Test] Fill the input field')
73-
name_input_element.evaluate(f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}')
97+
name_input_element.evaluate(f'(elem) => {{ elem.value = "writable_notebook.ipynb"; return elem.value; }}')
7498
notebook_frontend.wait_for_condition(
7599
lambda: name_input_element.evaluate(
76-
f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}') == 'new_notebook.ipynb',
100+
f'(elem) => {{ elem.value = "writable_notebook.ipynb"; return elem.value; }}') == 'writable_notebook.ipynb',
77101
timeout=120,
78102
period=.25
79103
)
80104
# Show the input field value
81105
print('[Test] Name input field contents:')
82106
field_value = name_input_element.evaluate(f'(elem) => {{ return elem.value; }}')
83107
print('[Test] ' + field_value)
84-
if field_value != 'new_notebook.ipynb':
108+
if field_value != 'writable_notebook.ipynb':
85109
return False
86110

87111
print('[Test] Locate and click the save button')
@@ -96,13 +120,13 @@ def attempt_form_fill_and_save():
96120
print('[Test] Save element still visible after save, wait for hidden')
97121
try:
98122
save_element.expect_not_to_be_visible(timeout=120)
99-
except PlaywrightTimeoutError as err:
123+
except EndToEndTimeout as err:
100124
traceback.print_exc()
101125
print('[Test] Save button failed to hide...')
102126

103127
# Check if the save operation succeeded (by checking notebook name change)
104128
notebook_frontend.wait_for_condition(
105-
lambda: get_notebook_name(notebook_frontend) == "new_notebook.ipynb", timeout=120, period=5
129+
lambda: get_notebook_name(notebook_frontend) == "writable_notebook.ipynb", timeout=120, period=5
106130
)
107131
print(f'[Test] Notebook name: {get_notebook_name(notebook_frontend)}')
108132
print('[Test] Notebook name was changed!')
@@ -113,4 +137,41 @@ def attempt_form_fill_and_save():
113137

114138
# Test that address bar was updated
115139
print('[Test] Test address bar')
116-
assert "new_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE)
140+
assert "writable_notebook.ipynb" in notebook_frontend.get_page_url(page=EDITOR_PAGE)
141+
142+
print('[Test] Check that the notebook is no longer read only')
143+
notebook_frontend.wait_for_condition(
144+
lambda: notebook_frontend.evaluate('() => { return Jupyter.notebook.writable }', page=EDITOR_PAGE) is True,
145+
timeout=150,
146+
period=1
147+
)
148+
149+
print('[Test] Add some more content')
150+
test_content_1 = "print('a second simple')\nprint('script to test save feature')"
151+
notebook_frontend.add_and_execute_cell(content=test_content_1)
152+
# and save the notebook
153+
notebook_frontend.evaluate("Jupyter.notebook.save_notebook()", page=EDITOR_PAGE)
154+
155+
print('[Test] Test that it still contains the content')
156+
notebook_frontend.wait_for_condition(
157+
lambda: notebook_frontend.get_cell_contents(index=0) == test_content_0,
158+
timeout=150,
159+
period=1
160+
)
161+
notebook_frontend.wait_for_condition(
162+
lambda: notebook_frontend.get_cell_contents(index=1) == test_content_1,
163+
timeout=150,
164+
period=1
165+
)
166+
print('[Test] Test that it persists even after a refresh')
167+
notebook_frontend.reload(EDITOR_PAGE)
168+
notebook_frontend.wait_for_condition(
169+
lambda: notebook_frontend.get_cell_contents(index=0) == test_content_0,
170+
timeout=150,
171+
period=1
172+
)
173+
notebook_frontend.wait_for_condition(
174+
lambda: notebook_frontend.get_cell_contents(index=1) == test_content_1,
175+
timeout=150,
176+
period=1
177+
)

nbclassic/tests/end_to_end/utils.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
CELL_OUTPUT_SELECTOR = '.output_subarea'
3434

3535

36-
class TimeoutError(Exception):
36+
class EndToEndTimeout(Exception):
3737

3838
def get_result(self):
3939
return None if not self.args else self.args[0]
@@ -179,6 +179,8 @@ def expect_not_to_be_visible(self, timeout=30):
179179
expect(self._element).not_to_be_visible(timeout=timeout * seconds_to_milliseconds)
180180
except ValueError as err:
181181
raise Exception('Cannot expect not_to_be_visible on this type!') from err
182+
except AssertionError as err:
183+
raise EndToEndTimeout('Error waiting not_to_be_visible!') from err
182184

183185
def expect_to_have_text(self, text):
184186
try:
@@ -393,6 +395,17 @@ def evaluate(self, text, page):
393395
def _pause(self):
394396
self._editor_page.pause()
395397

398+
def reload(self, page):
399+
"""Find an element matching selector on the given page"""
400+
if page == TREE_PAGE:
401+
specified_page = self._tree_page
402+
elif page == EDITOR_PAGE:
403+
specified_page = self._editor_page
404+
else:
405+
raise Exception('Error, provide a valid page to locate from!')
406+
407+
specified_page.reload()
408+
396409
def locate(self, selector, page):
397410
"""Find an element matching selector on the given page"""
398411
if page == TREE_PAGE:
@@ -662,7 +675,7 @@ def wait_for_condition(self, check_func, timeout=30, period=.1):
662675
traceback.print_exc()
663676
print('\n[NotebookFrontend] Ignoring exception in wait_for_condition, read more above')
664677
else:
665-
raise TimeoutError()
678+
raise EndToEndTimeout()
666679

667680
def wait_for_cell_output(self, index=0, timeout=30):
668681
"""Waits for the cell to finish executing and return the cell output"""

0 commit comments

Comments
 (0)