From 0dc7bb19d9a95df9d9c6bd00e943d407fc11f49e Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 22 Apr 2026 22:22:47 -0700 Subject: [PATCH] Table headers and column options visible for 0 rows Closes #2701 --- datasette/templates/_table.html | 5 +++-- datasette/templates/table.html | 2 -- tests/test_html.py | 2 +- tests/test_table_html.py | 26 ++++++++++++++++++++++++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/datasette/templates/_table.html b/datasette/templates/_table.html index ba34b60f..f47a325f 100644 --- a/datasette/templates/_table.html +++ b/datasette/templates/_table.html @@ -1,6 +1,6 @@
-{% if display_rows %} +{% if display_columns %}
@@ -31,6 +31,7 @@
-{% else %} +{% endif %} +{% if not display_rows %}

0 records

{% endif %} diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 2919d306..c841e1be 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -141,7 +141,6 @@ {% if all_columns %} -{% if display_rows %} -{% endif %} diff --git a/tests/test_html.py b/tests/test_html.py index e38898da..7425692d 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -805,7 +805,7 @@ async def test_blob_download_invalid_messages(ds_client, path, expected_message) async def test_zero_results(ds_client, path): response = await ds_client.get(path) soup = Soup(response.text, "html.parser") - assert 0 == len(soup.select("table")) + assert 0 == len(soup.select("table tbody tr")) assert 1 == len(soup.select("p.zero-results")) diff --git a/tests/test_table_html.py b/tests/test_table_html.py index d8dde593..86b9a4eb 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -752,8 +752,11 @@ async def test_column_chooser_present(ds_client): @pytest.mark.asyncio -async def test_mobile_column_actions_present(ds_client): - response = await ds_client.get("/fixtures/facetable") +@pytest.mark.parametrize( + "path", ["/fixtures/facetable", "/fixtures/123_starts_with_digits"] +) +async def test_mobile_column_actions_present(ds_client, path): + response = await ds_client.get(path) assert response.status_code == 200 soup = Soup(response.text, "html.parser") button = soup.select_one("button.column-actions-mobile.small-screen-only") @@ -764,6 +767,25 @@ async def test_mobile_column_actions_present(ds_client): "mobile-column-actions.js" in (script.get("src") or "") for script in soup.find_all("script") ) + # mobile-column-actions.js builds its dialog from elements, + # so the thead must render even when the table has no rows. + ths = soup.select("table.rows-and-columns thead th[data-column]") + assert len(ths) >= 1 + + +@pytest.mark.asyncio +async def test_zero_row_table_renders_thead(ds_client): + response = await ds_client.get("/fixtures/123_starts_with_digits") + assert response.status_code == 200 + soup = Soup(response.text, "html.parser") + table = soup.select_one("table.rows-and-columns") + assert table is not None + column_names = [ + th.get("data-column") for th in table.select("thead th[data-column]") + ] + assert "content" in column_names + assert table.select_one("tbody tr") is None + assert soup.select_one("p.zero-results") is not None @pytest.mark.asyncio