2026-05-30 20:16:39 +02:00
|
|
|
import re
|
2026-06-05 05:36:18 +02:00
|
|
|
from uuid import uuid4
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
2026-05-30 20:16:39 +02:00
|
|
|
|
|
|
|
|
from playwright.sync_api import expect
|
|
|
|
|
|
2026-05-23 03:21:55 +02:00
|
|
|
from tests.conftest import BASE_URL, assert_share_copies
|
2026-06-05 05:36:18 +02:00
|
|
|
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
|
2026-05-23 03:21:55 +02:00
|
|
|
|
|
|
|
|
|
2026-05-30 20:16:39 +02:00
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:21:55 +02:00
|
|
|
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
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_page_loads(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.click("a:has-text('Trending')")
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/feed?tab=trending", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.click("a:has-text('Recent')")
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/feed?tab=recent", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_topic_filter_sidebar(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.click("a:has-text('Devlog')")
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/feed?topic=devlog", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_community_stats(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
assert page.is_visible("text=Top Authors")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_daily_topic(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
assert page.is_visible("text=Daily Topic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_post_fab(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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")
|
2026-05-11 07:02:06 +02:00
|
|
|
assert page.is_visible("#create-post-modal button:has-text('Post')")
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_post_devlog(alice):
|
|
|
|
|
page, _ = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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")
|
2026-05-11 07:02:06 +02:00
|
|
|
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
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
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.locator(".feed-fab").click()
|
|
|
|
|
page.check("input[value='showcase']")
|
|
|
|
|
page.fill("#post-content", "Check out my new project showcase")
|
2026-05-11 07:02:06 +02:00
|
|
|
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_post_without_title(alice):
|
|
|
|
|
page, _ = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.locator(".feed-fab").click()
|
|
|
|
|
page.check("input[value='rant']")
|
|
|
|
|
page.fill("#post-content", "Rant without a title")
|
2026-05-11 07:02:06 +02:00
|
|
|
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
assert page.is_visible("text=Rant without a title")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_topnav_navigation(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.click("a:has-text('Projects')")
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.click("a:has-text('Home')")
|
2026-05-11 00:41:41 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_topnav_notification_bell(alice):
|
|
|
|
|
page, _ = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-23 10:16:56 +02:00
|
|
|
bell = page.locator(".topnav-icon[href='/notifications']")
|
2026-05-10 09:08:12 +02:00
|
|
|
assert bell.is_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_topnav_user_menu(alice):
|
|
|
|
|
page, user = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
assert page.is_visible(f"text={user['username']}")
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 07:02:06 +02:00
|
|
|
def test_feed_inline_comment(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", "Post with inline comment test " + "x" * 30)
|
|
|
|
|
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")
|
|
|
|
|
inline_form = page.locator(".feed-comment-form").first
|
2026-06-02 23:17:51 +02:00
|
|
|
expect(inline_form).to_be_visible()
|
|
|
|
|
inline_form.locator("input[name='content']").fill("Inline comment from feed")
|
|
|
|
|
inline_form.locator(".feed-comment-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()
|
2026-05-11 07:02:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_feed_inline_comment_placeholder(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-06-02 23:17:51 +02:00
|
|
|
page.locator(".feed-fab").first.click()
|
|
|
|
|
page.fill("#post-content", "Placeholder check post " + "x" * 20)
|
|
|
|
|
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")
|
|
|
|
|
inline_input = page.locator(".feed-comment-form input[name='content']").first
|
|
|
|
|
expect(inline_input).to_be_visible()
|
|
|
|
|
assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."
|
2026-05-11 07:02:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 09:08:12 +02:00
|
|
|
def test_create_post_cancel_modal(alice):
|
|
|
|
|
page, _ = alice
|
2026-05-11 00:41:41 +02:00
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
2026-05-10 09:08:12 +02:00
|
|
|
page.locator(".feed-fab").click()
|
2026-05-11 07:02:06 +02:00
|
|
|
cancel = page.locator("#create-post-modal button:has-text('Cancel')").first
|
2026-05-10 09:08:12 +02:00
|
|
|
cancel.click()
|
|
|
|
|
modal = page.locator("#create-post-modal")
|
|
|
|
|
assert not modal.is_visible()
|
2026-05-12 12:45:52 +02:00
|
|
|
|
|
|
|
|
|
2026-06-05 05:36:18 +02:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 12:45:52 +02:00
|
|
|
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):
|
|
|
|
|
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
|
|
|
|
assert page.locator(".feed-comment-form").count() == 0
|