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