fix: resolve review round 2, E2E failures, and mypy error
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / pr-e2e (pull_request) Successful in 1m30s
CI / ci (pull_request) Failing after 1m48s

Review blocker A — form error swap and false success:
- Change HTMX contract so forms target their own container (outerHTML)
  instead of appending to #comments-list
- Use OOB swaps to append approved comments to the correct target
- Add success/error message display inside form templates
- Remove hx-on::after-request handlers (no longer needed)

Review blocker B — reply rendering shape:
- Create _reply.html partial with compact reply markup
- Approved replies via HTMX now use compact template + OOB swap
  into parent's .replies-container
- Reply form errors render inside reply form container

E2E test fixes:
- Update 4 failing tests to wait for inline HTMX messages instead
  of redirect-based URL assertions
- Add aria-label='Comment form errors' to form error display
- Rename test_reply_submission_redirects to
  test_reply_submission_shows_moderation_message

Mypy internal error workaround:
- Add mypy override for apps.comments.views (django-stubs triggers
  internal error on ORM annotate() chain with mypy 1.11.2)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mark
2026-03-03 23:47:12 +00:00
parent 88ce59aecc
commit c01fc14258
8 changed files with 188 additions and 79 deletions

View File

@@ -67,30 +67,37 @@ def _post_comment(client, article, extra=None, htmx=False):
@pytest.mark.django_db
def test_htmx_post_returns_partial_on_success(client, _article):
"""HTMX POST with Turnstile disabled returns moderation notice partial."""
def test_htmx_post_returns_form_with_moderation_on_success(client, _article):
"""HTMX POST with Turnstile disabled returns fresh form + moderation message."""
resp = _post_comment(client, _article, htmx=True)
assert resp.status_code == 200
assert b"awaiting moderation" in resp.content
# Response swaps the form container (contains form + success message)
assert b"comment-form-container" in resp.content
assert "HX-Request" in resp["Vary"]
@pytest.mark.django_db
@override_settings(TURNSTILE_SECRET_KEY="test-secret")
def test_htmx_post_returns_comment_partial_when_turnstile_passes(client, _article):
"""HTMX POST with successful Turnstile returns comment partial for append."""
def test_htmx_post_returns_form_plus_oob_comment_when_approved(client, _article):
"""HTMX POST with successful Turnstile returns fresh form + OOB comment."""
with patch("apps.comments.views._verify_turnstile", return_value=True):
resp = _post_comment(client, _article, extra={"cf-turnstile-response": "tok"}, htmx=True)
assert resp.status_code == 200
assert b"Hello world" in resp.content
assert b"comment-" in resp.content
content = resp.content.decode()
# Fresh form container is the primary response
assert "comment-form-container" in content
assert "Comment posted!" in content
# OOB swap appends the comment to #comments-list
assert "hx-swap-oob" in content
assert "Hello world" in content
comment = Comment.objects.get()
assert comment.is_approved is True
@pytest.mark.django_db
def test_htmx_post_returns_form_with_errors_on_invalid(client, _article):
"""HTMX POST with invalid data returns form partial with HTTP 200 (HTMX 2 requires 2xx for swap)."""
"""HTMX POST with invalid data returns form with errors (HTTP 200)."""
cache.clear()
resp = client.post(
"/comments/post/",
@@ -98,10 +105,43 @@ def test_htmx_post_returns_form_with_errors_on_invalid(client, _article):
HTTP_HX_REQUEST="true",
)
assert resp.status_code == 200
assert b"comment-form-container" in resp.content
assert b"Comment form errors" in resp.content
assert "HX-Request" in resp["Vary"]
assert Comment.objects.count() == 0
@pytest.mark.django_db
@override_settings(TURNSTILE_SECRET_KEY="test-secret")
def test_htmx_reply_returns_oob_reply_when_approved(client, _article, approved_comment):
"""Approved reply via HTMX returns compact reply partial via OOB swap."""
cache.clear()
with patch("apps.comments.views._verify_turnstile", return_value=True):
resp = client.post(
"/comments/post/",
{
"article_id": _article.id,
"parent_id": approved_comment.id,
"author_name": "Replier",
"author_email": "r@r.com",
"body": "Nice reply",
"honeypot": "",
"cf-turnstile-response": "tok",
},
HTTP_HX_REQUEST="true",
)
content = resp.content.decode()
assert resp.status_code == 200
# OOB targets the parent's replies-container
assert f"#comment-{approved_comment.id}" in content
assert "hx-swap-oob" in content
# Reply uses compact markup (no nested reply form)
assert "Reply posted!" in content
reply = Comment.objects.exclude(pk=approved_comment.pk).get()
assert reply.parent_id == approved_comment.id
assert reply.is_approved is True
@pytest.mark.django_db
def test_non_htmx_post_still_redirects(client, _article):
"""Non-HTMX POST continues to redirect (progressive enhancement)."""