Fix server-rendered admin messages never auto-dismissing
All checks were successful
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / ci (pull_request) Successful in 1m25s
CI / pr-e2e (pull_request) Successful in 1m24s

Root cause: Wagtail's w-messages Stimulus controller only auto-clears
messages added dynamically via JavaScript (the add() method).  Server-
rendered messages — the <li> elements produced by Django's messages
framework after a redirect — have no connect() lifecycle handler and
sit in the DOM indefinitely.

PR #64 added data-w-messages-auto-clear-value="8000" which correctly
handles dynamic messages, but server-rendered ones were unaffected.
PR #64 also added {% ifchanged %} for de-duplication, which doesn't
address persistence.

Fix: mark server-rendered <li> elements with data-server-rendered and
add an inline script that removes them after 8 seconds (matching the
auto-clear timeout for dynamic messages).  Also remove the ineffective
{% ifchanged %} de-duplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 10:55:30 +00:00
parent 0fab9ac0bf
commit ff587d9e1b
2 changed files with 77 additions and 33 deletions

View File

@@ -15,7 +15,6 @@ from apps.core.middleware import AdminMessageGuardMiddleware
def admin_message_test_view(request): def admin_message_test_view(request):
messages.success(request, "Page 'Test page' has been updated.")
messages.success(request, "Page 'Test page' has been updated.") messages.success(request, "Page 'Test page' has been updated.")
messages.success(request, "Page 'Test page' has been published.") messages.success(request, "Page 'Test page' has been published.")
return render(request, "wagtailadmin/base.html", {}) return render(request, "wagtailadmin/base.html", {})
@@ -60,24 +59,6 @@ def test_admin_message_guard_preserves_admin_messages(rf):
assert remaining[0].message == "Page 'Test page' has been updated." assert remaining[0].message == "Page 'Test page' has been updated."
@pytest.mark.django_db
@override_settings(ROOT_URLCONF="apps.core.tests.test_message_handling")
def test_wagtail_admin_template_deduplicates_consecutive_messages(client, django_user_model):
admin = django_user_model.objects.create_superuser(
username="admin-messages",
email="admin-messages@example.com",
password="admin-pass",
)
client.force_login(admin)
response = client.get("/cms/__tests__/admin-messages/")
content = response.content.decode()
assert response.status_code == 200
assert content.count("has been updated.") == 1
assert content.count("has been published.") == 1
@pytest.mark.django_db @pytest.mark.django_db
@override_settings(ROOT_URLCONF="apps.core.tests.test_message_handling") @override_settings(ROOT_URLCONF="apps.core.tests.test_message_handling")
def test_admin_messages_have_auto_clear(client, django_user_model): def test_admin_messages_have_auto_clear(client, django_user_model):
@@ -94,3 +75,46 @@ def test_admin_messages_have_auto_clear(client, django_user_model):
assert response.status_code == 200 assert response.status_code == 200
assert "data-w-messages-auto-clear-value" in content assert "data-w-messages-auto-clear-value" in content
@pytest.mark.django_db
@override_settings(ROOT_URLCONF="apps.core.tests.test_message_handling")
def test_server_rendered_messages_have_auto_dismiss_script(client, django_user_model):
"""Server-rendered messages must include an inline script that removes them
after a timeout, because the w-messages Stimulus controller only auto-clears
messages added via JavaScript — not ones already in the HTML."""
admin = django_user_model.objects.create_superuser(
username="admin-dismiss",
email="admin-dismiss@example.com",
password="admin-pass",
)
client.force_login(admin)
response = client.get("/cms/__tests__/admin-messages/")
content = response.content.decode()
assert response.status_code == 200
# Messages are rendered with the data-server-rendered marker
assert "data-server-rendered" in content
# The auto-dismiss script targets those markers
assert "querySelectorAll" in content
assert "[data-server-rendered]" in content
@pytest.mark.django_db
@override_settings(ROOT_URLCONF="apps.core.tests.test_message_handling")
def test_admin_messages_render_all_messages(client, django_user_model):
"""All messages should be rendered (no de-duplication filtering)."""
admin = django_user_model.objects.create_superuser(
username="admin-render",
email="admin-render@example.com",
password="admin-pass",
)
client.force_login(admin)
response = client.get("/cms/__tests__/admin-messages/")
content = response.content.decode()
assert response.status_code == 200
assert "has been updated." in content
assert "has been published." in content

View File

@@ -8,13 +8,13 @@
{% keyboard_shortcuts_dialog %} {% keyboard_shortcuts_dialog %}
<main class="content-wrapper w-overflow-x-hidden" id="main"> <main class="content-wrapper w-overflow-x-hidden" id="main">
<div class="content"> <div class="content">
{# Always show messages div so it can be appended to by JS #}
<div class="messages" role="status" data-controller="w-messages" data-action="w-messages:add@document->w-messages#add" data-w-messages-added-class="new" data-w-messages-show-class="appear" data-w-messages-show-delay-value="100" data-w-messages-auto-clear-value="8000"> <div class="messages" role="status" data-controller="w-messages" data-action="w-messages:add@document->w-messages#add" data-w-messages-added-class="new" data-w-messages-show-class="appear" data-w-messages-show-delay-value="100" data-w-messages-auto-clear-value="8000">
<ul data-w-messages-target="container"> <ul data-w-messages-target="container">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
{% message_level_tag message as level_tag %} {% message_level_tag message as level_tag %}
{% ifchanged level_tag message.extra_tags message %} <li class="{% message_tags message %}" data-server-rendered>
<li class="{% message_tags message %}">
{% if level_tag == "error" %} {% if level_tag == "error" %}
{% icon name="warning" classname="messages-icon" %} {% icon name="warning" classname="messages-icon" %}
{% elif message.extra_tags == "lock" %} {% elif message.extra_tags == "lock" %}
@@ -26,7 +26,6 @@
{% endif %} {% endif %}
{{ message }} {{ message }}
</li> </li>
{% endifchanged %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</ul> </ul>
@@ -41,6 +40,27 @@
</template> </template>
</div> </div>
{% comment %}
Wagtail's w-messages Stimulus controller only auto-clears messages
added dynamically via JavaScript (the add() method). Server-rendered
messages — the <li> elements above — have no connect() handler and
sit in the DOM forever. This script schedules their removal so they
auto-dismiss after the same timeout used for dynamic messages.
{% endcomment %}
<script>
(function () {
var items = document.querySelectorAll('[data-server-rendered]');
if (!items.length) return;
setTimeout(function () {
items.forEach(function (el) { el.remove(); });
var ul = document.querySelector('[data-w-messages-target="container"]');
if (ul && !ul.children.length) {
document.body.classList.remove('has-messages');
}
}, 8000);
})();
</script>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</main> </main>