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 @@ and visibility verification across different user roles.
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_user_creates_and_publishes_post_visible_to_guest_and_admin(
user_page: Page,
@@ -41,7 +48,7 @@ def test_user_creates_and_publishes_post_visible_to_guest_and_admin(
"""
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"])
@@ -51,12 +58,8 @@ def test_user_creates_and_publishes_post_visible_to_guest_and_admin(
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]
@@ -117,7 +120,7 @@ def test_post_visibility_policies_across_users(
generator = PostDataGenerator()
draft_data = generator.generate_post()
draft_title = str(draft_data["title"])
draft_title = _unique_title(str(draft_data["title"]))
draft_content = str(draft_data["content"])
draft_tags = ", ".join(draft_data["tags"])
@@ -127,12 +130,8 @@ def test_post_visibility_policies_across_users(
form = PostFormPage(user_page, base_url)
form.fill_form(draft_title, draft_content, draft_tags)
form.save_draft()
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.save_draft()
draft_url = user_page.url
assert "new" not in draft_url, f"Still on form page: {draft_url}"
draft_slug = draft_url.rstrip("/").split("/")[-1]
@@ -143,7 +142,7 @@ def test_post_visibility_policies_across_users(
assert not draft_detail.is_published()
published_data = generator.generate_post()
published_title = str(published_data["title"])
published_title = _unique_title(str(published_data["title"]))
published_content = str(published_data["content"])
published_tags = ", ".join(published_data["tags"])
@@ -152,12 +151,8 @@ def test_post_visibility_policies_across_users(
form = PostFormPage(user_page, base_url)
form.fill_form(published_title, published_content, published_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()
published_url = user_page.url
assert "new" not in published_url, f"Still on form page: {published_url}"
published_slug = published_url.rstrip("/").split("/")[-1]
@@ -190,3 +185,68 @@ def test_post_visibility_policies_across_users(
user2_page.wait_for_selector('[data-testid="error-code"]', timeout=10000)
error_code = user2_page.locator('[data-testid="error-code"]').text_content()
assert error_code == "404"
@pytest.mark.e2e
def test_user_saves_draft_then_publishes_via_edit(
user_page: Page,
guest_page: Page,
base_url: str,
) -> None:
"""Test draft-to-publish flow through the web UI.
Steps:
1. User creates a post and saves it as draft.
2. Verify the post shows Draft status and guest cannot see it.
3. User opens the edit form and clicks Update Post (publish).
4. Verify the post shows Published status and guest can now see it.
Args:
user_page: Playwright page authenticated as regular user.
guest_page: Unauthenticated Playwright page.
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.save_draft()
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.get_status() == "Draft"
guest_home = HomePage(guest_page, base_url)
guest_home.open()
assert guest_home.has_no_post_with_title(title)
detail.edit()
user_page.wait_for_url(
lambda url: f"/web/posts/{slug}/edit" in url,
timeout=15000,
)
edit_form = PostFormPage(user_page, base_url)
with user_page.expect_navigation(wait_until="networkidle"):
edit_form.publish()
user_page.wait_for_selector('[data-testid="post-detail-title"]')
updated_detail = PostDetailPage(user_page, base_url, slug)
assert updated_detail.get_title() == title
assert updated_detail.get_status() == "Published"
guest_home.open()
assert guest_home.has_post_with_title(title)