Files
blog.pyaqa.ru/tests/e2e/test_comments.py
Sergey Vanyushkin 7ff3fa0992
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
feat: add comments feature with nested replies and recursive rendering
Implement full comments system: domain entities (Comment, CommentLike),
value objects (CommentContent), use cases (CRUD, like toggle), SQLAlchemy
repository, API v1 endpoints, web UI with comment form and nested replies,
i18n translations (EN/RU/FR/DE), and E2E tests.

Fix nested reply (reply-to-reply) not displaying — the flat reply_comments
dict was only queried for top-level comment IDs, so deeply nested replies
were saved to DB (incrementing comment count) but never rendered. Switch
to a recursive Jinja2 macro that renders any nesting depth.
2026-05-11 15:34:20 +03:00

191 lines
7.0 KiB
Python

"""End-to-end tests for blog post comments.
Tests comment creation, nested replies with @username prefix,
and comment visibility for authenticated users.
"""
from __future__ import annotations
import uuid
import pytest
from playwright.sync_api import Page, expect
from pytfm.generators import PostDataGenerator
from tests.e2e.pages import HomePage, PostDetailPage, PostFormPage
def _unique_title(base: str) -> str:
"""Append a short UUID to a title to avoid slug collisions."""
return f"{base} {uuid.uuid4().hex[:8]}"
@pytest.mark.e2e
def test_create_and_reply_comment(
user_page: Page,
base_url: str,
) -> None:
"""Test TC-E2E-110: Create a top-level comment, reply, and nested reply.
Steps:
1. User creates and publishes a post.
2. User navigates to the post detail page.
3. User clicks "Write a Comment" button.
4. User types a top-level comment and submits it.
5. Page reloads, top-level comment is visible.
6. User clicks "Reply" on the top-level comment.
7. User types a reply and submits it.
8. Page reloads, reply is visible under the parent comment.
9. User clicks "Reply" on the reply (nested reply).
10. User types a nested reply and submits it.
11. Page reloads, nested reply is visible under the reply.
Args:
user_page: Playwright page authenticated as regular user.
base_url: Application base URL.
"""
generator = PostDataGenerator()
post_data = generator.generate_post()
title = _unique_title(str(post_data["title"]))
content = str(post_data["content"])
tags = ", ".join(post_data["tags"])
home = HomePage(user_page, base_url)
home.open()
home.create_post()
form = PostFormPage(user_page, base_url)
form.fill_form(title, content, tags)
with user_page.expect_navigation(wait_until="networkidle"):
form.publish()
current_url = user_page.url
assert "new" not in current_url, f"Still on form page: {current_url}"
slug = current_url.rstrip("/").split("/")[-1]
user_page.wait_for_selector('[data-testid="post-detail-title"]')
detail = PostDetailPage(user_page, base_url, slug)
assert detail.get_title() == title
# Click "Write a Comment" button to show the form
user_page.locator('[data-testid="btn-show-comment-form"]').click()
user_page.wait_for_selector('[data-testid="form-create-comment"]', state="visible")
# Write a top-level comment
comment_text = "This is a top-level comment with enough length for testing."
user_page.locator('[data-testid="input-comment-content"]').fill(comment_text)
# Submit the comment
user_page.locator('[data-testid="submit-comment"]').click()
# Page should reload after successful comment creation
user_page.wait_for_selector('[data-testid="comments-section"]', timeout=15000)
# Verify the top-level comment appears
comment_locator = user_page.locator('[data-testid^="comment-content-"]', has_text=comment_text)
expect(comment_locator.first).to_be_visible(timeout=10000)
# Get the comment ID for the reply button
top_comment = user_page.locator('[data-testid^="comment-"][data-comment-id]').first
comment_id = top_comment.get_attribute("data-comment-id")
# Click Reply on the top-level comment
reply_btn = user_page.locator(f'[data-testid="btn-comment-reply-{comment_id}"]')
reply_btn.click()
# The comment form should appear with reply info
user_page.wait_for_selector('[data-testid="comment-form-help"]', state="visible")
# Write a reply
reply_text = "This is a reply to the comment."
user_page.locator('[data-testid="input-comment-content"]').fill(reply_text)
# Submit the reply
user_page.locator('[data-testid="submit-comment"]').click()
# Page should reload
user_page.wait_for_selector('[data-testid="comments-section"]', timeout=15000)
# Verify the reply appears in the comment-replies section under the parent
replies_section = user_page.locator(f'[data-testid="comment-replies-{comment_id}"]')
expect(replies_section).to_be_visible(timeout=10000)
# Verify reply text is visible within the replies section
reply_in_section = replies_section.locator(
'[data-testid^="comment-content-"]', has_text=reply_text
)
expect(reply_in_section).to_be_visible(timeout=5000)
# Get the reply's comment ID for the nested reply
reply_element = replies_section.locator('[data-testid^="comment-"][data-comment-id]').first
reply_id = reply_element.get_attribute("data-comment-id")
# Click Reply on the reply (nested reply)
user_page.locator(f'[data-testid="btn-comment-reply-{reply_id}"]').click()
user_page.wait_for_selector('[data-testid="comment-form-help"]', state="visible")
# Write a nested reply
nested_text = "This is a reply to the reply."
user_page.locator('[data-testid="input-comment-content"]').fill(nested_text)
# Submit the nested reply
user_page.locator('[data-testid="submit-comment"]').click()
# Page should reload
user_page.wait_for_selector('[data-testid="comments-section"]', timeout=15000)
# Verify the nested reply appears in the comment-replies section under the reply
nested_replies = user_page.locator(f'[data-testid="comment-replies-{reply_id}"]')
expect(nested_replies).to_be_visible(timeout=10000)
# Verify nested reply text is visible
nested_in_section = nested_replies.locator(
'[data-testid^="comment-content-"]', has_text=nested_text
)
expect(nested_in_section).to_be_visible(timeout=5000)
@pytest.mark.e2e
def test_guest_cannot_comment(
guest_page: Page,
user_page: Page,
base_url: str,
) -> None:
"""Test TC-E2E-112: Guest cannot see the comment form.
Steps:
1. User creates and publishes a post.
2. Guest opens the post detail page.
3. Verify guest cannot see the "Write a Comment" button.
Args:
guest_page: Unauthenticated Playwright page.
user_page: Playwright page authenticated as regular user.
base_url: Application base URL.
"""
generator = PostDataGenerator()
post_data = generator.generate_post()
title = _unique_title(str(post_data["title"]))
content = str(post_data["content"])
tags = ", ".join(post_data["tags"])
home = HomePage(user_page, base_url)
home.open()
home.create_post()
form = PostFormPage(user_page, base_url)
form.fill_form(title, content, tags)
with user_page.expect_navigation(wait_until="networkidle"):
form.publish()
current_url = user_page.url
assert "new" not in current_url, f"Still on form page: {current_url}"
slug = current_url.rstrip("/").split("/")[-1]
# Guest opens the published post
guest_detail = PostDetailPage(guest_page, base_url, slug)
guest_detail.open()
guest_page.wait_for_selector('[data-testid="post-detail-title"]')
# Verify guest cannot see comment form or button
comment_btn = guest_page.locator('[data-testid="btn-show-comment-form"]')
expect(comment_btn).to_have_count(0)