Repository: pyscript/pyscript

Issue:
[622] py-script should only render to the page through the `display` function
### Checklist

- [X] I added a descriptive title
- [X] I searched for other feature requests and couldn't find a duplicate (including also the ``type-feature`` tag)
- [X] I confirmed that it's not related to another project area (see the above section)

### What is the idea?

In order to disambiguate debugging output and rendering to the page, we want to add a new `display` function that can be called explicitly whenever the user wants to render things on the page. See more details at #622

### Why is this needed

Se #622

### What should happen?

We will introduce a new method `display` that can be used to display objects and other things on the page.

Here's the proposed method:

```python
def display(*objects, parent=None):
    """Display `objects` on parent. 

All non-keyword arguments are converted using the global renderer mapper or using the object `__repr__` if a renderer for that object is not present. 

If `parent` is None, the parent object set at the global `py-script` tag level will be used. 
    """
```



### Additional Context

_No response_


Discussion:
Sorry for the commit spam. Should not include the issue number in the commit message next time.
Is there a way i can unlink them from the issue?
A couple of edge details I'd propose:

### Exceptions in Display Should Show Up Onscreen

I'd propose that if an object fails to render (e.g. if an exception is raised when processing the object through the rendered mapper, or in the objet's __repr__, etc) that _something_ still be rendered to the DOM in the specified location. Since the intent is to present objects onscreen, failure's should be visible to the user onscreen. This _something_ should ideally include the Exception message.

I'd propose that if Exceptions in general are being output to the console, then errors during display() should also be output to the console, in addition to being displayed onscreen in some way.

#### Exception Formatting
Optional Extra Idea: this opens the door to a parameter that changes this behavior. Perhaps a parameter called `error_handler`, with default value None, which takes a callable. If error_handler is not None, if an exception `exc` is raised during display(), what's output to the screen is not the (formatted?) trace of the error itself, but the results of `error_handler(exc)`. The error-handler parameter is set equal to the error-handler attribute on the associated tag, if it exists.

This allows advanced users to customize the behavior when errors occur. This would also allow users to suppress output of display() errors by setting `error_handler = lambda x: None`. We can flesh out further details if this feels like an interesting path.

### When `parent` is None

A question about the details of this:

> If `parent` is None, the parent object set at the global `py-script` tag level will be used. 

I take this to mean that, if no parent is provided, the object is displayed as the next sibling of the <py-script> tag's parent in the DOM? This seems like a decent option, and I'd propose that:
- The first time display(...parent=None) is called in a <py-script>tag, the object is rendered as the next sibling of that <py-script> tag
- All subsequent calls in that tag where parent is None should render objects as the next sibling _of the previous object_ rendered this way.

Just another way of saying that:
```python
<p>an introduction</p>
<py-script>
    display("this is my first thought")
    display("and this is a second thought")
    display("i thought of a footnote", parent = Element("footnote"))
    display("also, a third thought")
</py-script>
<p>my closing thoughts</p>
<p id="footnote"></p>
```
Should render as:
![image](https://user-images.githubusercontent.com/1931111/183143277-8a40968f-32c1-42cb-bc1a-24b21420530f.png)
Maybe that seemed self-evident, but I thought it'd be worth specifying.

**Edit: I've realized baked into that image is the assumption that consecutive display() calls output on separate lines (separate `<p>` tags? separate `<div>`s as currently?). Perhaps that's not right... the issues referenced in the parent issue are almost all about how print() renders consecutive outputs on different lines...**

> ### Exceptions in Display Should Show Up Onscreen
> I'd propose that if an object fails to render (e.g. if an exception is raised when processing the object through the rendered mapper, or in the objet's **repr**, etc) that _something_ still be rendered to the DOM in the specified location. Since the intent is to present objects onscreen, failure's should be visible to the user onscreen. This _something_ should ideally include the Exception message.

I agree, exceptions should not be silent. Although this is probably an discussion which is orthogonal from this particular issue and should probably be discussed elsewhere.

> I'd propose that if Exceptions in general are being output to the console, then errors during display() should also be output to the console, in addition to being displayed onscreen in some way.

I would make it even more extreme and propose that exception should _never_ be silent, even if they are raised outside a `display()` call.

