Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -0,0 +1 @@
# UX Component Tests

View File

@@ -0,0 +1,193 @@
"""
Tests for breadcrumb utilities.
These tests verify that the breadcrumb system generates
correct navigation structures and Schema.org markup.
"""
import pytest
from django.test import RequestFactory
from django.urls import reverse
from apps.core.utils.breadcrumbs import (
Breadcrumb,
BreadcrumbBuilder,
build_breadcrumb,
)
class TestBreadcrumb:
"""Tests for Breadcrumb dataclass."""
def test_basic_breadcrumb(self):
"""Should create breadcrumb with required fields."""
crumb = Breadcrumb(label="Home", url="/")
assert crumb.label == "Home"
assert crumb.url == "/"
assert crumb.icon is None
assert crumb.is_current is False
def test_breadcrumb_with_icon(self):
"""Should accept icon parameter."""
crumb = Breadcrumb(label="Home", url="/", icon="fas fa-home")
assert crumb.icon == "fas fa-home"
def test_current_breadcrumb(self):
"""Should mark breadcrumb as current."""
crumb = Breadcrumb(label="Current Page", is_current=True)
assert crumb.is_current is True
assert crumb.url is None
def test_schema_position(self):
"""Should have default schema position."""
crumb = Breadcrumb(label="Test")
assert crumb.schema_position == 1
class TestBuildBreadcrumb:
"""Tests for build_breadcrumb helper function."""
def test_basic_breadcrumb(self):
"""Should create breadcrumb dict with defaults."""
crumb = build_breadcrumb("Home", "/")
assert crumb["label"] == "Home"
assert crumb["url"] == "/"
assert crumb["is_current"] is False
def test_current_breadcrumb(self):
"""Should mark as current when specified."""
crumb = build_breadcrumb("Current", is_current=True)
assert crumb["is_current"] is True
def test_breadcrumb_with_icon(self):
"""Should include icon when specified."""
crumb = build_breadcrumb("Home", "/", icon="fas fa-home")
assert crumb["icon"] == "fas fa-home"
class TestBreadcrumbBuilder:
"""Tests for BreadcrumbBuilder class."""
def test_empty_builder(self):
"""Should build empty list when no crumbs added."""
builder = BreadcrumbBuilder()
crumbs = builder.build()
assert crumbs == []
def test_add_home(self):
"""Should add home breadcrumb with defaults."""
builder = BreadcrumbBuilder()
crumbs = builder.add_home().build()
assert len(crumbs) == 1
assert crumbs[0].label == "Home"
assert crumbs[0].url == "/"
assert crumbs[0].icon == "fas fa-home"
def test_add_home_custom(self):
"""Should allow customizing home breadcrumb."""
builder = BreadcrumbBuilder()
crumbs = builder.add_home(
label="Dashboard",
url="/dashboard/",
icon="fas fa-tachometer-alt",
).build()
assert crumbs[0].label == "Dashboard"
assert crumbs[0].url == "/dashboard/"
assert crumbs[0].icon == "fas fa-tachometer-alt"
def test_add_breadcrumb(self):
"""Should add breadcrumb with label and URL."""
builder = BreadcrumbBuilder()
crumbs = builder.add("Parks", "/parks/").build()
assert len(crumbs) == 1
assert crumbs[0].label == "Parks"
assert crumbs[0].url == "/parks/"
def test_add_current(self):
"""Should add current page breadcrumb."""
builder = BreadcrumbBuilder()
crumbs = builder.add_current("Current Page").build()
assert len(crumbs) == 1
assert crumbs[0].label == "Current Page"
assert crumbs[0].is_current is True
assert crumbs[0].url is None
def test_add_current_with_icon(self):
"""Should add current page with icon."""
builder = BreadcrumbBuilder()
crumbs = builder.add_current("Settings", icon="fas fa-cog").build()
assert crumbs[0].icon == "fas fa-cog"
def test_chain_multiple_breadcrumbs(self):
"""Should chain multiple breadcrumbs."""
builder = BreadcrumbBuilder()
crumbs = (
builder.add_home()
.add("Parks", "/parks/")
.add("California", "/parks/california/")
.add_current("Disneyland")
.build()
)
assert len(crumbs) == 4
assert crumbs[0].label == "Home"
assert crumbs[1].label == "Parks"
assert crumbs[2].label == "California"
assert crumbs[3].label == "Disneyland"
assert crumbs[3].is_current is True
def test_schema_positions_auto_assigned(self):
"""Should auto-assign schema positions."""
builder = BreadcrumbBuilder()
crumbs = (
builder.add_home().add("Parks", "/parks/").add_current("Test").build()
)
assert crumbs[0].schema_position == 1
assert crumbs[1].schema_position == 2
assert crumbs[2].schema_position == 3
def test_builder_is_reusable(self):
"""Builder should be reusable after build."""
builder = BreadcrumbBuilder()
builder.add_home()
crumbs1 = builder.build()
builder.add("New", "/new/")
crumbs2 = builder.build()
assert len(crumbs1) == 1
assert len(crumbs2) == 2
class TestBreadcrumbContextProcessor:
"""Tests for breadcrumb context processor."""
def test_empty_breadcrumbs_when_not_set(self):
"""Should return empty list when not set on request."""
from apps.core.context_processors import breadcrumbs
factory = RequestFactory()
request = factory.get("/")
context = breadcrumbs(request)
assert context["breadcrumbs"] == []
def test_returns_breadcrumbs_from_request(self):
"""Should return breadcrumbs when set on request."""
from apps.core.context_processors import breadcrumbs
factory = RequestFactory()
request = factory.get("/")
request.breadcrumbs = [
build_breadcrumb("Home", "/"),
build_breadcrumb("Test", is_current=True),
]
context = breadcrumbs(request)
assert len(context["breadcrumbs"]) == 2

