import re from playwright.sync_api import expect from tests.conftest import BASE_URL, assert_share_copies def create_post(page, topic="random", content="Test post content", title=None): 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) if title: page.fill("#post-title", title) 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_post_detail_page(alice): page, _ = alice create_post(page, "random", "Detail page test content") assert page.is_visible("text=Detail page test content") assert page.is_visible("text=Back to Feed") def test_post_topic_badge(alice): page, _ = alice create_post(page, "question", "Question post for badge check") badge = page.locator(".badge-question") assert badge.is_visible() def test_post_author_info(alice): page, user = alice create_post(page, "fun", "Post with author check") assert page.is_visible(f"text={user['username']}") def test_post_vote_button(alice): page, _ = alice create_post(page, "devlog", "Votable post") vote_btn = page.locator(".post-action-btn.vote-up").first assert vote_btn.is_visible() def test_post_vote_increment(alice): page, _ = alice create_post(page, "showcase", "Vote increment test") page.locator(".post-action-btn.vote-up").first.click() expect(page.locator(".post-vote-count").first).to_have_text("1") def test_post_upvote_voted_state_persists(alice): page, _ = alice create_post(page, "showcase", "Upvote persistence test") page.locator(".post-action-btn.vote-up").first.click() expect(page.locator(".post-vote-count").first).to_have_text("1") page.reload(wait_until="domcontentloaded") expect(page.locator(".post-action-btn.vote-up").first).to_have_class(re.compile(r"\bvoted\b")) assert "voted" not in (page.locator(".post-action-btn.vote-down").first.get_attribute("class") or "") def test_post_downvote_voted_state_persists(alice): page, _ = alice create_post(page, "showcase", "Downvote persistence test") page.locator(".post-action-btn.vote-down").first.click() expect(page.locator(".post-vote-count").first).to_have_text("-1") page.reload(wait_until="domcontentloaded") expect(page.locator(".post-action-btn.vote-down").first).to_have_class(re.compile(r"\bvoted\b")) assert "voted" not in (page.locator(".post-action-btn.vote-up").first.get_attribute("class") or "") def test_comment_voted_state_persists(alice): page, _ = alice create_post(page, "devlog", "Post for comment vote persistence") textarea = page.locator(".comment-form textarea[name='content']") textarea.fill("Comment whose vote should persist") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('Comment whose vote should persist')")).to_be_visible() page.locator(".comment-vote-btn").first.click() expect(page.locator(".comment-vote-btn").first).to_have_class(re.compile(r"\bvoted\b")) page.reload(wait_until="domcontentloaded") expect(page.locator(".comment-vote-btn").first).to_have_class(re.compile(r"\bvoted\b")) def _profile_stars(page, username): page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded") value = page.locator(".profile-stat:has(.profile-stat-label:has-text('Stars')) .profile-stat-value").first return int(value.text_content().strip()) def test_profile_stars_reflect_content_votes(alice): page, user = alice before = _profile_stars(page, user["username"]) create_post(page, "devlog", "Reputation contribution post") page.locator(".post-action-btn.vote-up").first.click() expect(page.locator(".post-vote-count").first).to_have_text("1") after = _profile_stars(page, user["username"]) assert after == before + 1, f"expected stars {before + 1}, got {after}" def test_post_comments_section(alice): page, _ = alice create_post(page, "rant", "Comment section test") assert page.is_visible("text=Comments") assert page.is_visible("textarea[placeholder='Your opinion goes here...']") def test_add_comment(alice): page, _ = alice create_post(page, "random", "Post for commenting") textarea = page.locator(".comment-form textarea[name='content']") textarea.fill("This is a test comment from Playwright") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('This is a test comment from Playwright')")).to_be_visible() def test_comment_voting(alice): page, _ = alice create_post(page, "devlog", "Post for comment voting") textarea = page.locator(".comment-form textarea[name='content']") textarea.fill("Votable comment") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('Votable comment')")).to_be_visible() vote_btns = page.locator(".comment-vote-btn") upvote = vote_btns.first upvote.click() expect(vote_btns.first).to_have_class(re.compile(r"\bvoted\b")) def test_delete_own_comment(alice): page, _ = alice create_post(page, "fun", "Post for comment deletion") textarea = page.locator(".comment-form textarea[name='content']") textarea.fill("Comment to delete") page.locator(".comment-form button:has-text('Post')").click() comment = page.locator(".comment-text:has-text('Comment to delete')") expect(comment).to_be_visible() delete_btn = page.locator(".comment-action-btn:has-text('Delete')").last expect(delete_btn).to_be_visible() delete_btn.click() expect(comment).to_have_count(0) def test_comment_form_elements(alice): page, _ = alice create_post(page, "random", "Post for checking comment form") assert page.is_visible("textarea[placeholder='Your opinion goes here...']") def test_share_button(alice): page, _ = alice create_post(page, "showcase", "Share button check") assert_share_copies(page, "/posts/") def test_back_to_feed_link(alice): page, _ = alice create_post(page, "random", "Back link test") back = page.locator("a:has-text('Back to Feed')") assert back.is_visible() back.click() page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded") def test_multiple_comments_on_post(alice): page, _ = alice create_post(page, "devlog", "Post with many comments") for i in range(3): page.locator(".comment-form textarea[name='content']").fill(f"Comment number {i + 1}") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(f".comment-text:has-text('Comment number {i + 1}')")).to_be_visible() assert page.is_visible("text=Comment number 1") assert page.is_visible("text=Comment number 3") def test_comment_and_vote_then_delete(alice): page, _ = alice create_post(page, "showcase", "Full comment lifecycle post") page.locator(".comment-form textarea[name='content']").fill("Lifecycle comment") page.locator(".comment-form button:has-text('Post')").click() comment = page.locator(".comment-text:has-text('Lifecycle comment')") expect(comment).to_be_visible() vote_up = page.locator(".comment-vote-btn").first vote_up.click() expect(page.locator(".comment-vote-btn").first).to_have_class(re.compile(r"\bvoted\b")) delete_btn = page.locator(".comment-action-btn:has-text('Delete')").last expect(delete_btn).to_be_visible() delete_btn.click() expect(comment).to_have_count(0) def test_comment_reply_inline_form(alice): page, _ = alice create_post(page, "devlog", "Post for inline reply") page.locator(".comment-form textarea[name='content']").fill("Parent comment here") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('Parent comment here')")).to_be_visible() page.locator(".comment [data-action='reply']").first.click() reply_form = page.locator(".comment-reply-form").first expect(reply_form).to_be_visible() reply_form.locator("textarea[name='content']").fill("Inline reply text") reply_form.locator("button:has-text('Post')").click() expect(page.locator(".comment-replies .comment-text:has-text('Inline reply text')")).to_be_visible() def test_comment_reply_toggle_and_cancel(alice): page, _ = alice create_post(page, "devlog", "Post for reply toggle") page.locator(".comment-form textarea[name='content']").fill("Toggle parent") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('Toggle parent')")).to_be_visible() reply_btn = page.locator(".comment [data-action='reply']").first reply_btn.click() expect(page.locator(".comment-reply-form")).to_have_count(1) reply_btn.click() expect(page.locator(".comment-reply-form")).to_have_count(0) reply_btn.click() expect(page.locator(".comment-reply-form")).to_have_count(1) page.locator(".comment-reply-cancel").first.click() expect(page.locator(".comment-reply-form")).to_have_count(0) def test_comment_create_scrolls_to_anchor(alice): page, _ = alice create_post(page, "devlog", "Post for comment anchor") page.locator(".comment-form textarea[name='content']").fill("Anchor comment text") page.locator(".comment-form button:has-text('Post')").click() expect(page.locator(".comment-text:has-text('Anchor comment text')")).to_be_visible() assert "#comment-" in page.url comment_uid = page.url.split("#comment-")[1] page.wait_for_selector(f"#comment-{comment_uid}.comment-highlight", timeout=5000) expect(page.locator(f"#comment-{comment_uid}")).to_contain_text("Anchor comment text") def test_post_edit_button(alice): page, _ = alice page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") page.locator(".feed-fab").click() page.fill("#post-content", "Post to be edited later") page.fill("#post-title", "Editable Post") page.locator("#create-post-modal button.btn-primary:has-text('Post')").click() page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded") edit_btn = page.locator("button:has-text('Edit')") assert edit_btn.is_visible() def test_post_edit_modal(alice): page, _ = alice page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") page.locator(".feed-fab").click() page.fill("#post-content", "Post for edit modal test") page.fill("#post-title", "Edit Modal Test") page.locator("#create-post-modal button.btn-primary:has-text('Post')").click() page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded") page.click("button:has-text('Edit')") assert page.is_visible("h3:has-text('Edit Post')") def test_post_edit_submit(alice): page, _ = alice page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") page.locator(".feed-fab").click() page.fill("#post-content", "Original content for editing test") page.fill("#post-title", "Original Title") page.locator("#create-post-modal button.btn-primary:has-text('Post')").click() page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded") page.click("button:has-text('Edit')") page.fill("#edit-title", "Edited Title") page.fill("#edit-content", "Edited content for the post") page.click("button:has-text('Save Changes')") expect(page.locator(".post-detail-title:has-text('Edited Title')")).to_be_visible() def test_post_across_all_topics(alice): page, _ = alice topics = ["devlog", "showcase", "question", "rant", "fun", "signals"] for topic in topics: create_post(page, topic, f"Topic test post for {topic}") badge = page.locator(f".badge-{topic}") expect(badge).to_be_visible() def test_delete_own_post(alice): page, _ = alice create_post(page, "random", "Post to be deleted permanently here") post_url = page.url page.once("dialog", lambda d: d.accept()) page.locator(".post-action-btn:has-text('Delete')").click() page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded") resp = page.goto(post_url, wait_until="domcontentloaded") assert resp.status == 404 def test_content_rendering_markdown_and_highlight(alice): page, _ = alice create_post(page, "random", "Hello **world bold** text\n\n```python\nprint('hi')\n```") page.locator(".rendered-content strong").first.wait_for(state="visible") assert page.locator(".rendered-content pre code").count() >= 1 def test_mention_autocomplete(alice): page, _ = alice create_post(page, "random", "Post for mention autocomplete here") ta = page.locator(".comment-form textarea[data-mention]").first ta.click() ta.press_sequentially("@bob") item = page.locator(".mention-dropdown-item").first item.wait_for(state="visible") assert "bob" in item.inner_text().lower() def test_emoji_picker_opens(alice): page, _ = alice create_post(page, "random", "Post for emoji picker here") page.locator(".emoji-toggle-btn").first.click() page.locator(".emoji-picker-wrapper").first.wait_for(state="visible") assert page.locator("emoji-picker").first.is_visible() def test_attachment_upload_ui(alice): import io from PIL import Image page, _ = alice create_post(page, "random", "Post for attachment upload UI") buf = io.BytesIO() Image.new("RGB", (4, 4), (0, 128, 255)).save(buf, "PNG") file_input = page.locator(".comment-form .attachment-upload-container input[type='file']").first file_input.set_input_files({"name": "pic.png", "mimeType": "image/png", "buffer": buf.getvalue()}) page.locator(".attachment-preview[data-uid]").first.wait_for(state="visible", timeout=15000)