|
# retoor <retoor@molodetz.nl>
|
|
|
|
import re
|
|
from playwright.sync_api import expect
|
|
from tests.conftest import (
|
|
BASE_URL,
|
|
assert_share_copies,
|
|
create_post_with_files,
|
|
paste_image,
|
|
)
|
|
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 _png_bytes(color=(0, 128, 255), size=(600, 400)):
|
|
import io
|
|
from PIL import Image
|
|
|
|
buf = io.BytesIO()
|
|
Image.new("RGB", size, color).save(buf, "PNG")
|
|
return buf.getvalue()
|
|
|
|
|
|
def _gif_bytes():
|
|
import io
|
|
from PIL import Image
|
|
|
|
frames = [Image.new("RGB", (40, 40), c) for c in ((255, 0, 0), (0, 0, 255))]
|
|
buf = io.BytesIO()
|
|
frames[0].save(
|
|
buf, "GIF", save_all=True, append_images=frames[1:], duration=120, loop=0
|
|
)
|
|
return buf.getvalue()
|
|
|
|
|
|
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_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_create_post_modal_targets_modal_textarea_on_populated_feed(alice):
|
|
page, _ = alice
|
|
create_post(page, "devlog", "Seed post so the feed renders comment forms")
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
|
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
|
assert page.locator("textarea[name='content']").count() > 1
|
|
page.locator(".feed-fab").first.click()
|
|
modal_content = page.locator("#create-post-modal textarea[name='content']")
|
|
modal_content.wait_for(state="visible", timeout=10000)
|
|
modal_content.click()
|
|
modal_content.fill("Modal scoped body well over ten characters long.")
|
|
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=Modal scoped body well over ten characters long.")
|
|
|
|
|
|
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")
|
|
with page.expect_response(
|
|
lambda r: "/votes/" in r.url and r.request.method == "POST"
|
|
):
|
|
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")
|
|
with page.expect_response(
|
|
lambda r: "/votes/" in r.url and r.request.method == "POST"
|
|
):
|
|
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()
|
|
with page.expect_response(
|
|
lambda r: "/votes/" in r.url and r.request.method == "POST"
|
|
):
|
|
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 test_mention_first_match_highlighted(alice):
|
|
page, _ = alice
|
|
create_post(page, "random", "Mention highlight test post")
|
|
textarea = page.locator(".comment-form textarea[name='content']").first
|
|
textarea.wait_for(state="visible", timeout=10000)
|
|
textarea.fill("@bob")
|
|
item = page.locator(".mention-dropdown-item:has-text('bob_test')").first
|
|
item.wait_for(state="visible", timeout=5000)
|
|
expect(item).to_have_class(re.compile(r"\bactive\b"))
|
|
|
|
|
|
def test_mention_tab_inserts_first_match(alice):
|
|
page, _ = alice
|
|
create_post(page, "random", "Mention tab insert test post")
|
|
textarea = page.locator(".comment-form textarea[name='content']").first
|
|
textarea.wait_for(state="visible", timeout=10000)
|
|
textarea.fill("@bob")
|
|
page.locator(".mention-dropdown-item:has-text('bob_test')").first.wait_for(
|
|
state="visible", timeout=5000
|
|
)
|
|
textarea.press("Tab")
|
|
expect(textarea).to_have_value(re.compile(r"^@bob_test\s"))
|
|
|
|
|
|
def test_mention_arrow_then_enter_inserts(alice):
|
|
page, _ = alice
|
|
create_post(page, "random", "Mention arrow nav test post")
|
|
textarea = page.locator(".comment-form textarea[name='content']").first
|
|
textarea.wait_for(state="visible", timeout=10000)
|
|
textarea.fill("@bob")
|
|
page.locator(".mention-dropdown-item:has-text('bob_test')").first.wait_for(
|
|
state="visible", timeout=5000
|
|
)
|
|
textarea.press("ArrowDown")
|
|
selected = page.locator(".mention-dropdown-item.active").first
|
|
selected.wait_for(state="visible", timeout=5000)
|
|
username = selected.get_attribute("data-username")
|
|
textarea.press("Enter")
|
|
expect(textarea).to_have_value(re.compile(rf"@{re.escape(username)}\s"))
|
|
|
|
|
|
def test_mention_escape_closes_dropdown(alice):
|
|
page, _ = alice
|
|
create_post(page, "random", "Mention escape test post")
|
|
textarea = page.locator(".comment-form textarea[name='content']").first
|
|
textarea.wait_for(state="visible", timeout=10000)
|
|
textarea.fill("@bob")
|
|
item = page.locator(".mention-dropdown-item:has-text('bob_test')").first
|
|
item.wait_for(state="visible", timeout=5000)
|
|
textarea.press("Escape")
|
|
expect(item).to_be_hidden()
|
|
|
|
|
|
def test_profile_stars_reflect_content_votes(alice):
|
|
page, user = alice
|
|
before = _profile_stars(page, user["username"])
|
|
create_post(page, "devlog", "Reputation contribution post")
|
|
with page.expect_response(
|
|
lambda r: "/votes/" in r.url and r.request.method == "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()
|
|
page.locator(".dialog-overlay.visible .dialog-confirm").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_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()
|
|
page.locator(".dialog-overlay.visible .dialog-confirm").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_across_all_topics(alice):
|
|
page, _ = alice
|
|
topics = ["devlog", "showcase", "question", "rant", "fun", "politics"]
|
|
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_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_paste_image_attaches_in_comment_form(alice):
|
|
page, _ = alice
|
|
create_post(page, "random", "Post for pasted comment attachment")
|
|
page.locator(".comment-form dp-upload .dp-upload-btn").first.wait_for(
|
|
state="visible", timeout=10000
|
|
)
|
|
paste_image(page, ".comment-form textarea[name='content']")
|
|
page.locator(".comment-form dp-upload .dp-upload-count").first.wait_for(
|
|
state="visible", timeout=15000
|
|
)
|
|
expect(
|
|
page.locator(".comment-form dp-upload input[name='attachment_uids']")
|
|
).to_have_value(re.compile(r".+"))
|
|
|
|
|
|
def test_single_image_post_shows_the_original_full_size(alice):
|
|
page, _ = alice
|
|
create_post_with_files(
|
|
page,
|
|
"Post carrying exactly one image",
|
|
[{"name": "shot.png", "mimeType": "image/png", "buffer": _png_bytes()}],
|
|
)
|
|
gallery = page.locator(".attachment-gallery.single")
|
|
gallery.wait_for(state="visible", timeout=10000)
|
|
img = gallery.locator(".gallery-thumb").first
|
|
src = img.get_attribute("src")
|
|
assert "_thumb" not in src, f"hero image served the 200px thumbnail: {src}"
|
|
assert src == img.get_attribute("data-full")
|
|
|
|
|
|
def test_multiple_image_post_keeps_thumbnails(alice):
|
|
page, _ = alice
|
|
create_post_with_files(
|
|
page,
|
|
"Post carrying two images",
|
|
[
|
|
{"name": "one.png", "mimeType": "image/png", "buffer": _png_bytes()},
|
|
{
|
|
"name": "two.png",
|
|
"mimeType": "image/png",
|
|
"buffer": _png_bytes((200, 30, 90)),
|
|
},
|
|
],
|
|
expected_count=2,
|
|
)
|
|
page.locator(".attachment-gallery").first.wait_for(state="visible", timeout=10000)
|
|
assert page.locator(".attachment-gallery.single").count() == 0
|
|
thumbs = page.locator(".attachment-gallery .gallery-thumb")
|
|
assert thumbs.count() == 2
|
|
for i in range(thumbs.count()):
|
|
assert "_thumb" in thumbs.nth(i).get_attribute("src")
|
|
|
|
|
|
def test_animated_gif_post_serves_the_original_file(alice):
|
|
page, _ = alice
|
|
create_post_with_files(
|
|
page,
|
|
"Post carrying an animated gif",
|
|
[{"name": "loop.gif", "mimeType": "image/gif", "buffer": _gif_bytes()}],
|
|
)
|
|
img = page.locator(".attachment-gallery .gallery-thumb").first
|
|
img.wait_for(state="visible", timeout=10000)
|
|
src = img.get_attribute("src")
|
|
assert src.endswith(".gif"), f"animation lost, served {src}"
|
|
|
|
|
|
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 dp-upload .dp-upload-input").first
|
|
file_input.set_input_files(
|
|
{"name": "pic.png", "mimeType": "image/png", "buffer": buf.getvalue()}
|
|
)
|
|
page.locator(".comment-form dp-upload .dp-upload-count").first.wait_for(
|
|
state="visible", timeout=15000
|
|
)
|
|
expect(
|
|
page.locator(".comment-form dp-upload input[name='attachment_uids']")
|
|
).to_have_value(re.compile(r".+"))
|