View File

@@ -0,0 +1,357 @@
"""
Tests for UX component templates.
These tests verify that component templates render correctly
with various parameter combinations.
"""
import pytest
from django.template import Context, Template
from django.test import RequestFactory, override_settings
@pytest.mark.django_db
class TestPageHeaderComponent:
"""Tests for page_header.html component."""
def test_renders_title(self):
"""Should render title text."""
template = Template(
"""
{% include 'components/layout/page_header.html' with title='Test Title' %}
"""
)
html = template.render(Context({}))
assert "Test Title" in html
def test_renders_subtitle(self):
"""Should render subtitle when provided."""
template = Template(
"""
{% include 'components/layout/page_header.html' with
title='Title'
subtitle='Subtitle text'
%}
"""
)
html = template.render(Context({}))
assert "Subtitle text" in html
def test_renders_icon(self):
"""Should render icon when provided."""
template = Template(
"""
{% include 'components/layout/page_header.html' with
title='Title'
icon='fas fa-star'
%}
"""
)
html = template.render(Context({}))
assert "fas fa-star" in html
def test_renders_primary_action(self):
"""Should render primary action button."""
template = Template(
"""
{% include 'components/layout/page_header.html' with
title='Title'
primary_action_url='/create/'
primary_action_text='Create'
%}
"""
)
html = template.render(Context({}))
assert "Create" in html
assert "/create/" in html
@pytest.mark.django_db
class TestActionBarComponent:
"""Tests for action_bar.html component."""
def test_renders_primary_action(self):
"""Should render primary action button."""
template = Template(
"""
{% include 'components/ui/action_bar.html' with
primary_action_text='Save'
primary_action_url='/save/'
%}
"""
)
html = template.render(Context({}))
assert "Save" in html
assert "/save/" in html
def test_renders_secondary_action(self):
"""Should render secondary action button."""
template = Template(
"""
{% include 'components/ui/action_bar.html' with
secondary_action_text='Preview'
%}
"""
)
html = template.render(Context({}))
assert "Preview" in html
def test_renders_tertiary_action(self):
"""Should render tertiary action button."""
template = Template(
"""
{% include 'components/ui/action_bar.html' with
tertiary_action_text='Cancel'
tertiary_action_url='/back/'
%}
"""
)
html = template.render(Context({}))
assert "Cancel" in html
assert "/back/" in html
def test_alignment_classes(self):
"""Should apply correct alignment classes."""
template = Template(
"""
{% include 'components/ui/action_bar.html' with
align='between'
primary_action_text='Save'
%}
"""
)
html = template.render(Context({}))
assert "justify-between" in html
@pytest.mark.django_db
class TestSkeletonComponents:
"""Tests for skeleton screen components."""
def test_list_skeleton_renders(self):
"""Should render list skeleton with specified rows."""
template = Template(
"""
{% include 'components/skeletons/list_skeleton.html' with rows=3 %}
"""
)
html = template.render(Context({}))
assert "animate-pulse" in html
def test_card_grid_skeleton_renders(self):
"""Should render card grid skeleton."""
template = Template(
"""
{% include 'components/skeletons/card_grid_skeleton.html' with cards=4 %}
"""
)
html = template.render(Context({}))
assert "animate-pulse" in html
def test_detail_skeleton_renders(self):
"""Should render detail skeleton."""
template = Template(
"""
{% include 'components/skeletons/detail_skeleton.html' %}
"""
)
html = template.render(Context({}))
assert "animate-pulse" in html
def test_form_skeleton_renders(self):
"""Should render form skeleton."""
template = Template(
"""
{% include 'components/skeletons/form_skeleton.html' with fields=3 %}
"""
)
html = template.render(Context({}))
assert "animate-pulse" in html
def test_table_skeleton_renders(self):
"""Should render table skeleton."""
template = Template(
"""
{% include 'components/skeletons/table_skeleton.html' with rows=5 columns=4 %}
"""
)
html = template.render(Context({}))
assert "animate-pulse" in html
@pytest.mark.django_db
class TestModalComponents:
"""Tests for modal components."""
def test_modal_base_renders(self):
"""Should render modal base structure."""
template = Template(
"""
{% include 'components/modals/modal_base.html' with
modal_id='test-modal'
show_var='showModal'
title='Test Modal'
%}
"""
)
html = template.render(Context({}))
assert "test-modal" in html
assert "Test Modal" in html
assert "showModal" in html
def test_modal_confirm_renders(self):
"""Should render confirmation modal."""
template = Template(
"""
{% include 'components/modals/modal_confirm.html' with
modal_id='confirm-modal'
show_var='showConfirm'
title='Confirm Action'
message='Are you sure?'
confirm_text='Yes'
%}
"""
)
html = template.render(Context({}))
assert "confirm-modal" in html
assert "Confirm Action" in html
assert "Are you sure?" in html
assert "Yes" in html
def test_modal_confirm_destructive_variant(self):
"""Should apply destructive styling."""
template = Template(
"""
{% include 'components/modals/modal_confirm.html' with
modal_id='delete-modal'
show_var='showDelete'
title='Delete'
message='Delete this item?'
confirm_variant='destructive'
%}
"""
)
html = template.render(Context({}))
assert "btn-destructive" in html
@pytest.mark.django_db
class TestBreadcrumbComponent:
"""Tests for breadcrumb component."""
def test_renders_breadcrumbs(self):
"""Should render breadcrumb navigation."""
template = Template(
"""
{% include 'components/navigation/breadcrumbs.html' %}
"""
)
breadcrumbs = [
{"label": "Home", "url": "/", "is_current": False},
{"label": "Parks", "url": "/parks/", "is_current": False},
{"label": "Test Park", "url": None, "is_current": True},
]
html = template.render(Context({"breadcrumbs": breadcrumbs}))
assert "Home" in html
assert "Parks" in html
assert "Test Park" in html
def test_renders_schema_org_markup(self):
"""Should include Schema.org BreadcrumbList."""
template = Template(
"""
{% include 'components/navigation/breadcrumbs.html' %}
"""
)
breadcrumbs = [
{"label": "Home", "url": "/", "is_current": False, "schema_position": 1},
{"label": "Test", "url": None, "is_current": True, "schema_position": 2},
]
html = template.render(Context({"breadcrumbs": breadcrumbs}))
assert "BreadcrumbList" in html
def test_empty_breadcrumbs(self):
"""Should handle empty breadcrumbs gracefully."""
template = Template(
"""
{% include 'components/navigation/breadcrumbs.html' %}
"""
)
html = template.render(Context({"breadcrumbs": []}))
# Should not error, may render nothing or empty nav
assert html is not None
@pytest.mark.django_db
class TestStatusBadgeComponent:
"""Tests for status badge component."""
def test_renders_status_text(self):
"""Should render status label."""
template = Template(
"""
{% include 'components/status_badge.html' with status='published' label='Published' %}
"""
)
html = template.render(Context({}))
assert "Published" in html
def test_applies_status_colors(self):
"""Should apply appropriate color classes for status."""
# Test published/active status
template = Template(
"""
{% include 'components/status_badge.html' with status='published' %}
"""
)
html = template.render(Context({}))
# Should have some indication of success/green styling
assert "green" in html.lower() or "success" in html.lower() or "published" in html.lower()
@pytest.mark.django_db
class TestLoadingIndicatorComponent:
"""Tests for loading indicator component."""
def test_renders_loading_indicator(self):
"""Should render loading indicator."""
template = Template(
"""
{% include 'htmx/components/loading_indicator.html' with text='Loading...' %}
"""
)
html = template.render(Context({}))
assert "Loading" in html
def test_renders_with_id(self):
"""Should render with specified ID for htmx-indicator."""
template = Template(
"""
{% include 'htmx/components/loading_indicator.html' with id='my-loader' %}
"""
)
html = template.render(Context({}))
assert "my-loader" in html