> #### Exception Formatting
> Optional Extra Idea: this opens the door to a parameter that changes this behavior. Perhaps a parameter called `error_handler`, with default value None, which takes a callable. If error_handler is not None, if an exception `exc` is raised during display(), what's output to the screen is not the (formatted?) trace of the error itself, but the results of `error_handler(exc)`. The error-handler parameter is set equal to the error-handler attribute on the associated tag, if it exists.
> 
> This allows advanced users to customize the behavior when errors occur. This would also allow users to suppress output of display() errors by setting `error_handler = lambda x: None`. We can flesh out further details if this feels like an interesting path.

I like the idea. Probably we can extend it to have also a per-page error handler. I.e.:

1. there is a page-level `error_handler`
2. individual `<py-script>` tags can override `error_handler` if they want
3. the default page-level `error_handler` displays the exception as a node in the DOM
4. you can suppress exception reporting by overriding `error_handler` (either at the page level or the tag level).

This design has also the bonus point of being very similar to what python itself uses to display errors in the REPL, i.e. `sys.excepthook`:
```
>>> import sys
>>> sys.excepthook = lambda etype, eval, tb: None
>>> raise Exception('hello')
>>>
```

> ### When `parent` is None
> A question about the details of this:
> 
> > If `parent` is None, the parent object set at the global `py-script` tag level will be used.
> 
> I take this to mean that, if no parent is provided, the object is displayed as the next sibling of the tag's parent in the DOM? This seems like a decent option, and I'd propose that:

yes, I think it's a good idea and a reasonable default.


> * The first time display(...parent=None) is called in a tag, the object is rendered as the next sibling of that  tag
> * All subsequent calls in that tag where parent is None should render objects as the next sibling _of the previous object_ rendered this way.
[cut]
> Maybe that seemed self-evident, but I thought it'd be worth specifying.

I agree with this. And it's a good idea to be explicit, thanks :)

> **Edit: I've realized baked into that image is the assumption that consecutive display() calls output on separate lines (separate `<p>` tags? separate `<div>`s as currently?). Perhaps that's not right... the issues referenced in the parent issue are almost all about how print() renders consecutive outputs on different lines...**

I think that every call to `display()` should insert its own HTML tag. Which tag depends on the object being displayed:
- `display('some string')` (and other simple objects) should probably insert something like `<div class="pyscript-text">some string</div>`, to make it possible to apply a CSS style to it
- most "rich" objects (e.g. plots, dataframes, images, etc.) will have a custom renderer which generates some complex HTML.


Resolving patch:
diff --git a/pyscriptjs/src/python/pyscript.py b/pyscriptjs/src/python/pyscript.py
--- a/pyscriptjs/src/python/pyscript.py
+++ b/pyscriptjs/src/python/pyscript.py
@@ -1,7 +1,6 @@
 import asyncio
 import base64
 import io
-import sys
 import time
 from textwrap import dedent
 
@@ -124,6 +123,33 @@ def write(element_id, value, append=False, exec_id=0):
         )
 
 
+def set_current_display_target(target_id):
+    get_current_display_target._id = target_id
+
+
+def get_current_display_target():
+    return get_current_display_target._id
+
+
+get_current_display_target._id = None
+
+
+def display(*values, target=None, append=True):
+    default_target = get_current_display_target()
+
+    if default_target is None and target is None:
+        raise Exception(
+            "Implicit target not allowed here. Please use display(..., target=...)"
+        )
+
+    if target is not None:
+        for v in values:
+            Element(target).write(v, append=append)
+    else:
+        for v in values:
+            Element(default_target).write(v, append=append)
+
+
 class Element:
     def __init__(self, element_id, element=None):
         self._id = element_id
@@ -365,59 +391,4 @@ def child_appended(self, child):
         pass
 
 
