All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
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.
191 lines
7.0 KiB
Python
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)
|