Simplify pyrepl.ts and kill base.ts (#884)

Major highlights:

1. Merge&simplify base.ts and pyrepl.ts; kill base.ts
2. improve and extente the py-repl integration tests
3. Reorder the code in pyrepl.ts. This part of the PR doesn't change much of the concrete logic: it's just a sequence of renaming variables, moving code around, group code into functions, killing code which is no longer needed. But the end result is much better and nicer to read, IMHO.

Minor highlights:

1. py-repl now uses the new logic in pyexec.ts to run the code

2. after PR Add display impl, rm outputManage, print and console.log default to browser console #749 py-repl no longer displayed the result of the last evaluated expression (e.g. if you typed 42 and run it, it displayed nothing). This PR re-introduces this behavior, which is what you would expect by a REPL.

3. improve the pytest --dev option: now it implies --no-fake-server so that sourcemaps works automatically

4. improve the names of the CSS classes to be more consistent

5. kill pyrepl.test.ts: the old tests didn't check anything useful,  this style of unit test doesn't really add much value if you have good integration tests (which now we have) and trying to revive them was not worth the hassle
This commit is contained in:
Antonio Cuni
2022-10-27 10:10:57 +02:00
committed by GitHub
parent 2d33afc195
commit 214e39537b
8 changed files with 355 additions and 427 deletions

View File

@@ -1,115 +1,231 @@
import pytest
from playwright.sync_api import expect
from .support import PyScriptTest
class TestPyRepl(PyScriptTest):
def _replace(self, py_repl, newcode):
"""
Clear the editor and write new code in it.
WARNING: this assumes that the textbox has already the focus
"""
# clear the editor, write new code
self.page.keyboard.press("Control+A")
self.page.keyboard.press("Backspace")
self.page.keyboard.type(newcode)
def test_repl_loads(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl></py-repl>
"""
)
py_repl = self.page.query_selector("py-repl")
assert py_repl
assert "Python" in py_repl.inner_text()
def test_repl_runs_on_button_press(self):
def test_execute_preloaded_source(self):
"""
Unfortunately it tests two things at once, but it's impossible to write a
smaller test. I think this is the most basic test that we can write.
We test that:
1. the source code that we put in the tag is loaded inside the editor
2. clicking the button executes it
"""
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl>
print('hello from py-repl')
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
src = py_repl.inner_text()
assert "print('hello from py-repl')" in src
py_repl.locator("button").click()
assert self.console.log.lines[-1] == "hello from py-repl"
self.page.locator("py-repl").type('display("hello")')
# We only have one button in the page
self.page.locator("button").click()
# The result gets the id of the repl + n
repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
assert repl_result.inner_text() == "hello"
def test_repl_runs_with_shift_enter(self):
def test_execute_code_typed_by_the_user(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl></py-repl>
"""
)
self.page.locator("py-repl").type('display("hello")')
py_repl = self.page.locator("py-repl")
py_repl.type('print("hello")')
py_repl.locator("button").click()
assert self.console.log.lines[-1] == "hello"
# Confirm that we get a result by using the keys shortcut
def test_execute_on_shift_enter(self):
self.pyscript_run(
"""
<py-repl>
print("hello world")
</py-repl>
"""
)
self.page.keyboard.press("Shift+Enter")
py_repl = self.page.query_selector("#my-repl-2")
assert "hello" in py_repl.inner_text()
def test_repl_console_ouput(self):
# when we use locator('button').click() the message appears
# immediately, with keyboard.press we need to wait for it. I don't
# really know why it has a different behavior, I didn't investigate
# further.
self.wait_for_console("hello world")
def test_display(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl>
display('hello world')
</py-repl>
"""
)
self.page.locator("py-repl").type("print('apple')")
self.page.keyboard.press("Enter")
self.page.locator("py-repl").type("console.log('banana')")
self.page.locator("button").click()
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
out_div = py_repl.locator("div.py-repl-output")
assert out_div.inner_text() == "hello world"
# The result gets the id of the repl + n
repl_result = self.page.wait_for_selector("#my-repl-1", state="attached")
assert repl_result.inner_text() == ""
def test_repl_error_ouput(self):
def test_show_last_expression(self):
"""
Test that we display() the value of the last expression, as you would
expect by a REPL
"""
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl>
42
</py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
expect(self.page.locator(".py-error")).to_be_visible()
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
out_div = py_repl.locator("div.py-repl-output")
assert out_div.inner_text() == "42"
# console errors are observable on the headed instance
# but is just not possible to access them using the self object
@pytest.mark.xfail(reason="Cannot access console errors")
def test_repl_error_ouput_console(self):
def test_run_clears_previous_output(self):
"""
Check that we clear the previous output of the cell before executing it
again
"""
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl>
display('hello world')
</py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
def test_repl_error_and_fail_moving_forward_ouput(self):
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
"""
)
self.page.locator("py-repl").type("this is an error")
self.page.locator("button").click()
expect(self.page.locator(".py-error")).to_be_visible()
py_repl = self.page.locator("py-repl")
out_div = py_repl.locator("div.py-repl-output")
self.page.keyboard.press("Shift+Enter")
expect(self.page.locator(".py-error")).to_be_visible()
assert out_div.inner_text() == "hello world"
#
# clear the editor, write new code, execute
self._replace(py_repl, "display('another output')")
self.page.keyboard.press("Shift+Enter")
assert out_div.inner_text() == "another output"
# this tests the fact that a new error div should be created once there's
# an error but also that it should disappear automatically once the error
# is fixed
def test_repl_show_error_fix_error_check_for_ouput(self):
def test_python_exception(self):
"""
See also test01_basic::test_python_exception, since it's very similar
"""
self.pyscript_run(
"""
<py-repl id="my-repl" auto-generate="true"> </py-repl>
<py-repl>
raise Exception('this is an error')
</py-repl>
"""
)
self.page.locator("py-repl").type("d")
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
#
# check that we sent the traceback to the console
tb_lines = self.console.error.lines[-1].splitlines()
assert tb_lines[0] == "[pyexec] Python exception:"
assert tb_lines[1] == "Traceback (most recent call last):"
assert tb_lines[-1] == "Exception: this is an error"
#
# check that we show the traceback in the page
err_pre = py_repl.locator("div.py-repl-output > pre.py-error")
tb_lines = err_pre.inner_text().splitlines()
assert tb_lines[0] == "Traceback (most recent call last):"
assert tb_lines[-1] == "Exception: this is an error"
def test_python_exception_after_previous_output(self):
self.pyscript_run(
"""
<py-repl>
display('hello world')
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
out_div = py_repl.locator("div.py-repl-output")
self.page.keyboard.press("Shift+Enter")
expect(self.page.locator(".py-error")).to_be_visible()
self.page.keyboard.press("Backspace")
self.page.locator("py-repl").type("display('ok')")
assert out_div.inner_text() == "hello world"
#
# clear the editor, write new code, execute
self._replace(py_repl, "0/0")
self.page.keyboard.press("Shift+Enter")
repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
assert repl_result.inner_text() == "ok"
assert "hello world" not in out_div.inner_text()
assert "ZeroDivisionError" in out_div.inner_text()
def test_hide_previous_error_after_successful_run(self):
"""
this tests the fact that a new error div should be created once there's an
error but also that it should disappear automatically once the error
is fixed
"""
self.pyscript_run(
"""
<py-repl>
raise Exception('this is an error')
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
out_div = py_repl.locator("div.py-repl-output")
self.page.keyboard.press("Shift+Enter")
assert "this is an error" in out_div.inner_text()
#
self._replace(py_repl, "display('hello')")
self.page.keyboard.press("Shift+Enter")
assert out_div.inner_text() == "hello"
def test_output_attribute(self):
self.pyscript_run(
"""
<py-repl output="mydiv">
display('hello world')
</py-repl>
<hr>
<div id="mydiv"></div>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
#
# check that we did NOT write to py-repl-output
out_div = py_repl.locator("div.py-repl-output")
assert out_div.inner_text() == ""
# check that we are using mydiv instead
mydiv = self.page.locator("#mydiv")
assert mydiv.inner_text() == "hello world"
def test_output_attribute_does_not_exist(self):
"""
If we try to use an attribute which doesn't exist, we display an error
instead
"""
self.pyscript_run(
"""
<py-repl output="I-dont-exist">
print('I will not be executed')
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
#
out_div = py_repl.locator("div.py-repl-output")
msg = "py-repl ERROR: cannot find the output element #I-dont-exist in the DOM"
assert out_div.inner_text() == msg
assert "I will not be executed" not in self.console.log.text