import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from playwright.sync_api import expect
from tests.conftest import BASE_URL, assert_share_copies
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def _seed_posts(count):
owner = str(uuid4())
topic = f"pag{owner[:8]}"
get_table("users").insert({
"uid": owner, "username": f"pag_{owner[:8]}", "email": f"{owner[:8]}@test.devplace",
"password_hash": "x", "role": "Member", "is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
})
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
posts = get_table("posts")
for i in range(count):
uid = str(uuid4())
content = f"Paginated post {i}"
posts.insert({
"uid": uid, "user_uid": owner, "slug": make_combined_slug(content, uid),
"title": None, "content": content, "topic": topic, "project_uid": None,
"image": None, "stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
})
return topic
def _seed_post_with_comments(comment_texts, topic="devlog"):
owner = str(uuid4())
get_table("users").insert({
"uid": owner, "username": f"seed_{owner[:8]}", "email": f"{owner[:8]}@seed.devplace",
"password_hash": "x", "role": "Member", "is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
})
post_uid = str(uuid4())
marker = f"seedpost-{post_uid[:8]}"
get_table("posts").insert({
"uid": post_uid, "user_uid": owner, "slug": make_combined_slug(marker, post_uid),
"title": None, "content": marker, "topic": topic, "project_uid": None,
"image": None, "stars": 0, "created_at": datetime.now(timezone.utc).isoformat(),
})
comments = get_table("comments")
base = datetime.now(timezone.utc)
for i, text in enumerate(comment_texts):
comments.insert({
"uid": str(uuid4()), "target_type": "post", "target_uid": post_uid, "post_uid": post_uid,
"user_uid": owner, "content": text, "parent_uid": None,
"created_at": (base + timedelta(seconds=i)).isoformat(),
})
return marker, post_uid
def _create_post_ui(page, content, topic="devlog"):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
page.locator(".feed-fab").first.click()
page.check(f"input[value='{topic}']")
page.fill("#post-content", content)
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def test_feed_vote_voted_state_persists(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").first.click()
page.fill("#post-content", "Feed vote persistence test content")
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-action-btn.vote-up").first.click()
expect(page.locator(".post-vote-count").first).to_have_text("1")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
expect(page.locator(".post-action-btn.vote-up").first).to_have_class(re.compile(r"\bvoted\b"))
def test_feed_card_share_button(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").first.click()
page.fill("#post-content", "Feed share button test content")
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert_share_copies(page, "/posts/")
assert "/feed" in page.url
def test_feed_page_loads(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Topics")
assert page.is_visible("text=All")
assert page.is_visible("text=Devlog")
assert page.is_visible("text=Showcase")
def test_feed_nav_tabs(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("a:has-text('All')")
assert page.is_visible("a:has-text('Trending')")
assert page.is_visible("a:has-text('Recent')")
assert page.is_visible("a:has-text('Following')")
def test_feed_tab_switching(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.click("a:has-text('Trending')")
page.wait_for_url(f"{BASE_URL}/feed?tab=trending", wait_until="domcontentloaded")
page.click("a:has-text('Recent')")
page.wait_for_url(f"{BASE_URL}/feed?tab=recent", wait_until="domcontentloaded")
def test_feed_topic_filter_sidebar(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
topics = ["Devlog", "Showcase", "Question", "Rant", "Fun"]
for topic in topics:
link = page.locator(f"a:has-text('{topic}')").first
assert link.is_visible()
def test_feed_topic_filter_navigation(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.click("a:has-text('Devlog')")
page.wait_for_url(f"{BASE_URL}/feed?topic=devlog", wait_until="domcontentloaded")
def test_feed_community_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Total Members")
assert page.is_visible("text=Posts Today")
assert page.is_visible("text=Total Projects")
def test_feed_top_authors(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Top Authors")
def test_feed_daily_topic(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Daily Topic")
def test_create_post_fab(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
fab = page.locator(".feed-fab")
assert fab.is_visible()
fab.click()
assert page.is_visible("text=Create New Post")
assert page.is_visible("#post-content")
assert page.is_visible("#create-post-modal button:has-text('Post')")
def test_create_post_devlog(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").click()
page.check("input[value='devlog']")
page.fill("#post-content", "This is a test devlog post created by Playwright")
page.fill("#post-title", "Test Devlog Title")
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
assert page.is_visible("text=Test Devlog Title")
assert page.is_visible("text=This is a test devlog post created by Playwright")
def test_create_post_showcase(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").click()
page.check("input[value='showcase']")
page.fill("#post-content", "Check out my new project showcase")
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def test_create_post_without_title(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").click()
page.check("input[value='rant']")
page.fill("#post-content", "Rant without a title")
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
assert page.is_visible("text=Rant without a title")
def test_feed_topnav_navigation(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.click("a:has-text('Projects')")
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.click("a:has-text('Home')")
page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")
def test_topnav_notification_bell(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
bell = page.locator(".topnav-icon[href='/notifications']")
assert bell.is_visible()
def test_topnav_user_menu(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible(f"text={user['username']}")
def test_feed_inline_comment(alice):
page, _ = alice
marker = f"inline-{uuid4().hex[:8]}"
_create_post_ui(page, f"{marker} post with inline comment test")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
inline_form = card.locator(".comment-form")
expect(inline_form).to_be_visible()
inline_form.locator("textarea[name='content']").fill("Inline comment from feed")
inline_form.locator("button.comment-form-submit").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
expect(page.locator(".comment-text:has-text('Inline comment from feed')")).to_be_visible()
def test_feed_inline_comment_placeholder(alice):
page, _ = alice
marker = f"placeholder-{uuid4().hex[:8]}"
_create_post_ui(page, f"{marker} placeholder check post")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
inline_input = card.locator(".comment-form textarea[name='content']")
expect(inline_input).to_be_visible()
assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."
def test_feed_shows_last_three_comments(page, app_server):
m = uuid4().hex[:6]
marker, _ = _seed_post_with_comments([f"{m}-one", f"{m}-two", f"{m}-three", f"{m}-four"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
previews = card.locator(".post-card-comments .comment-text")
expect(previews).to_have_count(3)
expect(previews.nth(0)).to_contain_text(f"{m}-two")
expect(previews.nth(1)).to_contain_text(f"{m}-three")
expect(previews.nth(2)).to_contain_text(f"{m}-four")
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{m}-one")
def test_feed_guest_vote_redirects_to_login(page, app_server):
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
card.locator(".post-action-btn.vote-up").first.click()
page.wait_for_url("**/auth/login**", wait_until="domcontentloaded")
assert "next=" in page.url
def test_feed_guest_reply_redirects_to_login(page, app_server):
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
card.locator(".post-card-comments [data-action='reply']").first.click()
page.wait_for_url("**/auth/login**", wait_until="domcontentloaded")
assert "next=" in page.url
def test_feed_signals_topic(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
signals_link = page.locator("a:has-text('Signals')")
assert signals_link.is_visible()
def test_create_post_cancel_modal(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").click()
cancel = page.locator("#create-post-modal button:has-text('Cancel')").first
cancel.click()
modal = page.locator("#create-post-modal")
assert not modal.is_visible()
def test_feed_pagination_first_page(alice):
page, _ = alice
topic = _seed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
assert page.locator(".post-card").count() == 25
assert page.is_visible(".load-more-wrap")
def test_feed_pagination_load_more(alice):
page, _ = alice
topic = _seed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
page.click(".load-more-wrap a")
page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
assert f"topic={topic}" in page.url
assert page.locator(".post-card").count() == 1
assert not page.is_visible(".load-more-wrap")
def test_feed_public_access(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Topics")
assert page.is_visible("text=All")
assert page.is_visible("text=Login")
def test_feed_guest_no_fab(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator(".feed-fab").count() == 0
def test_feed_guest_no_inline_comment(page, app_server):
_seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator(".post-card").count() > 0
assert page.locator(".comment-form").count() == 0