Initial Playwright setup plus first test

Refs #2779
This commit is contained in:
Simon Willison 2026-06-14 16:39:55 -07:00
commit 6cd65cf4fb
5 changed files with 148 additions and 2 deletions

48
.github/workflows/playwright.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Playwright
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.14"
allow-prereleases: true
cache: pip
cache-dependency-path: pyproject.toml
- name: Cache uv
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: ${{ runner.os }}-py3.14-uv-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-py3.14-uv-
- name: Cache Playwright browsers
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright/
key: ${{ runner.os }}-playwright-${{ matrix.browser }}-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-playwright-${{ matrix.browser }}-
- name: Install uv
run: python -m pip install uv
- name: Install dependencies
run: uv sync --group dev --group playwright
- name: Install ${{ matrix.browser }}
run: uv run --group dev --group playwright playwright install --with-deps ${{ matrix.browser }}
- name: Run Playwright tests
run: uv run --group dev --group playwright pytest tests/test_playwright.py --playwright --browser ${{ matrix.browser }}

View file

@ -81,6 +81,9 @@ dev = [
"ruamel.yaml",
"psutil>=5.9",
]
playwright = [
"pytest-playwright>=0.8.0",
]
[project.optional-dependencies]
rich = ["rich"]

View file

@ -6,4 +6,5 @@ filterwarnings=
ignore:Using or importing the ABCs::bs4.element
markers =
serial: tests to avoid using with pytest-xdist
asyncio_mode = strict
playwright: browser automation tests, skipped unless --playwright is passed
asyncio_mode = strict

View file

@ -96,6 +96,15 @@ def pytest_report_header(config):
return "SQLite: {}".format(version)
def pytest_addoption(parser):
parser.addoption(
"--playwright",
action="store_true",
default=False,
help="run Playwright browser automation tests",
)
def pytest_configure(config):
import sys
@ -108,7 +117,13 @@ def pytest_unconfigure(config):
del sys._called_from_test
def pytest_collection_modifyitems(items):
def pytest_collection_modifyitems(config, items):
if not config.getoption("--playwright"):
skip_playwright = pytest.mark.skip(reason="need --playwright option to run")
for item in items:
if "playwright" in item.keywords:
item.add_marker(skip_playwright)
# Ensure test_cli.py and test_black.py and test_inspect.py run first before any asyncio code kicks in
move_to_front(items, "test_cli")
move_to_front(items, "test_black")

79
tests/test_playwright.py Normal file
View file

@ -0,0 +1,79 @@
import socket
import subprocess
import sys
import time
import httpx
import pytest
from datasette.fixtures import write_fixture_database
def find_free_port():
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
def wait_for_server(process, url, timeout=10):
deadline = time.monotonic() + timeout
last_error = None
while time.monotonic() < deadline:
if process.poll() is not None:
stdout, stderr = process.communicate()
raise AssertionError(
"Datasette server exited early\n"
f"stdout:\n{stdout}\n"
f"stderr:\n{stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code < 500:
return
last_error = f"HTTP {response.status_code}: {response.text[:200]}"
except httpx.HTTPError as ex:
last_error = repr(ex)
time.sleep(0.1)
raise AssertionError(f"Timed out waiting for {url}: {last_error}")
@pytest.fixture
def datasette_server(tmp_path):
db_path = tmp_path / "fixtures.db"
write_fixture_database(str(db_path))
port = find_free_port()
process = subprocess.Popen(
[
sys.executable,
"-m",
"datasette",
str(db_path),
"--host",
"127.0.0.1",
"--port",
str(port),
"--setting",
"num_sql_threads",
"1",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
url = f"http://127.0.0.1:{port}/"
try:
wait_for_server(process, url)
yield url
finally:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
@pytest.mark.playwright
def test_datasette_homepage_contains_datasette(page, datasette_server):
page.goto(datasette_server)
assert "Datasette" in page.locator("body").inner_text()