-class OutputCtxManager:
-    def __init__(self, out=None, output_to_console=True, append=True):
-        self._out = out
-        self._prev = out
-        self.output_to_console = output_to_console
-        self._append = append
-
-    def change(self, out=None, output_to_console=True, append=True):
-        self._prev = self._out
-        self._out = out
-        self.output_to_console = output_to_console
-        self._append = append
-
-    def revert(self):
-        self._out = self._prev
-
-    def write(self, value):
-        if self._out:
-            Element(self._out).write(value, self._append)
-
-        if self.output_to_console:
-            console.info(value)
-
-
-class OutputManager:
-    def __init__(self, out=None, err=None, output_to_console=True, append=True):
-        sys.stdout = self._out_manager = OutputCtxManager(
-            out=out, output_to_console=output_to_console, append=append
-        )
-        sys.stderr = self._err_manager = OutputCtxManager(
-            out=err, output_to_console=output_to_console, append=append
-        )
-        self.output_to_console = output_to_console
-        self._append = append
-
-    def change(self, out=None, err=None, output_to_console=True, append=True):
-        self._out_manager.change(
-            out=out, output_to_console=output_to_console, append=append
-        )
-        sys.stdout = self._out_manager
-        self._err_manager.change(
-            out=err, output_to_console=output_to_console, append=append
-        )
-        sys.stderr = self._err_manager
-        self.output_to_console = output_to_console
-        self._append = append
-
-    def revert(self):
-        self._out_manager.revert()
-        self._err_manager.revert()
-        sys.stdout = self._out_manager
-        sys.stderr = self._err_manager
-
-
 pyscript = PyScript()
-output_manager = OutputManager()


Test patch:
diff --git a/pyscriptjs/tests/integration/test_01_basic.py b/pyscriptjs/tests/integration/test_01_basic.py
--- a/pyscriptjs/tests/integration/test_01_basic.py
+++ b/pyscriptjs/tests/integration/test_01_basic.py
@@ -8,7 +8,7 @@ def test_pyscript_hello(self):
         self.pyscript_run(
             """
             <py-script>
-                print('hello pyscript')
+                display('hello pyscript')
             </py-script>
         """
         )
