datasette/tests/test_playwright.py
2026-06-14 16:44:17 -07:00

147 lines
4.3 KiB
Python

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()
@pytest.mark.playwright
def test_navigation_search_tracks_and_renders_recent_items(page, datasette_server):
page.goto(datasette_server)
result = page.evaluate("""
async () => {
await customElements.whenDefined("navigation-search");
const element = document.querySelector("navigation-search");
const key = element.recentItemsStorageKey();
localStorage.removeItem(key);
const items = Array.from({ length: 6 }, (_, index) => ({
name: `Item ${index + 1}`,
url: `/item-${index + 1}`,
type: "table",
description: "Table",
}));
items[5].name = "content: recent_datasette_releases";
items[5].display_name = "Recent Datasette releases";
for (const item of items) {
element.saveRecentItem(item);
}
const stored = JSON.parse(localStorage.getItem(key));
element.matches = [
items[5],
items[4],
{
name: "Other",
url: "/other",
type: "database",
description: "Database",
},
];
element.shadowRoot.querySelector(".search-input").value = "";
element.renderResults();
const html = element.shadowRoot.querySelector(".results-container").innerHTML;
element.clearRecentItems();
const clearedValue = localStorage.getItem(key);
element.renderResults();
const htmlAfterClear = element.shadowRoot
.querySelector(".results-container")
.innerHTML;
return { stored, html, clearedValue, htmlAfterClear };
}
""")
assert [item["url"] for item in result["stored"]] == [
"/item-6",
"/item-5",
"/item-4",
"/item-3",
"/item-2",
]
assert result["stored"][0]["display_name"] == "Recent Datasette releases"
assert "Recent" in result["html"]
assert "Recent Datasette releases" in result["html"]
assert "Item 5" in result["html"]
assert "content: recent_datasette_releases" in result["html"]
assert "Item 4" in result["html"]
assert "Item 2" in result["html"]
assert "Other" not in result["html"]
assert "Clear recent" in result["html"]
assert result["clearedValue"] is None
assert "Clear recent" not in result["htmlAfterClear"]