Working with playwright

Turşu does not provide playwright option, but you can works with both playwright and Turşu for testing your web application.

Actually, it has been develop for that!

Installation

uv add tursu pytest-playwright
uv run playwright install chromium
uv tursu init --no-dummies -o tests/functionals-playwright/
  • playwright requires to download its own browsers, refer to the playwright documentation for more options.

  • The --no-dummies does not create dummy scenario.

Create a step

from playwright.sync_api import Page, expect

from tursu import given, then, when


@given("anonymous user on {path}")
@when("I visit {path}")
def i_visit(page: Page, http_server: str, path: str):
    page.goto(f"{http_server}{path}")


@then('the user sees the text "{text}"')
def assert_text(page: Page, text: str):
    loc = page.get_by_text(text)
    expect(loc).to_be_visible()

That’s it. You can just use the “Page” fixture provided by pytest-playwright directly.

Create a scenario

@wip
Feature: Basic Test

  Scenario: Hello world
    Given anonymous user on /
    Then the user sees the text "Hello, World!"

Run the test

uv run pytest --browser chromium -v tests/using_playwright/

Sure this tests will fail, this is BDD :)

Adding a fixture

import socket
import threading
import time
from collections.abc import Iterator
from http.server import BaseHTTPRequestHandler, HTTPServer

import pytest

from tursu import tursu_collect_file

tursu_collect_file()


class HelloWorldHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(b"<body>Hello, World!</body>")


def wait_for_socket(host: str, port: int, timeout: int = 5, poll_time: float = 0.1):
    """Wait until the socket is open, or raise an error if the timeout is exceeded."""
    for _ in range(timeout * int(1 / poll_time)):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            if sock.connect_ex((host, port)) == 0:
                break
        time.sleep(poll_time)
    else:
        raise RuntimeError(f"Server on {host}:{port} did not start in time.")


@pytest.fixture(autouse=True)
def http_server() -> Iterator[str]:
    """Start the service in a thread."""
    server_address = ("127.0.0.1", 8888)
    httpd = HTTPServer(server_address, HelloWorldHandler)

    thread = threading.Thread(target=httpd.serve_forever, daemon=True)
    thread.start()

    wait_for_socket(*server_address)
    yield "http://127.0.0.1:8888"

    httpd.shutdown()
    thread.join()

Run the test

$ uv run pytest --browser chromium -v tests/using_playwright/
================================= test session starts =================================
baseurl: http://localhost:8888
configfile: pyproject.toml
plugins: cov-6.0.0, playwright-0.7.0, base-url-2.1.0
collected 1 item

📄 Document: 01_basic.feature_Basic_Test.py::test_2_Hello_world[chromium]
🥒 Feature: Basic Test
🎬 Scenario: Hello world
✅ Given anonymous user on /
✅ Then the user sees the text "Hello, World!"
                                                                           PASSED [100%]

================================== 1 passed in 0.94s ==================================

Use the trace artefact

While doing continuous integration, always activate the option --tracing retain-on-failure from pytest-playright, it will generate a trace.zip file in a test-results directory to configured as a build artefact.

Afterwhat you can see all step of the scenario using the show-trace command:

$ uv run playwright show-trace trace.zip

While running locally, you can stop on the first error and then show the trace.

$ rm -rf test-results
$ uv run pytest -sxv --tracing retain-on-failure tests/functionals
$ uv run playwright show-trace test-results/**/trace.zip

Asyncio

pytest-playwright does not works well with pytest-asyncio, both plugins conflicts on the asyncio loop, and in that case, the package pytest-playwright-asyncio shoud be used, and step definition has to be coroutine and the scenario decorated manually with a @asyncio tag.

This is the subject of the next chapter.