diff --git a/pyscriptjs/tests/integration/test_02_async.py b/pyscriptjs/tests/integration/test_02_async.py
deleted file mode 100644
--- a/pyscriptjs/tests/integration/test_02_async.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from .support import PyScriptTest
-
-
-class TestAsync(PyScriptTest):
-    def test_multiple_async(self):
-        self.pyscript_run(
-            """
-        <py-script>
-            import js
-            import asyncio
-            for i in range(3):
-                js.console.log('A', i)
-                await asyncio.sleep(0.1)
-        </py-script>
-
-        <py-script>
-            import js
-            import asyncio
-            for i in range(3):
-                js.console.log('B', i)
-                await asyncio.sleep(0.1)
-            js.console.log("async tadone")
-        </py-script>
-        """
-        )
-        self.wait_for_console("async tadone")
-        assert self.console.log.lines == [
-            "Python initialization complete",
-            "A 0",
-            "B 0",
-            "A 1",
-            "B 1",
-            "A 2",
-            "B 2",
-            "async tadone",
-        ]
diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py
new file mode 100644
--- /dev/null
+++ b/pyscriptjs/tests/integration/test_02_output.py
@@ -0,0 +1,247 @@
+import re
+
+from .support import PyScriptTest
+
+
+class TestOutuput(PyScriptTest):
+    def test_simple_display(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('hello world')
+            </py-script>
+        """
+        )
+        inner_html = self.page.content()
+        pattern = r'<div id="py-.*">hello world</div>'
+        assert re.search(pattern, inner_html)
+
+    def test_consecutive_display(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('hello 1')
+            </py-script>
+            <py-script>
+                display('hello 2')
+            </py-script>
+        """
+        )
+        # need to improve this to get the first/second input
+        # instead of just searching for it in the page
+        inner_html = self.page.content()
+        first_pattern = r'<div id="py-.*?-2">hello 1</div>'
+        assert re.search(first_pattern, inner_html)
+        second_pattern = r'<div id="py-.*?-3">hello 2</div>'
+        assert re.search(second_pattern, inner_html)
+
+        assert first_pattern is not second_pattern
+
+    def test_multiple_display_calls_same_tag(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('hello')
+                display('world')
+            </py-script>
+        """
+        )
+        inner_html = self.page.content()
+        pattern = r'<div id="py-.*?-2">hello</div>'
+        assert re.search(pattern, inner_html)
+        pattern = r'<div id="py-.*?-3">world</div>'
+        assert re.search(pattern, inner_html)
+
+    def test_no_implicit_target(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                def display_hello():
+                    # this fails because we don't have any implicit target
+                    # from event handlers
+                    display('hello')
+            </py-script>
+            <button id="my-button" py-onClick="display_hello()">Click me</button>
+        """
+        )
+        self.page.locator("text=Click me").click()
+        text = self.page.text_content("body")
+        assert "hello" not in text
+
+        # currently the test infrastructure doesn't allow to easily assert that
+        # js exceptions were raised this is a workaround but we need a better fix.
+        # Antonio promised to write it
+        assert len(self._page_errors) == 1
+        console_text = self._page_errors
+        assert (
+            "Implicit target not allowed here. Please use display(..., target=...)"
+            in console_text[0].message
+        )
+        self._page_errors = []
+
+    def test_explicit_target_pyscript_tag(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                def display_hello():
+                    display('hello', target='second-pyscript-tag')
+            </py-script>
+            <py-script id="second-pyscript-tag">
+                display_hello()
+            </py-script>
+        """
+        )
+        text = self.page.locator("id=second-pyscript-tag-2").inner_text()
+        assert "hello" in text
+
+    def test_explicit_target_on_button_tag(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                def display_hello():
+                    display('hello', target='my-button')
+            </py-script>
+            <button id="my-button" py-onClick="display_hello()">Click me</button>
+        """
+        )
+        self.page.locator("text=Click me").click()
+        text = self.page.locator("id=my-button").inner_text()
+        assert "hello" in text
+
+    def test_explicit_different_target_from_call(self):
+        self.pyscript_run(
+            """
+            <py-script id="first-pyscript-tag">
+                def display_hello():
+                    display('hello', target='second-pyscript-tag')
+            </py-script>
+            <py-script id="second-pyscript-tag">
+                print('nothing to see here')
+            </py-script>
+            <py-script>
+                display_hello()
+            </py-script>
+        """
+        )
+        text = self.page.locator("id=second-pyscript-tag").all_inner_texts()
+        assert "hello" in text
+
+    def test_append_true(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('hello world', append=True)
+            </py-script>
+        """
+        )
+        inner_html = self.page.content()
+        pattern = r'<div id="py-.*">hello world</div>'
+        assert re.search(pattern, inner_html)
+
+    def test_append_false(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('hello world', append=False)
+            </py-script>
+        """
+        )
+        inner_html = self.page.content()
+        pattern = r'<py-script id="py-.*">hello world</py-script>'
+        assert re.search(pattern, inner_html)
+
+    def test_display_multiple_values(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                hello = 'hello'
+                world = 'world'
+                display(hello, world)
+            </py-script>
+            """
+        )
+        inner_text = self.page.inner_text("html")
+        assert inner_text == "hello\nworld"
+
+    def test_display_list_dict_tuple(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                l = ['A', 1, '!']
+                d = {'B': 2, 'List': l}
+                t = ('C', 3, '!')
+                display(l, d, t)
+            </py-script>
+            """
+        )
+        inner_text = self.page.inner_text("html")
+        print(inner_text)
+        assert (
+            inner_text
+            == "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
+        )
+
+    def test_image_display(self):
+        self.pyscript_run(
+            """
+                <py-config> packages = [  "matplotlib"] </py-config>
+                <py-script>
+                    import matplotlib.pyplot as plt
+                    xpoints = [3, 6, 9]
+                    ypoints = [1, 2, 3]
+                    plt.plot(xpoints, ypoints)
+                    plt.show()
+                </py-script>
+            """
+        )
+        inner_html = self.page.content()
+        pattern = r'<style id="matplotlib-figure-styles">'
+        assert re.search(pattern, inner_html)
+
+    def test_empty_HTML_and_console_output(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                print('print from python')
+                console.log('print from js')
+                console.error('error from js');
+            </py-script>
+        """
+        )
+        inner_html = self.page.content()
+        assert re.search("", inner_html)
+        console_text = self.console.all.lines
+        assert "print from python" in console_text
+        assert "print from js" in console_text
+        assert "error from js" in console_text
+
+    def test_text_HTML_and_console_output(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display('0')
+                print('print from python')
+                console.log('print from js')
+                console.error('error from js');
+            </py-script>
+        """
+        )
+        inner_text = self.page.inner_text("html")
+        assert "0" == inner_text
+        console_text = self.console.all.lines
+        assert "print from python" in console_text
+        assert "print from js" in console_text
+        assert "error from js" in console_text
+
+    def test_console_line_break(self):
+        self.pyscript_run(
+            """
+            <py-script>
+            print('1print\\n2print')
+            print('1console\\n2console')
+            </py-script>
+        """
+        )
+        console_text = self.console.all.lines
+        assert console_text.index("1print") == (console_text.index("2print") - 1)
+        assert console_text.index("1console") == (console_text.index("2console") - 1)
diff --git a/pyscriptjs/tests/integration/test_03_async.py b/pyscriptjs/tests/integration/test_03_async.py
new file mode 100644
--- /dev/null
+++ b/pyscriptjs/tests/integration/test_03_async.py
@@ -0,0 +1,75 @@
+import re
+
+from .support import PyScriptTest
+
+
+class TestAsync(PyScriptTest):
+    def test_multiple_async(self):
+        self.pyscript_run(
+            """
+        <py-script>
+            import js
+            import asyncio
+            for i in range(3):
+                js.console.log('A', i)
+                await asyncio.sleep(0.1)
+        </py-script>
+
+        <py-script>
+            import js
+            import asyncio
+            for i in range(3):
+                js.console.log('B', i)
+                await asyncio.sleep(0.1)
+            js.console.log("async tadone")
+        </py-script>
+        """
+        )
+        self.wait_for_console("async tadone")
+        assert self.console.log.lines == [
+            "Python initialization complete",
+            "A 0",
+            "B 0",
+            "A 1",
+            "B 1",
+            "A 2",
+            "B 2",
+            "async tadone",
+        ]
+
+    def test_multiple_async_display(self):
+        self.pyscript_run(
+            """
+                <py-script id="py1">
+                def say_hello():
+                    display('hello')
+                </py-script>
+                <py-script id="py2">
+                    say_hello()
+                </py-script>
+            """
+        )
+        inner_html = self.page.content()
+        pattern = r'<div id="py2-2">hello</div>'
+        assert re.search(pattern, inner_html)
+
+    def test_multiple_async_multiple_display(self):
+        self.pyscript_run(
+            """
+                <py-script id='pyA'>
+                    import asyncio
+                    for i in range(2):
+                        display('A')
+                        await asyncio.sleep(0)
+                </py-script>
+
+                <py-script id='pyB'>
+                    import asyncio
+                    for i in range(2):
+                        display('B')
+                        await asyncio.sleep(0)
+                </py-script>
+            """
+        )
+        inner_text = self.page.inner_text("html")
+        assert "A\nB\nA\nB" in inner_text
diff --git a/pyscriptjs/tests/integration/test_03_style.py b/pyscriptjs/tests/integration/test_03_style.py
--- a/pyscriptjs/tests/integration/test_03_style.py
+++ b/pyscriptjs/tests/integration/test_03_style.py
@@ -39,8 +39,8 @@ def test_pyscript_defined(self):
             <py-config>
             name = "foo"
             </py-config>
