diff --git a/dnscms/pyproject.toml b/dnscms/pyproject.toml index 05d194e..ce87d88 100644 --- a/dnscms/pyproject.toml +++ b/dnscms/pyproject.toml @@ -5,22 +5,22 @@ description = "" authors = [{ name = "EDB", email = "edb@neuf.no" }] requires-python = ">=3.14, <3.15" dependencies = [ - "wagtail>=7.4.2,<8", + "wagtail>=8.0,<9", "wagtail-grapple>=0.31.0,<0.32", "wagtail-headless-preview>=0.9,<0.10", - "django>=6.0.6,<7", + "django>=6.1,<7", "django-extensions>=4.1,<5", "psycopg2-binary>=2.9.12,<3", - "gunicorn>=26.0.0,<27", + "gunicorn>=26.2.0,<27", "whitenoise>=6.12.0,<7", ] [dependency-groups] dev = [ - "ruff>=0.16.1,<0.17", + "ruff>=0.16.5,<0.17", "pytest>=9.1.1,<10", "pytest-cov>=7.0.0,<8", - "pytest-django>=4.12.0,<5", + "pytest-django>=4.14.0,<5", "wagtail-factories>=4.5.0,<5", ] diff --git a/dnscms/tests/test_admin_views.py b/dnscms/tests/test_admin_views.py new file mode 100644 index 0000000..1fc3f0e --- /dev/null +++ b/dnscms/tests/test_admin_views.py @@ -0,0 +1,75 @@ +"""Admin view tests: the choose-parent redirect and a smoke test over the +admin URLs served by our viewsets.""" + +from urllib.parse import parse_qs, urlparse + +import pytest +from django.urls import reverse + +from tests.conftest import EventIndexFactory, GenericPageFactory + + +def _parsed(response): + url = urlparse(response["Location"]) + return url.path, parse_qs(url.query) + + +def test_choose_parent_no_valid_parent_renders_form(admin_client, db): + response = admin_client.get(reverse("events:choose_parent")) + assert response.status_code == 200 + + +def test_choose_parent_single_parent_redirects_with_next(admin_client, event_index): + response = admin_client.get(reverse("events:choose_parent")) + + assert response.status_code == 302 + path, query = _parsed(response) + assert path == reverse("wagtailadmin_pages:add", args=["events", "eventpage", event_index.pk]) + assert query["next"] == [reverse("events:index")] + + +def test_choose_parent_multiple_parents_shows_form_then_redirects(admin_client, event_index): + # max_count=1 stops a second EventIndex being created through the admin, + # but the view logic only cares how many instances exist. + second_index = EventIndexFactory(parent=event_index.get_parent(), slug="events-2") + + response = admin_client.get(reverse("events:choose_parent")) + assert response.status_code == 200 + + response = admin_client.post(reverse("events:choose_parent"), {"parent_page": second_index.pk}) + assert response.status_code == 302 + path, query = _parsed(response) + assert path == reverse("wagtailadmin_pages:add", args=["events", "eventpage", second_index.pk]) + assert query["next"] == [reverse("events:index")] + + +@pytest.mark.parametrize( + "url_name", + [ + "wagtailadmin_home", + "events:index", + "events:choose_parent", + "associations:index", + "news:index", + "venues:index", + "wagtailimages:index", + "wagtailsnippets_events_eventorganizer:list", + "association_chooser:choose", + "event_organizer_chooser:choose", + ], +) +def test_admin_views_render(admin_client, db, url_name): + response = admin_client.get(reverse(url_name)) + assert response.status_code == 200 + + +def test_page_edit_view_renders_with_custom_editor_assets(admin_client, home_page): + """The page editor must load with our Draftail feature and editor JS wired in.""" + page = GenericPageFactory(parent=home_page, slug="edit-me") + + response = admin_client.get(reverse("wagtailadmin_pages:edit", args=[page.pk])) + + assert response.status_code == 200 + content = response.content.decode() + assert "button-link" in content # dnscms.rich_text feature on the editor + assert "js/page-editor.js" in content # insert_editor_js hook output diff --git a/dnscms/tests/test_rich_text.py b/dnscms/tests/test_rich_text.py new file mode 100644 index 0000000..4cc209a --- /dev/null +++ b/dnscms/tests/test_rich_text.py @@ -0,0 +1,51 @@ +"""Tests for the custom "button" rich text link feature (dnscms/rich_text.py). + +- DB HTML -> front-end HTML (expand_db_html) +- DB HTML -> ContentState (editor load) +- ContentState -> DB HTML (editor save) +""" + +import json + +from wagtail.admin.rich_text.converters.contentstate import ContentstateConverter +from wagtail.rich_text import expand_db_html + +DB_HTML = 'Kjøp billett' + + +def test_expand_db_html_rewrites_button_link(): + assert expand_db_html(DB_HTML) == 'Kjøp billett' + + +def test_expand_db_html_escapes_href(): + html = expand_db_html('B') + assert html == 'B' + + +def test_expand_db_html_missing_href_falls_back_to_empty(): + assert expand_db_html('B') == 'B' + + +def test_db_html_to_contentstate(): + """Editor load: the linktype anchor becomes a MUTABLE BUTTON entity.""" + converter = ContentstateConverter(features=["button-link"]) + contentstate = json.loads(converter.from_database_format(DB_HTML)) + + entities = list(contentstate["entityMap"].values()) + assert len(entities) == 1 + entity = entities[0] + assert entity["type"] == "BUTTON" + assert entity["mutability"] == "MUTABLE" + assert entity["data"] == {"url": "/billetter/"} + + block = contentstate["blocks"][0] + assert block["text"] == "Kjøp billett" + assert len(block["entityRanges"]) == 1 + + +def test_contentstate_roundtrip_preserves_button_link(): + """Editor load + save must not lose or mangle the stored linktype anchor.""" + stored = f'
{DB_HTML}
' + converter = ContentstateConverter(features=["button-link"]) + roundtripped = converter.to_database_format(converter.from_database_format(stored)) + assert roundtripped == stored diff --git a/dnscms/tests/test_wagtail_hooks.py b/dnscms/tests/test_wagtail_hooks.py new file mode 100644 index 0000000..40a87f4 --- /dev/null +++ b/dnscms/tests/test_wagtail_hooks.py @@ -0,0 +1,51 @@ +"""Tests for the admin-UI hooks in dnscms/wagtail_hooks.py. + +Both hooks run against Wagtail admin internals (the page action menu, editor +JS injection) — the surface most likely to churn silently across Wagtail +upgrades. +""" + +from dataclasses import dataclass + +from dnscms.wagtail_hooks import editor_js, make_publish_default_action + + +@dataclass +class FakeMenuItem: + name: str + + +def _names(items): + return [item.name for item in items] + + +def test_make_publish_default_action_moves_publish_first(): + menu_items = [ + FakeMenuItem("action-save-draft"), + FakeMenuItem("action-submit"), + FakeMenuItem("action-publish"), + FakeMenuItem("action-unpublish"), + ] + + make_publish_default_action(menu_items, request=None, context={}) + + assert _names(menu_items) == [ + "action-publish", + "action-save-draft", + "action-submit", + "action-unpublish", + ] + + +def test_make_publish_default_action_without_publish_is_a_noop(): + menu_items = [FakeMenuItem("action-save-draft"), FakeMenuItem("action-submit")] + + make_publish_default_action(menu_items, request=None, context={}) + + assert _names(menu_items) == ["action-save-draft", "action-submit"] + + +def test_editor_js_returns_script_tag(): + html = editor_js() + assert html.startswith("