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)