|
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_projects(count):
|
|
owner = str(uuid4())
|
|
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)
|
|
projects = get_table("projects")
|
|
for i in range(count):
|
|
uid = str(uuid4())
|
|
title = f"Pag Project {i}"
|
|
projects.insert({
|
|
"uid": uid, "user_uid": owner, "slug": make_combined_slug(title, uid),
|
|
"title": title, "description": "paginated", "project_type": "software",
|
|
"status": "In Development", "stars": 0,
|
|
"created_at": (base - timedelta(seconds=i)).isoformat(),
|
|
})
|
|
return owner
|
|
|
|
|
|
def _create_project(page, title):
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", title)
|
|
page.fill("#description", "Project for testing")
|
|
page.fill("#release_date", "2026-06-01")
|
|
page.check("input[value='game']")
|
|
page.locator("#platforms-input").fill("PC")
|
|
page.locator("#platforms-input").press("Enter")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_project_vote(alice):
|
|
page, _ = alice
|
|
_create_project(page, "Votable Project")
|
|
star = "form[action*='/votes/project/'] button"
|
|
count = "form[action*='/votes/project/'] .vote-count-value"
|
|
before = int(page.locator(star).first.inner_text().strip("☆ "))
|
|
page.locator(star).first.click()
|
|
expect(page.locator(count).first).to_have_text(str(before + 1))
|
|
after = int(page.locator(star).first.inner_text().strip("☆ "))
|
|
assert after == before + 1
|
|
|
|
|
|
def test_project_voted_state_persists(alice):
|
|
page, _ = alice
|
|
_create_project(page, "Voted State Project")
|
|
star = "form[action*='/votes/project/'] button"
|
|
page.locator(star).first.click()
|
|
page.wait_for_timeout(500)
|
|
page.reload(wait_until="domcontentloaded")
|
|
expect(page.locator(star).first).to_have_class(re.compile(r"\bvoted\b"))
|
|
|
|
|
|
def test_delete_own_project(alice):
|
|
page, _ = alice
|
|
_create_project(page, "Deletable Project XYZ")
|
|
proj_url = page.url
|
|
page.once("dialog", lambda d: d.accept())
|
|
page.locator("form[action*='/projects/delete/'] button").click()
|
|
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
resp = page.goto(proj_url, wait_until="domcontentloaded")
|
|
assert resp.status == 404
|
|
|
|
|
|
def test_project_detail_share_button(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "Share Project")
|
|
page.fill("#description", "A project created for the share button test")
|
|
page.fill("#release_date", "2026-06-01")
|
|
page.check("input[value='game']")
|
|
page.locator("#platforms-input").fill("PC")
|
|
page.locator("#platforms-input").press("Enter")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert_share_copies(page, "/projects/")
|
|
|
|
|
|
def test_projects_page_loads(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
assert page.is_visible("h1:has-text('Projects')")
|
|
assert page.is_visible("text=Discover amazing projects")
|
|
|
|
|
|
def test_projects_search_bar(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
search = page.locator("input[placeholder='Search projects...']")
|
|
assert search.is_visible()
|
|
|
|
|
|
def test_projects_tabs(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
tabs = ["Recently Released", "Most Popular", "New This Week"]
|
|
for tab in tabs:
|
|
assert page.is_visible(f"text={tab}")
|
|
|
|
|
|
def test_projects_tab_switch(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.click("a:has-text('Most Popular')")
|
|
page.wait_for_url(f"{BASE_URL}/projects?tab=popular", wait_until="domcontentloaded")
|
|
page.click("a:has-text('New This Week')")
|
|
page.wait_for_url(f"{BASE_URL}/projects?tab=new", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_create_project_button(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
fab = page.locator("#create-project-btn")
|
|
assert fab.is_visible()
|
|
fab.click()
|
|
assert page.is_visible("h3:has-text('Create Project')")
|
|
|
|
|
|
def test_create_project_full(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "Playwright Test Game")
|
|
page.fill("#description", "A game created by Playwright integration tests")
|
|
page.fill("#release_date", "2026-06-01")
|
|
page.check("input[value='game']")
|
|
page.locator("#platforms-input").fill("PC")
|
|
page.locator("#platforms-input").press("Enter")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert page.is_visible("text=Playwright Test Game")
|
|
|
|
|
|
def test_create_project_software(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "CLI Tool")
|
|
page.fill("#description", "A command line tool")
|
|
page.check("input[value='software']")
|
|
page.check("input[value='Released']")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert page.is_visible("text=CLI Tool")
|
|
|
|
|
|
def test_create_project_mobile_app(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "Mobile Messenger")
|
|
page.fill("#description", "A cross-platform messaging app")
|
|
page.check("input[value='mobile_app']")
|
|
page.locator("#platforms-input").fill("iOS")
|
|
page.locator("#platforms-input").press("Enter")
|
|
page.locator("#platforms-input").fill("Android")
|
|
page.locator("#platforms-input").press("Enter")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert page.is_visible("text=Mobile Messenger")
|
|
|
|
|
|
def test_project_search(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "SearchableProject")
|
|
page.fill("#description", "Find me via search")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.fill("input[placeholder='Search projects...']", "SearchableProject")
|
|
page.locator("input[placeholder='Search projects...']").press("Enter")
|
|
page.wait_for_timeout(500)
|
|
assert page.is_visible("text=SearchableProject")
|
|
|
|
|
|
def test_project_cancel_modal(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
cancel = page.locator("button:has-text('Cancel')").first
|
|
cancel.click()
|
|
modal = page.locator("#create-project-modal")
|
|
assert not modal.is_visible()
|
|
|
|
|
|
def test_project_platform_presets(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
presets = page.locator(".platform-preset")
|
|
assert presets.count() >= 5
|
|
presets.first.click()
|
|
tag = page.locator("#platforms-tags .platform-tag")
|
|
assert tag.is_visible()
|
|
|
|
|
|
def test_project_pagination_first_page(alice):
|
|
page, _ = alice
|
|
owner = _seed_projects(26)
|
|
page.goto(f"{BASE_URL}/projects?user_uid={owner}", wait_until="domcontentloaded")
|
|
assert page.locator(".project-card").count() == 25
|
|
assert page.is_visible(".load-more-wrap")
|
|
|
|
|
|
def test_project_pagination_load_more(alice):
|
|
page, _ = alice
|
|
owner = _seed_projects(26)
|
|
page.goto(f"{BASE_URL}/projects?user_uid={owner}", wait_until="domcontentloaded")
|
|
page.click(".load-more-wrap a")
|
|
page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
|
|
assert f"user_uid={owner}" in page.url
|
|
assert page.locator(".project-card").count() == 1
|
|
assert not page.is_visible(".load-more-wrap")
|
|
|
|
|
|
def test_project_pagination_count_reflects_total(alice):
|
|
page, _ = alice
|
|
owner = _seed_projects(26)
|
|
page.goto(f"{BASE_URL}/projects?user_uid={owner}", wait_until="domcontentloaded")
|
|
count_text = page.text_content(".projects-count")
|
|
assert "Showing 25 of 26 projects" in count_text
|
|
|
|
|
|
def test_projects_count(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
count_text = page.text_content(".projects-count")
|
|
assert "Showing" in count_text
|
|
assert "projects" in count_text
|
|
|
|
|
|
def test_project_detail_page(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "DetailTestProject")
|
|
page.fill("#description", "Project detail page test")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert page.is_visible("text=DetailTestProject")
|
|
assert page.is_visible("text=Back to Projects")
|
|
|
|
|
|
def test_project_detail_unauth(page):
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_project_detail_back_link(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "BackLinkProject")
|
|
page.fill("#description", "Test back link")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
back = page.locator("a:has-text('Back to Projects')")
|
|
assert back.is_visible()
|
|
back.click()
|
|
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_project_comments_form_visible(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "CommentProject")
|
|
page.fill("#description", "Project for testing comments")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
assert page.is_visible("text=Comments")
|
|
assert page.is_visible("text=No comments yet")
|
|
assert page.is_visible("textarea[name='content']")
|
|
|
|
|
|
def test_project_comment_create(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "CommentCreateProj")
|
|
page.fill("#description", "Project for creating a comment")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
page.fill("textarea[name='content']", "Great project!")
|
|
page.click("button:has-text('Post')")
|
|
page.wait_for_timeout(500)
|
|
assert page.is_visible("text=Great project!")
|
|
|
|
|
|
def test_project_comment_reply(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "CommentReplyProj")
|
|
page.fill("#description", "Project for testing reply")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
page.fill("textarea[name='content']", "First comment")
|
|
page.click("button:has-text('Post')")
|
|
page.wait_for_timeout(500)
|
|
assert page.is_visible("text=First comment")
|
|
page.click("button:has-text('Reply')")
|
|
reply_form = page.locator(".comment-reply-form").first
|
|
reply_form.locator("textarea[name='content']").fill("Reply to comment")
|
|
reply_form.locator("button:has-text('Post')").click()
|
|
page.wait_for_timeout(500)
|
|
assert page.is_visible("text=Reply to comment")
|
|
|
|
|
|
def test_project_comment_delete(alice):
|
|
page, _ = alice
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
page.locator("#create-project-btn").click()
|
|
page.fill("#title", "CommentDeleteProj")
|
|
page.fill("#description", "Project for testing delete")
|
|
page.click("button:has-text('Create Project')")
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
page.fill("textarea[name='content']", "Comment to delete")
|
|
page.click("button:has-text('Post')")
|
|
page.wait_for_timeout(500)
|
|
assert page.is_visible("text=Comment to delete")
|
|
page.locator(".comment-action-btn:has-text('Delete')").click()
|
|
page.wait_for_timeout(500)
|
|
assert not page.is_visible("text=Comment to delete")
|