View File

@@ -0,0 +1,282 @@
"""
Tests for HTMX utility functions.
These tests verify that the HTMX response helpers generate
correct responses with proper headers and content.
"""
import json
import pytest
from django.http import HttpRequest
from django.test import RequestFactory
from apps.core.htmx_utils import (
get_htmx_target,
get_htmx_trigger,
htmx_error,
htmx_modal_close,
htmx_redirect,
htmx_refresh,
htmx_refresh_section,
htmx_success,
htmx_trigger,
htmx_validation_response,
htmx_warning,
is_htmx_request,
)
class TestIsHtmxRequest:
"""Tests for is_htmx_request function."""
def test_returns_true_for_htmx_request(self):
"""Should return True when HX-Request header is 'true'."""
factory = RequestFactory()
request = factory.get("/", HTTP_HX_REQUEST="true")
assert is_htmx_request(request) is True
def test_returns_false_for_regular_request(self):
"""Should return False for regular requests without HTMX header."""
factory = RequestFactory()
request = factory.get("/")
assert is_htmx_request(request) is False
def test_returns_false_for_wrong_value(self):
"""Should return False when HX-Request header has wrong value."""
factory = RequestFactory()
request = factory.get("/", HTTP_HX_REQUEST="false")
assert is_htmx_request(request) is False
class TestGetHtmxTarget:
"""Tests for get_htmx_target function."""
def test_returns_target_when_present(self):
"""Should return target ID when HX-Target header is present."""
factory = RequestFactory()
request = factory.get("/", HTTP_HX_TARGET="my-target")
assert get_htmx_target(request) == "my-target"
def test_returns_none_when_missing(self):
"""Should return None when HX-Target header is missing."""
factory = RequestFactory()
request = factory.get("/")
assert get_htmx_target(request) is None
class TestGetHtmxTrigger:
"""Tests for get_htmx_trigger function."""
def test_returns_trigger_when_present(self):
"""Should return trigger ID when HX-Trigger header is present."""
factory = RequestFactory()
request = factory.get("/", HTTP_HX_TRIGGER="my-button")
assert get_htmx_trigger(request) == "my-button"
def test_returns_none_when_missing(self):
"""Should return None when HX-Trigger header is missing."""
factory = RequestFactory()
request = factory.get("/")
assert get_htmx_trigger(request) is None
class TestHtmxRedirect:
"""Tests for htmx_redirect function."""
def test_sets_redirect_header(self):
"""Should set HX-Redirect header with correct URL."""
response = htmx_redirect("/parks/")
assert response["HX-Redirect"] == "/parks/"
def test_returns_empty_body(self):
"""Should return empty response body."""
response = htmx_redirect("/parks/")
assert response.content == b""
class TestHtmxTrigger:
"""Tests for htmx_trigger function."""
def test_simple_trigger(self):
"""Should set simple trigger name."""
response = htmx_trigger("myEvent")
assert response["HX-Trigger"] == "myEvent"
def test_trigger_with_payload(self):
"""Should set trigger with JSON payload."""
response = htmx_trigger("myEvent", {"key": "value"})
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data == {"myEvent": {"key": "value"}}
class TestHtmxRefresh:
"""Tests for htmx_refresh function."""
def test_sets_refresh_header(self):
"""Should set HX-Refresh header to 'true'."""
response = htmx_refresh()
assert response["HX-Refresh"] == "true"
class TestHtmxSuccess:
"""Tests for htmx_success function."""
def test_basic_success_message(self):
"""Should create success response with toast trigger."""
response = htmx_success("Item saved!")
trigger_data = json.loads(response["HX-Trigger"])
assert "showToast" in trigger_data
assert trigger_data["showToast"]["type"] == "success"
assert trigger_data["showToast"]["message"] == "Item saved!"
assert trigger_data["showToast"]["duration"] == 5000
def test_success_with_custom_duration(self):
"""Should allow custom duration."""
response = htmx_success("Quick message", duration=2000)
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["duration"] == 2000
def test_success_with_title(self):
"""Should include title when provided."""
response = htmx_success("Details here", title="Success!")
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["title"] == "Success!"
def test_success_with_action(self):
"""Should include action button config."""
response = htmx_success(
"Item deleted",
action={"label": "Undo", "onClick": "undoDelete()"},
)
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["action"]["label"] == "Undo"
assert trigger_data["showToast"]["action"]["onClick"] == "undoDelete()"
def test_success_with_html_content(self):
"""Should include HTML in response body."""
response = htmx_success("Done", html="<div>Updated</div>")
assert response.content == b"<div>Updated</div>"
class TestHtmxError:
"""Tests for htmx_error function."""
def test_basic_error_message(self):
"""Should create error response with toast trigger."""
response = htmx_error("Something went wrong")
trigger_data = json.loads(response["HX-Trigger"])
assert response.status_code == 400
assert trigger_data["showToast"]["type"] == "error"
assert trigger_data["showToast"]["message"] == "Something went wrong"
assert trigger_data["showToast"]["duration"] == 0 # Persistent by default
def test_error_with_custom_status(self):
"""Should allow custom HTTP status code."""
response = htmx_error("Validation failed", status=422)
assert response.status_code == 422
def test_error_with_retry_action(self):
"""Should include retry action when requested."""
response = htmx_error("Server error", show_retry=True)
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["action"]["label"] == "Retry"
class TestHtmxWarning:
"""Tests for htmx_warning function."""
def test_basic_warning_message(self):
"""Should create warning response with toast trigger."""
response = htmx_warning("Session expiring soon")
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["type"] == "warning"
assert trigger_data["showToast"]["message"] == "Session expiring soon"
assert trigger_data["showToast"]["duration"] == 8000
class TestHtmxModalClose:
"""Tests for htmx_modal_close function."""
def test_basic_modal_close(self):
"""Should trigger closeModal event."""
response = htmx_modal_close()
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["closeModal"] is True
def test_modal_close_with_message(self):
"""Should include success toast when message provided."""
response = htmx_modal_close(message="Saved successfully!")
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["closeModal"] is True
assert trigger_data["showToast"]["message"] == "Saved successfully!"
def test_modal_close_with_refresh(self):
"""Should include refresh section trigger."""
response = htmx_modal_close(
message="Done",
refresh_target="#items-list",
refresh_url="/items/",
)
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["refreshSection"]["target"] == "#items-list"
assert trigger_data["refreshSection"]["url"] == "/items/"
class TestHtmxRefreshSection:
"""Tests for htmx_refresh_section function."""
def test_sets_retarget_header(self):
"""Should set HX-Retarget header."""
response = htmx_refresh_section("#my-section", html="<div>New</div>")
assert response["HX-Retarget"] == "#my-section"
assert response["HX-Reswap"] == "innerHTML"
def test_includes_success_message(self):
"""Should include toast when message provided."""
response = htmx_refresh_section(
"#my-section",
html="<div>New</div>",
message="Section updated",
)
trigger_data = json.loads(response["HX-Trigger"])
assert trigger_data["showToast"]["message"] == "Section updated"
@pytest.mark.django_db
class TestHtmxValidationResponse:
"""Tests for htmx_validation_response function."""
def test_validation_error_response(self):
"""Should render error template with errors."""
response = htmx_validation_response(
"email",
errors=["Invalid email format"],
)
assert response.status_code == 200
# Response should contain error markup
def test_validation_success_response(self):
"""Should render success template with message."""
response = htmx_validation_response(
"username",
success_message="Username available",
)
assert response.status_code == 200
# Response should contain success markup
def test_validation_neutral_response(self):
"""Should render empty success when no errors or message."""
response = htmx_validation_response("field")
assert response.status_code == 200

