Test UI rendering#

This guide addresses how to test the UI with Pytest and Playwright.


Testing is key to developing robust and performant applications. Particularly when you build complex UIs you will want to ensure that it behaves as expected. Unit tests will allow you test that the logic on the backend behaves correctly, but it is also useful to test that the UI is rendered correctly and responds appropriately.

For testing the UI we recommend the framework Playwright. Panel itself is tested with this framework.

Before you get started ensure you have installed the required dependencies:

pip install panel pytest pytest-playwright

and ensure playwright sets up the browsers it will use to display the applications:

playwright install

Create the app#

Let’s create a simple data app for testing. The app sleeps 0.5 seconds (default) when loaded and when the button is clicked.

app.py

Create the file app.py and add the code below (don’t worry about the contents of the app for now):

app.py
import time

import panel as pn
import param

class App(pn.viewable.Viewer):
    run = param.Event(doc="Runs for click_delay seconds when clicked")
    runs = param.Integer(doc="The number of runs")
    status = param.String(default="No runs yet")

    load_delay = param.Number(default=0.5)
    run_delay = param.Number(default=0.5)

    def __init__(self, **params):
        super().__init__(**params)

        result = self._load()
        self._time = time.time()


        self._status_pane = pn.pane.Markdown(self.status, height=40, align="start", margin=(0,5,10,5))
        self._result_pane = pn.Column(result)
        self._view = pn.Column(
            pn.Row(pn.widgets.Button.from_param(self.param.run, sizing_mode="fixed"), self._status_pane),
            self._result_pane
        )

    def __panel__(self):
        return self._view

    def _start_run(self):
        self.status = f"Running ..."
        self._time = time.time()

    def _stop_run(self):
        now = time.time()
        duration = round(now-self._time,3)
        self._time = now
        self.runs+=1
        self.status=f"Finished run {self.runs} in {duration}sec"

    @param.depends("run", watch=True)
    def _run_with_status_update(self):
        self._start_run()
        self._result_pane[:] = [self._run()]
        self._stop_run()

    @param.depends("status", watch=True)
    def _update_status_pane(self):
        self._status_pane.object = self.status

    def _load(self):
        time.sleep(self.load_delay)
        return "Loaded"

    def _run(self):
        time.sleep(self.run_delay)
        return f"Result {self.runs+1}"


if pn.state.served:
    pn.extension(sizing_mode="stretch_width")

    App().servable()

Serve the app via panel serve app.py and open http://localhost:5006/app in your browser to see what it does.

Create a conftest.py#

The conftest.py file should be placed alongside your tests and will be loaded automatically by pytest. It is often used to declare fixtures that allow you declare reusable components. It will:

  • provide us with an available port.

  • clean up the Panel state after each test.

Create the file conftest.py and add the code below.

conftest.py
"""Shared configuration and fixtures for testing Panel"""
import panel as pn
import pytest

PORT = [6000]

@pytest.fixture
def port():
    PORT[0] += 1
    return PORT[0]

@pytest.fixture(autouse=True)
def server_cleanup():
    """
    Clean up server state after each test.
    """
    try:
        yield
    finally:
        pn.state.reset()

For more inspiration see the Panel conftest.py file

Test the app UI#

Now let us actually set up some UI tests, we will want to assert that the app:

  • Responds when we make an initial request

  • Renders a Run button

  • Updates as expected when the Run button is clicked

Create the file test_app_frontend.py and add the code below.

test_app_frontend.py
import time

import panel as pn
from app import App

CLICKS = 2

def test_component(page, port):
    # Given
    component = App()
    url = f"http://localhost:{port}"
    # When
    server = pn.serve(component, port=port, threaded=True, show=False)
    time.sleep(0.2)
    # Then
    page.goto(url)
    page.get_by_role("button", name="Run").wait_for()

    for index in range(CLICKS):
        page.get_by_role("button", name="Run").first.click()
        page.get_by_text(f"Finished run {index+1}").wait_for()
    # Clean up
    server.stop()

Let’s run pytest. We will add the --headed and --slowmo arguments to see what is going on in the browser. This is very illustrative and also helpful for debugging purposes.

pytest test_app_frontend.py --headed --slowmo 1000

Playwright UI test with --headed enabled

Record the test code#

Writing code to test complex UIs can be quite cumbersome, thankfully there is an easier way. Playwright allows you to record UI interactions as you navigate your live app and translates these interactions as code (see the Playwright codegen documentation for more detail).

You can try it yourself by launching the app again:

panel serve app.py

and starting the Playwright recorder:

playwright codegen http://localhost:5006/app

Playwright Code generation demo