test(e2e): add TC-E2E-003/004/005/007/008/009/010 — delete, pagination, errors, profile, theme

- test_post_deletion.py: user delete own, admin delete any, 403 for other's
- test_pagination.py: navigation across pages, boundary on last page
- test_errors.py: 404 nonexistent post, 404 for other user's draft
- test_post_lifecycle.py: draft-to-publish via edit flow
- test_post_ownership.py: user can edit own post
- test_profile_and_theme.py: profile page rendering, theme toggle with localStorage
- fix(web): remove infinite pagination for USER role (routes.py)
- fix(e2e): stabilize all publish() calls with expect_navigation
- fix(e2e): add _unique_title() to avoid slug collisions at scale
- docs: update FEATURE_POST_LIFECYCLE.md and TEST_MODEL.md coverage
This commit is contained in:
2026-05-08 20:25:01 +03:00
parent 714342f5ac
commit cf4982c0e5
10 changed files with 783 additions and 48 deletions

View File

@@ -6,6 +6,8 @@ cannot edit posts they do not own.
from __future__ import annotations
import uuid
import pytest
from playwright.sync_api import Page
from pytfm.generators import PostDataGenerator
@@ -13,6 +15,11 @@ 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_admin_can_edit_any_post(
user_page: Page,
@@ -34,7 +41,7 @@ def test_admin_can_edit_any_post(
"""
generator = PostDataGenerator()
post_data = generator.generate_post()
title = str(post_data["title"])
title = _unique_title(str(post_data["title"]))
content = str(post_data["content"])
tags = ", ".join(post_data["tags"])
@@ -44,12 +51,8 @@ def test_admin_can_edit_any_post(
form = PostFormPage(user_page, base_url)
form.fill_form(title, content, tags)
form.publish()
user_page.wait_for_url(
lambda url: "/web/posts/" in url and "new" not in url,
timeout=15000,
)
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]
@@ -69,13 +72,14 @@ def test_admin_can_edit_any_post(
)
new_data = generator.generate_post()
new_title = str(new_data["title"])
new_title = _unique_title(str(new_data["title"]))
new_content = str(new_data["content"])
new_tags = ", ".join(new_data["tags"])
admin_form = PostFormPage(admin_page, base_url)
admin_form.fill_form(new_title, new_content, new_tags)
admin_form.publish()
with admin_page.expect_navigation(wait_until="networkidle"):
admin_form.publish()
admin_page.wait_for_selector(
'[data-testid="post-detail-title"]',
@@ -87,6 +91,72 @@ def test_admin_can_edit_any_post(
assert updated_status == "Published"
@pytest.mark.e2e
def test_user_can_edit_own_post(
user_page: Page,
base_url: str,
) -> None:
"""Test that a user can edit their own post.
Steps:
1. User creates and publishes a post.
2. User opens the post detail page and clicks edit.
3. User changes the title and saves.
4. Verify the post detail shows the updated title.
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
assert detail.can_edit()
detail.edit()
user_page.wait_for_url(
lambda url: f"/web/posts/{slug}/edit" in url,
timeout=15000,
)
new_data = generator.generate_post()
new_title = _unique_title(str(new_data["title"]))
new_content = str(new_data["content"])
new_tags = ", ".join(new_data["tags"])
edit_form = PostFormPage(user_page, base_url)
edit_form.fill_form(new_title, new_content, new_tags)
with user_page.expect_navigation(wait_until="networkidle"):
edit_form.publish()
user_page.wait_for_selector(
'[data-testid="post-detail-title"]',
timeout=15000,
)
updated_title = user_page.locator('[data-testid="post-detail-title"]').text_content()
assert updated_title == new_title
updated_status = user_page.locator('[data-testid="post-detail-status"]').text_content()
assert updated_status == "Published"
@pytest.mark.e2e
def test_user_cannot_edit_other_users_post(
user_page: Page,
@@ -109,7 +179,7 @@ def test_user_cannot_edit_other_users_post(
"""
generator = PostDataGenerator()
post_data = generator.generate_post()
title = str(post_data["title"])
title = _unique_title(str(post_data["title"]))
content = str(post_data["content"])
tags = ", ".join(post_data["tags"])
@@ -119,12 +189,8 @@ def test_user_cannot_edit_other_users_post(
form = PostFormPage(user_page, base_url)
form.fill_form(title, content, tags)
form.publish()
user_page.wait_for_url(
lambda url: "/web/posts/" in url and "new" not in url,
timeout=15000,
)
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]