-            <py-script>print("hello")</py-script>
-            <py-repl>print("hello")</py-repl>
+            <py-script>display("hello")</py-script>
+            <py-repl>display("hello")</py-repl>
             <py-title>hello</py-title>
             <py-inputbox label="my input">
                 import js
diff --git a/pyscriptjs/tests/integration/test_py_config.py b/pyscriptjs/tests/integration/test_py_config.py
--- a/pyscriptjs/tests/integration/test_py_config.py
+++ b/pyscriptjs/tests/integration/test_py_config.py
@@ -95,7 +95,7 @@ def test_runtime_config(self, tar_location):
                 import sys, js
                 pyodide_version = sys.modules["pyodide"].__version__
                 js.console.log("version", pyodide_version)
-                pyodide_version
+                display(pyodide_version)
             </py-script>
         """,
         )
diff --git a/pyscriptjs/tests/integration/test_py_repl.py b/pyscriptjs/tests/integration/test_py_repl.py
--- a/pyscriptjs/tests/integration/test_py_repl.py
+++ b/pyscriptjs/tests/integration/test_py_repl.py
@@ -1,3 +1,6 @@
+import pytest
+from playwright.sync_api import expect
+
 from .support import PyScriptTest
 
 
@@ -20,15 +23,15 @@ def test_repl_runs_on_button_press(self):
             """
         )
 