View File

@@ -0,0 +1,139 @@
"""
Tests for standardized message utilities.
These tests verify that message helper functions generate
consistent, user-friendly messages.
"""
import pytest
from apps.core.utils.messages import (
confirm_delete,
error_not_found,
error_permission,
error_validation,
info_no_changes,
success_created,
success_deleted,
success_updated,
warning_unsaved,
)
class TestSuccessMessages:
"""Tests for success message helpers."""
def test_success_created_basic(self):
"""Should generate basic created message."""
message = success_created("Park")
assert "Park" in message
assert "created" in message.lower()
def test_success_created_with_name(self):
"""Should include object name when provided."""
message = success_created("Park", "Disneyland")
assert "Disneyland" in message
def test_success_created_custom(self):
"""Should use custom message when provided."""
message = success_created("Park", custom_message="Your park is ready!")
assert message == "Your park is ready!"
def test_success_updated_basic(self):
"""Should generate basic updated message."""
message = success_updated("Park")
assert "Park" in message
assert "updated" in message.lower()
def test_success_updated_with_name(self):
"""Should include object name when provided."""
message = success_updated("Park", "Disneyland")
assert "Disneyland" in message
def test_success_deleted_basic(self):
"""Should generate basic deleted message."""
message = success_deleted("Park")
assert "Park" in message
assert "deleted" in message.lower()
def test_success_deleted_with_name(self):
"""Should include object name when provided."""
message = success_deleted("Park", "Old Park")
assert "Old Park" in message
class TestErrorMessages:
"""Tests for error message helpers."""
def test_error_validation_generic(self):
"""Should generate generic validation error."""
message = error_validation()
assert "validation" in message.lower() or "invalid" in message.lower()
def test_error_validation_with_field(self):
"""Should include field name when provided."""
message = error_validation("email")
assert "email" in message.lower()
def test_error_validation_custom(self):
"""Should use custom message when provided."""
message = error_validation(custom_message="Email format is invalid")
assert message == "Email format is invalid"
def test_error_not_found_basic(self):
"""Should generate not found message."""
message = error_not_found("Park")
assert "Park" in message
assert "not found" in message.lower() or "could not" in message.lower()
def test_error_permission_basic(self):
"""Should generate permission denied message."""
message = error_permission()
assert "permission" in message.lower() or "authorized" in message.lower()
def test_error_permission_with_action(self):
"""Should include action when provided."""
message = error_permission("delete this park")
assert "delete" in message.lower()
class TestWarningMessages:
"""Tests for warning message helpers."""
def test_warning_unsaved(self):
"""Should generate unsaved changes warning."""
message = warning_unsaved()
assert "unsaved" in message.lower() or "changes" in message.lower()
class TestInfoMessages:
"""Tests for info message helpers."""
def test_info_no_changes(self):
"""Should generate no changes message."""
message = info_no_changes()
assert "no changes" in message.lower() or "nothing" in message.lower()
class TestConfirmMessages:
"""Tests for confirmation message helpers."""
def test_confirm_delete_basic(self):
"""Should generate delete confirmation message."""
message = confirm_delete("Park")
assert "Park" in message
assert "delete" in message.lower()
def test_confirm_delete_with_name(self):
"""Should include object name when provided."""
message = confirm_delete("Park", "Disneyland")
assert "Disneyland" in message
def test_confirm_delete_warning(self):
"""Should include warning about irreversibility."""
message = confirm_delete("Park")
assert (
"cannot be undone" in message.lower()
or "permanent" in message.lower()
or "sure" in message.lower()
)