-        self.page.locator("py-repl").type("print(2+2)")
+        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-1", state="attached")
+        repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
 
-        assert repl_result.inner_text() == "4"
+        assert repl_result.inner_text() == "hello"
 
     def test_repl_runs_with_shift_enter(self):
         self.pyscript_run(
@@ -36,10 +39,77 @@ def test_repl_runs_with_shift_enter(self):
             <py-repl id="my-repl" auto-generate="true"> </py-repl>
             """
         )
-        self.page.locator("py-repl").type("2+2")
+        self.page.locator("py-repl").type('display("hello")')
 
         # Confirm that we get a result by using the keys shortcut
         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):
+        self.pyscript_run(
+            """
+            <py-repl id="my-repl" auto-generate="true"> </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()
+
+        # The result gets the id of the repl + n
         repl_result = self.page.wait_for_selector("#my-repl-1", state="attached")
 
-        assert repl_result.text_content() == "4"
+        assert repl_result.inner_text() == ""
+
+    def test_repl_error_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()
+
+    # 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):
+        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()
+
+    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()
+        self.page.keyboard.press("Shift+Enter")
+        expect(self.page.locator(".py-error")).to_be_visible()
+
+    # 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):
+        self.pyscript_run(
+            """
+            <py-repl id="my-repl" auto-generate="true"> </py-repl>
+            """
+        )
+        self.page.locator("py-repl").type("d")
+        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')")
+        self.page.keyboard.press("Shift+Enter")
+        repl_result = self.page.wait_for_selector("#my-repl-2", state="attached")
+        assert repl_result.inner_text() == "ok"
diff --git a/pyscriptjs/tests/integration/test_zz_examples.py b/pyscriptjs/tests/integration/test_zz_examples.py
--- a/pyscriptjs/tests/integration/test_zz_examples.py
+++ b/pyscriptjs/tests/integration/test_zz_examples.py
@@ -258,20 +258,18 @@ def test_repl(self):
         assert self.page.title() == "REPL"
         wait_for_render(self.page, "*", "<py-repl.*?>")
 
-        self.page.locator("py-repl").type("print('Hello, World!')")
+        self.page.locator("py-repl").type("display('Hello, World!')")
         self.page.locator("button").click()
 
-        assert self.page.locator("#my-repl-1").text_content() == "Hello, World!"
+        assert self.page.locator("#my-repl-2").text_content() == "Hello, World!"
 
         # Confirm that using the second repl still works properly
-        self.page.locator("#my-repl-2").type("2*2")
+        self.page.locator("#my-repl-2").type("display(2*2)")
         self.page.keyboard.press("Shift+Enter")
         # Make sure that the child of the second repl is attached properly
         # before looking into the text_content
-        second_repl_result = self.page.wait_for_selector(
-            "#my-repl-2-2", state="attached"
-        )
-        assert second_repl_result.text_content() == "4"
+        assert self.page.wait_for_selector("#my-repl-2-1", state="attached")
+        assert self.page.locator("#my-repl-2-1").text_content() == "4"
 
     def test_repl2(self):
         self.goto("examples/repl2.html")
@@ -279,7 +277,7 @@ def test_repl2(self):
         assert self.page.title() == "Custom REPL Example"
         wait_for_render(self.page, "*", "<py-repl.*?>")
         # confirm we can import utils and run one command
-        self.page.locator("py-repl").type("import utils\nutils.now()")
+        self.page.locator("py-repl").type("import utils\ndisplay(utils.now())")
         self.page.locator("button").click()
         # Make sure the output is in the page
         self.page.wait_for_selector("#output")