View File

@@ -0,0 +1,203 @@
"""
Tests for meta tag utilities.
These tests verify that meta tag helpers generate
correct SEO and social sharing metadata.
"""
import pytest
from django.test import RequestFactory
from apps.core.utils.meta import (
build_canonical_url,
build_meta_context,
generate_meta_description,
get_og_image,
)
class TestGenerateMetaDescription:
"""Tests for generate_meta_description function."""
def test_basic_text(self):
"""Should return text as description."""
description = generate_meta_description(text="This is a test description.")
assert description == "This is a test description."
def test_truncates_long_text(self):
"""Should truncate text longer than max_length."""
long_text = "A" * 200
description = generate_meta_description(text=long_text, max_length=160)
assert len(description) <= 160
assert description.endswith("...")
def test_custom_max_length(self):
"""Should respect custom max_length."""
text = "A" * 100
description = generate_meta_description(text=text, max_length=50)
assert len(description) <= 50
def test_strips_html(self):
"""Should strip HTML tags from text."""
html_text = "<p>This is <strong>bold</strong> text.</p>"
description = generate_meta_description(text=html_text)
assert "<p>" not in description
assert "<strong>" not in description
assert "bold" in description
def test_handles_none(self):
"""Should return empty string for None input."""
description = generate_meta_description(text=None)
assert description == ""
def test_handles_empty_string(self):
"""Should return empty string for empty input."""
description = generate_meta_description(text="")
assert description == ""
class TestGetOgImage:
"""Tests for get_og_image function."""
def test_returns_image_url(self):
"""Should return provided image URL."""
url = get_og_image(image_url="https://example.com/image.jpg")
assert url == "https://example.com/image.jpg"
def test_returns_default_when_none(self):
"""Should return default OG image when no image provided."""
url = get_og_image()
# Should return some default or empty string
assert url is not None
def test_makes_url_absolute(self):
"""Should convert relative URL to absolute when request provided."""
factory = RequestFactory()
request = factory.get("/")
request.META["HTTP_HOST"] = "example.com"
url = get_og_image(image_url="/static/images/og.jpg", request=request)
assert "example.com" in url or url.startswith("/")
class TestBuildCanonicalUrl:
"""Tests for build_canonical_url function."""
def test_returns_path(self):
"""Should return provided path."""
url = build_canonical_url(path="/parks/test/")
assert "/parks/test/" in url
def test_builds_from_request(self):
"""Should build URL from request."""
factory = RequestFactory()
request = factory.get("/parks/")
request.META["HTTP_HOST"] = "example.com"
url = build_canonical_url(request=request)
assert "/parks/" in url
def test_handles_none(self):
"""Should return empty string when nothing provided."""
url = build_canonical_url()
assert url == "" or url is not None
class TestBuildMetaContext:
"""Tests for build_meta_context function."""
def test_basic_meta_context(self):
"""Should build basic meta context with title."""
context = build_meta_context(title="Test Page")
assert "title" in context
assert context["title"] == "Test Page"
def test_includes_description(self):
"""Should include description when provided."""
context = build_meta_context(
title="Test Page",
description="This is a test page description.",
)
assert "description" in context
assert context["description"] == "This is a test page description."
def test_includes_og_tags(self):
"""Should include Open Graph tags."""
context = build_meta_context(
title="Test Page",
description="Description here.",
)
assert "og_title" in context or "title" in context
assert "og_description" in context or "description" in context
def test_includes_canonical_url(self):
"""Should include canonical URL when request provided."""
factory = RequestFactory()
request = factory.get("/test/")
request.META["HTTP_HOST"] = "example.com"
context = build_meta_context(
title="Test",
request=request,
)
assert "canonical_url" in context
def test_includes_og_image(self):
"""Should include OG image when provided."""
context = build_meta_context(
title="Test",
og_image="https://example.com/image.jpg",
)
assert "og_image" in context
assert context["og_image"] == "https://example.com/image.jpg"
def test_includes_og_type(self):
"""Should include OG type."""
context = build_meta_context(
title="Test",
og_type="article",
)
assert "og_type" in context
assert context["og_type"] == "article"
def test_default_og_type(self):
"""Should default to 'website' for OG type."""
context = build_meta_context(title="Test")
if "og_type" in context:
assert context["og_type"] == "website"
class TestMetaContextProcessor:
"""Tests for page_meta context processor."""
def test_empty_meta_when_not_set(self):
"""Should return empty dict when not set on request."""
from apps.core.context_processors import page_meta
factory = RequestFactory()
request = factory.get("/")
context = page_meta(request)
assert context["page_meta"] == {}
def test_returns_meta_from_request(self):
"""Should return page_meta when set on request."""
from apps.core.context_processors import page_meta
factory = RequestFactory()
request = factory.get("/")
request.page_meta = {
"title": "Test Page",
"description": "Test description",
}
context = page_meta(request)
assert context["page_meta"]["title"] == "Test Page"
assert context["page_meta"]["description"] == "Test description"