feat: scaffold initial DevPlace project with FastAPI, SQLite auth, and SSR routing

Add complete project skeleton including .gitignore, Makefile, AGENTS.md, config, database init with indexes, main app with router mounting for auth/feed/posts/comments/projects/profile/messages/notifications/votes, Pydantic models for signup/login/post/comment/project/message/profile, and auth router with signup/login page rendering and form handling.
This commit is contained in:
2026-05-10 07:08:12 +00:00
commit 15a3b990fe
54 changed files with 5906 additions and 0 deletions
View File
+158
View File
@@ -0,0 +1,158 @@
import os, sys, tempfile, subprocess, time
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parent.parent
BASE_URL = "http://127.0.0.1:10501"
SCREENSHOT_DIR = Path("/tmp/devplace_test_screenshots")
def save_failure_screenshot(page, test_name):
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
try:
if page and not page.is_closed():
path = str(SCREENSHOT_DIR / f"{test_name}.png")
page.screenshot(path=path, full_page=True)
except Exception:
pass
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
for name in ("page", "alice", "bob"):
if name in item.funcargs:
try:
obj = item.funcargs[name]
if name in ("alice", "bob"):
obj = obj[0]
save_failure_screenshot(obj, item.nodeid.replace("::", "_").replace("/", "_"))
except Exception:
pass
@pytest.fixture(scope="session")
def test_db_path():
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
tmp.close()
yield tmp.name
try:
os.unlink(tmp.name)
except OSError:
pass
@pytest.fixture(scope="session")
def app_server(test_db_path):
env = os.environ.copy()
env["DEVPLACE_DATABASE_URL"] = f"sqlite:///{test_db_path}"
env["SECRET_KEY"] = "test-secret-key"
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
"--host", "127.0.0.1", "--port", "10501"],
cwd=str(PROJECT_ROOT), env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
deadline = time.time() + 20
while time.time() < deadline:
try:
import urllib.request
urllib.request.urlopen(f"{BASE_URL}/", timeout=2)
break
except Exception:
time.sleep(0.5)
else:
proc.terminate(); proc.wait()
raise RuntimeError("Server did not start")
yield proc
proc.terminate()
proc.wait(timeout=10)
@pytest.fixture(scope="session")
def playwright_instance():
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
yield p
@pytest.fixture(scope="session")
def browser(playwright_instance):
headless = os.environ.get("PLAYWRIGHT_HEADLESS", "1") == "1"
b = playwright_instance.chromium.launch(
headless=headless, slow_mo=300, args=["--window-size=1400,900"],
)
yield b
b.close()
@pytest.fixture
def browser_context(browser):
ctx = browser.new_context(viewport={"width": 1400, "height": 900})
yield ctx
ctx.close()
@pytest.fixture
def page(browser_context):
p = browser_context.new_page()
p.set_default_timeout(10000)
yield p
p.close()
def signup_user(page, user):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", user["username"])
page.fill("#email", user["email"])
page.fill("#password", user["password"])
page.fill("#confirm_password", user["password"])
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
def login_user(page, user):
page.goto(f"{BASE_URL}/auth/login")
page.fill("#email", user["email"])
page.fill("#password", user["password"])
page.click("button:has-text('Sign in')")
page.wait_for_url("**/feed", timeout=10000)
@pytest.fixture(scope="session")
def seeded_db(app_server):
"""Create test users once at session level using requests directly."""
import urllib.request
import urllib.parse
users = [
("alice_test", "alice@test.devplace", "secret123"),
("bob_test", "bob@test.devplace", "secret456"),
]
for username, email, pw in users:
data = urllib.parse.urlencode({
"username": username, "email": email,
"password": pw, "confirm_password": pw,
}).encode()
req = urllib.request.Request(
f"{BASE_URL}/auth/signup",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
urllib.request.urlopen(req)
return {
"alice": {"username": "alice_test", "email": "alice@test.devplace", "password": "secret123"},
"bob": {"username": "bob_test", "email": "bob@test.devplace", "password": "secret456"},
}
@pytest.fixture
def alice(page, seeded_db):
login_user(page, seeded_db["alice"])
return page, seeded_db["alice"]
@pytest.fixture
def bob(browser, seeded_db):
ctx = browser.new_context(viewport={"width": 1400, "height": 900})
p = ctx.new_page()
p.set_default_timeout(10000)
login_user(p, seeded_db["bob"])
yield p, seeded_db["bob"]
p.close()
ctx.close()
+206
View File
@@ -0,0 +1,206 @@
from tests.conftest import BASE_URL
def test_signup_page_loads(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
assert page.is_visible("h2:has-text('Join the Community')")
assert page.is_visible("#username")
assert page.is_visible("#email")
assert page.is_visible("#password")
assert page.is_visible("#confirm_password")
def test_signup_success(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "fresh_user")
page.fill("#email", "fresh@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
assert page.is_visible("text=fresh_user")
def test_signup_existing_username(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "dup_user")
page.fill("#email", "dup1@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.context.clear_cookies()
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "dup_user")
page.fill("#email", "dup2@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_timeout(300)
assert page.is_visible("text=Username already taken")
def test_signup_existing_email(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "email_dup1")
page.fill("#email", "sameemail@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.context.clear_cookies()
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "email_dup2")
page.fill("#email", "sameemail@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_timeout(300)
assert page.is_visible("text=Email already registered")
def test_signup_password_mismatch(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "mismatch_user")
page.fill("#email", "mismatch@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "different456")
page.click("button:has-text('Create account')")
page.wait_for_timeout(300)
assert page.is_visible("text=Passwords do not match")
def test_signup_short_password(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "shortpw_user")
page.fill("#email", "shortpw@test.devplace")
page.fill("#password", "ab")
page.fill("#confirm_password", "ab")
page.click("button:has-text('Create account')")
page.wait_for_timeout(300)
assert page.is_visible("text=Password must be at least 6 characters")
def test_signup_invalid_username(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "ab")
page.fill("#email", "shortname@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_timeout(300)
assert page.is_visible("text=Username must be between 3 and 32 characters")
def test_login_page_loads(page, app_server):
page.goto(f"{BASE_URL}/auth/login")
assert page.is_visible("h2:has-text('Welcome Back')")
assert page.is_visible("#email")
assert page.is_visible("#password")
assert page.is_visible("button:has-text('Sign in')")
def test_login_success(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "login_user")
page.fill("#email", "login@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.goto(f"{BASE_URL}/auth/logout")
page.goto(f"{BASE_URL}/auth/login")
page.fill("#email", "login@test.devplace")
page.fill("#password", "secret123")
page.click("button:has-text('Sign in')")
page.wait_for_url("**/feed", timeout=10000)
assert page.is_visible("text=login_user")
def test_login_wrong_password(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "wrongpw_user")
page.fill("#email", "wrongpw@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.goto(f"{BASE_URL}/auth/logout")
page.goto(f"{BASE_URL}/auth/login")
page.fill("#email", "wrongpw@test.devplace")
page.fill("#password", "badpassword")
page.click("button:has-text('Sign in')")
page.wait_for_timeout(300)
assert page.is_visible("text=Invalid email or password")
def test_login_nonexistent_email(page, app_server):
page.goto(f"{BASE_URL}/auth/login")
page.fill("#email", "nobody@nowhere.devplace")
page.fill("#password", "secret123")
page.click("button:has-text('Sign in')")
page.wait_for_timeout(300)
assert page.is_visible("text=Invalid email or password")
def test_login_remember_me(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "remember_user")
page.fill("#email", "remember@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.goto(f"{BASE_URL}/auth/logout")
page.goto(f"{BASE_URL}/auth/login")
page.fill("#email", "remember@test.devplace")
page.fill("#password", "secret123")
page.check("input[name='remember_me']")
page.click("button:has-text('Sign in')")
page.wait_for_url("**/feed", timeout=10000)
assert page.is_visible("text=remember_user")
def test_logout(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "logout_user")
page.fill("#email", "logout@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.goto(f"{BASE_URL}/auth/logout")
page.wait_for_url("**/")
assert page.is_visible("text=Join DevPlace Free")
def test_signup_link_from_login(page, app_server):
page.goto(f"{BASE_URL}/auth/login")
page.click("a:has-text('Create new account')")
page.wait_for_url("**/auth/signup")
def test_login_link_from_signup(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.click("a:has-text('Sign in instead')")
page.wait_for_url("**/auth/login")
def test_password_toggle(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
pw_input = page.locator("#password")
toggle = page.locator(".auth-toggle-pw").first
assert pw_input.get_attribute("type") == "password"
toggle.click()
assert pw_input.get_attribute("type") == "text"
toggle.click()
assert pw_input.get_attribute("type") == "password"
+399
View File
@@ -0,0 +1,399 @@
import re
from tests.conftest import BASE_URL
def create_post(page, content, topic="random", title=None):
page.goto(f"{BASE_URL}/feed")
page.locator(".feed-fab").click()
page.check(f"input[value='{topic}']")
page.fill("#post-content", content)
if title:
page.fill("#post-title", title)
page.click("button:has-text('Post')")
page.wait_for_url("**/posts/*", timeout=10000)
def add_comment(page, text):
textarea = page.locator("textarea[name='content']")
textarea.fill(text)
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
def signup(page, username, email, password):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", username)
page.fill("#email", email)
page.fill("#password", password)
page.fill("#confirm_password", password)
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
def test_absurd_full_journey(browser, app_server):
page_a = browser.new_page()
page_a.set_default_timeout(10000)
page_b = browser.new_page()
page_b.set_default_timeout(10000)
# ═══════════════════════════════════════════
# ACT 1 — LANDING PAGE & DISCOVERY
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/")
assert page_a.is_visible("text=Devplace.net")
assert page_a.is_visible("text=The Developer Social Network")
assert page_a.is_visible("text=Join DevPlace Free")
assert page_a.is_visible("text=100% Free Forever")
assert page_a.is_visible("text=Zero Ads")
assert page_a.is_visible("text=No Censorship")
assert page_a.is_visible("text=No Payments")
assert page_a.is_visible("text=Daily Topic")
assert page_a.is_visible("text=Read More")
assert page_a.is_visible("text=Source")
page_b.goto(f"{BASE_URL}")
page_b.click("text=Log In")
page_b.wait_for_url("**/auth/login")
page_b.click("text=Create new account")
page_b.wait_for_url("**/auth/signup")
# ═══════════════════════════════════════════
# ACT 2 — TWO USERS SIGN UP
# ═══════════════════════════════════════════
signup(page_a, "alice_demo", "alice_demo@test.dev", "alice_pass_1")
signup(page_b, "bob_demo", "bob_demo@test.dev", "bob_pass_1")
# ═══════════════════════════════════════════
# ACT 3 — ALICE EXPLORES FEED
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/feed")
assert page_a.is_visible("text=Topics")
assert page_a.is_visible("text=All")
assert page_a.is_visible("text=Trending")
assert page_a.is_visible("text=Recent")
assert page_a.is_visible("text=Following")
assert page_a.is_visible("text=Devlog")
assert page_a.is_visible("text=Showcase")
assert page_a.is_visible("text=Question")
assert page_a.is_visible("text=Rant")
assert page_a.is_visible("text=Fun")
assert page_a.is_visible("text=Total Members")
assert page_a.is_visible("text=Top Authors")
# Switch tabs
page_a.click("a:has-text('Trending')")
page_a.wait_for_url("**/feed?tab=trending")
page_a.click("a:has-text('Recent')")
page_a.wait_for_url("**/feed?tab=recent")
page_a.click("a:has-text('All')")
page_a.wait_for_url("**/feed")
# Click topic filters
page_a.click("a:has-text('Showcase')")
page_a.wait_for_url("**/feed?topic=showcase")
page_a.click("a:has-text('Question')")
page_a.wait_for_url("**/feed?topic=question")
page_a.click("a:has-text('Rant')")
page_a.wait_for_url("**/feed?topic=rant")
page_a.click("a:has-text('Fun')")
page_a.wait_for_url("**/feed?topic=fun")
page_a.click("a:has-text('All')")
page_a.wait_for_url("**/feed")
# ═══════════════════════════════════════════
# ACT 4 — ALICE CREATES 4 POSTS
# ═══════════════════════════════════════════
create_post(page_a, "Just shipped a new feature for my REST API! Took me 3 days but it was worth it.", "devlog", "API Progress Update")
post1_url = page_a.url
create_post(page_a, "Check out this pixel art editor I built in WASM. Runs at 60fps!", "showcase", "Pixel Editor WASM")
post2_url = page_a.url
create_post(page_a, "Does anyone else think that tabs are objectively better than spaces? Fight me.", "rant", "Tabs vs Spaces")
create_post(page_a, "Why do we call it 'tech debt' when really it's just 'we cut corners and now we pay'?", "fun")
# ═══════════════════════════════════════════
# ACT 5 — ALICE CREATES A PROJECT
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/projects")
page_a.locator("#create-project-btn").click()
page_a.fill("#title", "N8Nme")
page_a.fill("#description", "Just powerful automation at your fingertips. Visual workflow builder with 300+ integrations.")
page_a.fill("#release_date", "2026-04-15")
page_a.check("input[value='software']")
page_a.locator("#platforms-input").fill("Linux")
page_a.locator("#platforms-input").press("Enter")
page_a.locator("#platforms-input").fill("Docker")
page_a.locator("#platforms-input").press("Enter")
page_a.locator("#platforms-input").fill("Web")
page_a.locator("#platforms-input").press("Enter")
page_a.click("button:has-text('Create Project')")
page_a.wait_for_timeout(500)
assert page_a.is_visible("text=N8Nme")
# Create a second project
page_a.locator("#create-project-btn").click()
page_a.fill("#title", "DWN")
page_a.fill("#description", "Decentralized web node implementation in Rust. Secure, private, and fast.")
page_a.fill("#release_date", "2026-05-01")
page_a.check("input[value='software']")
page_a.locator("#platforms-input").fill("Linux")
page_a.locator("#platforms-input").press("Enter")
page_a.locator("#platforms-input").fill("Mac")
page_a.locator("#platforms-input").press("Enter")
page_a.check("input[value='Released']")
page_a.click("button:has-text('Create Project')")
page_a.wait_for_timeout(500)
assert page_a.is_visible("text=DWN")
# ═══════════════════════════════════════════
# ACT 6 — ALICE EDITS HER PROFILE
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/profile/alice_demo")
assert page_a.is_visible("text=alice_demo")
assert page_a.is_visible("text=Member")
assert page_a.is_visible("text=Posts")
assert page_a.is_visible("text=Level")
assert page_a.is_visible("text=Stars")
assert page_a.is_visible("text=Progress to next level")
assert page_a.is_visible("text=Bio")
# Edit bio, location, git link, website
page_a.fill("textarea[name='bio']", "Full-stack developer and open source enthusiast. Building the future one commit at a time.")
page_a.fill("input[name='location']", "Berlin, Germany")
page_a.fill("input[name='git_link']", "https://github.com/alice_demo")
page_a.fill("input[name='website']", "https://alicedemo.dev")
page_a.click("button:has-text('Save Changes')")
page_a.wait_for_timeout(500)
assert page_a.is_visible("text=Full-stack developer")
assert page_a.is_visible("text=Berlin, Germany")
assert page_a.is_visible("text=https://github.com/alice_demo")
assert page_a.is_visible("text=https://alicedemo.dev")
# Check profile tabs
page_a.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
page_a.wait_for_timeout(300)
assert page_a.is_visible("text=N8Nme")
assert page_a.is_visible("text=DWN")
page_a.goto(f"{BASE_URL}/profile/alice_demo?tab=posts")
page_a.wait_for_timeout(300)
assert page_a.is_visible("text=API Progress Update")
assert page_a.is_visible("text=Pixel Editor WASM")
# ═══════════════════════════════════════════
# ACT 7 — BOB EXPLORES ALICE'S STUFF
# ═══════════════════════════════════════════
# Bob views Alice's profile
page_b.goto(f"{BASE_URL}/profile/alice_demo")
assert page_b.is_visible("text=alice_demo")
assert page_b.is_visible("text=Full-stack developer")
assert page_b.is_visible("text=Berlin, Germany")
# Bob checks Alice's projects
page_b.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
assert page_b.is_visible("text=N8Nme")
assert page_b.is_visible("text=DWN")
# Bob browses projects page
page_b.goto(f"{BASE_URL}/projects")
assert page_b.is_visible("text=Recently Released")
assert page_b.is_visible("text=Most Popular")
assert page_b.is_visible("text=N8Nme")
assert page_b.is_visible("text=DWN")
# Bob searches for a project
page_b.fill("input[placeholder='Search projects...']", "N8Nme")
page_b.locator("input[placeholder='Search projects...']").press("Enter")
page_b.wait_for_timeout(500)
assert page_b.is_visible("text=N8Nme")
page_b.fill("input[placeholder='Search projects...']", "dwn")
page_b.locator("input[placeholder='Search projects...']").press("Enter")
page_b.wait_for_timeout(500)
assert page_b.is_visible("text=DWN")
# Bob creates his own project
page_b.locator("#create-project-btn").click()
page_b.fill("#title", "ClaudeCode")
page_b.fill("#description", "AI-powered code generation tool for the terminal. Write code with natural language.")
page_b.check("input[value='software']")
page_b.check("input[value='Released']")
page_b.locator("#platforms-input").fill("Linux")
page_b.locator("#platforms-input").press("Enter")
page_b.locator("#platforms-input").fill("Mac")
page_b.locator("#platforms-input").press("Enter")
page_b.click("button:has-text('Create Project')")
page_b.wait_for_timeout(500)
assert page_b.is_visible("text=ClaudeCode")
# Bob views Alice's post
page_b.goto(post1_url)
assert page_b.is_visible("text=API Progress Update")
assert page_b.is_visible("text=Just shipped a new feature")
# Bob votes on Alice's post
vote_btn = page_b.locator("button").filter(has_text=re.compile(r"\+")).first
vote_btn.click()
page_b.wait_for_timeout(500)
# Bob comments on Alice's post
page_b.fill("textarea[name='content']", "Nice work! What framework did you use for the API?")
page_b.click("button:has-text('Post')")
page_b.wait_for_timeout(500)
assert page_b.is_visible("text=Nice work! What framework did you use for the API?")
# Bob votes on Alice's showcase post
page_b.goto(post2_url)
assert page_b.is_visible("text=Pixel Editor WASM")
page_b.fill("textarea[name='content']", "This is incredible! 60fps in WASM is impressive.")
page_b.click("button:has-text('Post')")
page_b.wait_for_timeout(500)
# ═══════════════════════════════════════════
# ACT 8 — ALICE REPLIES TO BOB'S COMMENT
# ═══════════════════════════════════════════
page_a.goto(post1_url)
assert page_a.is_visible("text=Nice work! What framework did you use for the API?")
# Write a reply comment
page_a.fill("textarea[name='content']", "Thanks! I used FastAPI with SQLAlchemy for the backend. The frontend is vanilla JS.")
page_a.click("button:has-text('Post')")
page_a.wait_for_timeout(500)
assert page_a.is_visible("text=Thanks! I used FastAPI")
# Alice votes on Bob's comment
vote_btns = page_a.locator(".comment-vote-btn")
upvote_count = vote_btns.count()
if upvote_count > 0:
vote_btns.first.click()
page_a.wait_for_timeout(500)
# Alice votes on her own post
vote_btn_a = page_a.locator("button").filter(has_text=re.compile(r"\+")).first
vote_btn_a.click()
page_a.wait_for_timeout(500)
# ═══════════════════════════════════════════
# ACT 9 — BOB SENDS ALICE A MESSAGE
# ═══════════════════════════════════════════
# Bob needs Alice's UID to message her. We can find it via the profile URL.
# Use the search in messages to find Alice
page_b.goto(f"{BASE_URL}/messages?search=alice_demo")
page_b.wait_for_timeout(500)
page_b.fill("input[name='content']", "Hey Alice! I saw your pixel editor project, it looks amazing!")
send_btn = page_b.locator("button:has-text('')")
if send_btn.is_visible():
send_btn.click()
page_b.wait_for_timeout(500)
page_b.fill("input[name='content']", "Would you be interested in collaborating on a game project?")
page_b.locator("button:has-text('')").click()
page_b.wait_for_timeout(500)
# ═══════════════════════════════════════════
# ACT 10 — ALICE CHECKS NOTIFICATIONS
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/notifications")
assert page_a.is_visible("h2:has-text('Notifications')")
assert page_a.is_visible("text=bob_demo") or page_a.is_visible("text=commented")
# Mark all as read
mark_all = page_a.locator("button:has-text('Mark all read')")
if mark_all.is_visible():
mark_all.click()
page_a.wait_for_timeout(500)
# ═══════════════════════════════════════════
# ACT 11 — ALICE CHECKS HER MESSAGES
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/messages")
assert page_a.is_visible(".messages-layout")
# Conversation list should show Bob
page_a.goto(f"{BASE_URL}/messages?search=bob_demo")
page_a.wait_for_timeout(500)
# Reply to Bob
msg_input = page_a.locator("input[name='content']")
if msg_input.is_visible():
msg_input.fill("Hey Bob! Thanks for the kind words! I'd love to collaborate.")
send_btn_a = page_a.locator("button:has-text('')")
if send_btn_a.is_visible():
send_btn_a.click()
page_a.wait_for_timeout(500)
# ═══════════════════════════════════════════
# ACT 12 — BOB CHECKS HIS NOTIFICATIONS
# ═══════════════════════════════════════════
page_b.goto(f"{BASE_URL}/notifications")
assert page_b.is_visible("h2:has-text('Notifications')")
# ═══════════════════════════════════════════
# ACT 13 — ALICE BACK ON FEED, UPDATES POST
# ═══════════════════════════════════════════
page_a.goto(post1_url)
assert page_a.is_visible("text=API Progress Update")
# Share button exists
share = page_a.locator("button:has-text('Share')")
assert share.is_visible()
# ═══════════════════════════════════════════
# ACT 14 — LOGOUT BOTH USERS
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/auth/logout")
page_a.wait_for_url("**/")
assert page_a.is_visible("text=Join DevPlace Free")
# Login form still works
page_a.click("text=Log In")
page_a.wait_for_url("**/auth/login")
assert page_a.is_visible("h2:has-text('Welcome Back')")
page_a.fill("#email", "alice_demo@test.dev")
page_a.fill("#password", "alice_pass_1")
page_a.click("button:has-text('Sign in')")
page_a.wait_for_url("**/feed", timeout=10000)
assert page_a.is_visible("text=alice_demo")
page_b.goto(f"{BASE_URL}/auth/logout")
page_b.wait_for_url("**/")
# ═══════════════════════════════════════════
# ACT 15 — FINAL CHECKS
# ═══════════════════════════════════════════
page_a.goto(f"{BASE_URL}/")
page_a.wait_for_url("**/feed")
assert page_a.is_visible("text=alice_demo")
page_a.goto(f"{BASE_URL}/notifications")
assert page_a.is_visible("h2:has-text('Notifications')")
# Back to the feed, everything loads
page_a.goto(f"{BASE_URL}/feed")
assert page_a.is_visible("text=Topics")
# Cleanup
page_a.close()
page_b.close()
print("\n═══════════════════════════════════════════════════")
print(" ABSURD DEMO COMPLETE — 15 ACTS, 0 MISTAKES")
print("═══════════════════════════════════════════════════")
+141
View File
@@ -0,0 +1,141 @@
from tests.conftest import BASE_URL
def test_feed_page_loads(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
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
page.goto(f"{BASE_URL}/feed")
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
page.goto(f"{BASE_URL}/feed")
page.click("a:has-text('Trending')")
page.wait_for_url(f"{BASE_URL}/feed?tab=trending")
page.click("a:has-text('Recent')")
page.wait_for_url(f"{BASE_URL}/feed?tab=recent")
def test_feed_topic_filter_sidebar(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
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
page.goto(f"{BASE_URL}/feed")
page.click("a:has-text('Devlog')")
page.wait_for_url(f"{BASE_URL}/feed?topic=devlog")
def test_feed_community_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
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
page.goto(f"{BASE_URL}/feed")
assert page.is_visible("text=Top Authors")
def test_feed_daily_topic(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
assert page.is_visible("text=Daily Topic")
def test_create_post_fab(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
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")
assert page.is_visible("button:has-text('Post')")
def test_create_post_devlog(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
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")
page.click("button:has-text('Post')")
page.wait_for_url(f"{BASE_URL}/posts/*")
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
page.goto(f"{BASE_URL}/feed")
page.locator(".feed-fab").click()
page.check("input[value='showcase']")
page.fill("#post-content", "Check out my new project showcase")
page.click("button:has-text('Post')")
page.wait_for_url(f"{BASE_URL}/posts/*")
def test_create_post_without_title(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
page.locator(".feed-fab").click()
page.check("input[value='rant']")
page.fill("#post-content", "Rant without a title")
page.click("button:has-text('Post')")
page.wait_for_url(f"{BASE_URL}/posts/*")
assert page.is_visible("text=Rant without a title")
def test_feed_topnav_navigation(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
page.click("a:has-text('Projects')")
page.wait_for_url(f"{BASE_URL}/projects")
page.click("a:has-text('Home')")
page.wait_for_url(f"{BASE_URL}/feed")
def test_topnav_notification_bell(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
bell = page.locator(".topnav-icon").first
assert bell.is_visible()
def test_topnav_user_menu(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
assert page.is_visible(f"text={user['username']}")
def test_create_post_cancel_modal(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
page.locator(".feed-fab").click()
cancel = page.locator("button:has-text('Cancel')").first
cancel.click()
modal = page.locator("#create-post-modal")
assert not modal.is_visible()
+82
View File
@@ -0,0 +1,82 @@
from tests.conftest import BASE_URL
def test_landing_page_loads(page, app_server):
page.goto(f"{BASE_URL}/")
assert "DevPlace" in page.title()
assert page.is_visible("text=Devplace.net")
assert page.is_visible("text=The Developer Social Network")
def test_landing_tagline(page, app_server):
page.goto(f"{BASE_URL}/")
tagline = page.text_content("p")
assert "Track industry shifts" in tagline
def test_landing_join_button(page, app_server):
page.goto(f"{BASE_URL}/")
btn = page.locator("text=Join DevPlace Free")
assert btn.is_visible()
assert btn.get_attribute("href") == "/auth/signup"
def test_landing_features(page, app_server):
page.goto(f"{BASE_URL}/")
features = [
"100% Free Forever",
"Zero Ads",
"No Censorship",
"No Payments",
]
for feature in features:
assert page.is_visible(f"text={feature}")
def test_landing_daily_topic(page, app_server):
page.goto(f"{BASE_URL}/")
assert page.is_visible("text=Daily Topic")
assert page.is_visible("text=Read More")
assert page.is_visible("text=Source")
def test_landing_nav_links(page, app_server):
page.goto(f"{BASE_URL}/")
assert page.is_visible("a:has-text('Home')")
assert page.is_visible("a:has-text('Projects')")
assert page.is_visible("a:has-text('Login')")
assert page.is_visible("a:has-text('Sign Up')")
def test_landing_nav_to_login(page, app_server):
page.goto(f"{BASE_URL}/")
page.click("a:has-text('Login')")
page.wait_for_url(f"{BASE_URL}/auth/login")
assert page.is_visible("h2:has-text('Welcome Back')")
def test_landing_nav_to_signup(page, app_server):
page.goto(f"{BASE_URL}/")
page.click("a:has-text('Sign Up')")
page.wait_for_url(f"{BASE_URL}/auth/signup")
assert page.is_visible("h2:has-text('Join the Community')")
def test_landing_log_in_link(page, app_server):
page.goto(f"{BASE_URL}/")
page.click("text=Log In")
page.wait_for_url(f"{BASE_URL}/auth/login")
def test_landing_authenticated_redirects_to_feed(page, app_server):
page.goto(f"{BASE_URL}/auth/signup")
page.fill("#username", "redirect_user")
page.fill("#email", "redirect@test.devplace")
page.fill("#password", "secret123")
page.fill("#confirm_password", "secret123")
with page.expect_navigation(timeout=10000):
page.click("button:has-text('Create account')")
page.wait_for_url("**/feed", timeout=10000)
page.goto(f"{BASE_URL}/")
page.wait_for_url("**/feed", timeout=10000)
+33
View File
@@ -0,0 +1,33 @@
from tests.conftest import BASE_URL
def test_messages_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/messages")
assert page.is_visible(".messages-layout")
def test_messages_search_input(alice):
page, _ = alice
page.goto(f"{BASE_URL}/messages")
search = page.locator("#message-search")
assert search.is_visible()
assert search.get_attribute("placeholder") == "Search conversations..."
def test_messages_empty_state(alice):
page, _ = alice
page.goto(f"{BASE_URL}/messages")
empty = page.locator("text=No conversations yet")
if empty.is_visible():
pass
else:
assert page.is_visible(".messages-layout")
def test_messages_search_for_bob(alice):
page, _ = alice
page.fill("#message-search", "bob_test")
page.locator("#message-search").press("Enter")
page.wait_for_timeout(500)
assert page.is_visible(".messages-layout")
+30
View File
@@ -0,0 +1,30 @@
from tests.conftest import BASE_URL
def test_notifications_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications")
assert page.is_visible("h2:has-text('Notifications')")
def test_mark_all_read(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications")
mark_all = page.locator("button:has-text('Mark all read')")
if mark_all.is_visible():
mark_all.click()
page.wait_for_timeout(500)
def test_notifications_navigation(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
page.goto(f"{BASE_URL}/notifications")
assert page.is_visible("h2:has-text('Notifications')")
def test_notifications_bell_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed")
bell = page.locator(".topnav-icon").first
assert bell.is_visible()
+154
View File
@@ -0,0 +1,154 @@
from tests.conftest import BASE_URL
def create_post(page, topic="random", content="Test post content", title=None):
page.goto(f"{BASE_URL}/feed")
page.locator(".feed-fab").click()
page.check(f"input[value='{topic}']")
page.fill("#post-content", content)
if title:
page.fill("#post-title", title)
page.click("button:has-text('Post')")
page.wait_for_url(f"{BASE_URL}/posts/*")
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("button:has-text('+0')").first
assert vote_btn.is_visible()
def test_post_vote_increment(alice):
page, _ = alice
create_post(page, "showcase", "Vote increment test")
vote_btn = page.locator("button").filter(has_text="+").first
vote_btn.click()
page.wait_for_timeout(500)
content = page.locator(".post-detail-actions").text_content()
assert "+1" in content
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("textarea[name='content']")
textarea.fill("This is a test comment from Playwright")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
assert page.is_visible("text=This is a test comment from Playwright")
def test_comment_voting(alice):
page, _ = alice
create_post(page, "devlog", "Post for comment voting")
textarea = page.locator("textarea[name='content']")
textarea.fill("Votable comment")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
vote_btns = page.locator(".comment-vote-btn")
upvote = vote_btns.first
upvote.click()
page.wait_for_timeout(500)
def test_delete_own_comment(alice):
page, _ = alice
create_post(page, "fun", "Post for comment deletion")
textarea = page.locator("textarea[name='content']")
textarea.fill("Comment to delete")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
delete_btn = page.locator("button:has-text('Delete')")
if delete_btn.is_visible():
delete_btn.click()
page.wait_for_timeout(500)
assert not delete_btn.is_visible()
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")
share = page.locator("button:has-text('Share')")
assert share.is_visible()
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")
def test_multiple_comments_on_post(alice):
page, _ = alice
create_post(page, "devlog", "Post with many comments")
for i in range(3):
page.locator("textarea[name='content']").fill(f"Comment number {i + 1}")
page.click("button:has-text('Post')")
page.wait_for_timeout(300)
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("textarea[name='content']").fill("Lifecycle comment")
page.click("button:has-text('Post')")
page.wait_for_timeout(300)
assert page.is_visible("text=Lifecycle comment")
vote_up = page.locator(".comment-vote-btn").first
vote_up.click()
page.wait_for_timeout(300)
delete_btn = page.locator("button:has-text('Delete')")
if delete_btn.is_visible():
delete_btn.click()
page.wait_for_timeout(300)
def test_post_across_all_topics(alice):
page, _ = alice
topics = ["devlog", "showcase", "question", "rant", "fun"]
for topic in topics:
create_post(page, topic, f"Topic test post for {topic}")
badge = page.locator(f".badge-{topic}")
assert badge.is_visible()
page.wait_for_timeout(200)
+139
View File
@@ -0,0 +1,139 @@
from tests.conftest import BASE_URL
def test_profile_page_loads(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible(f"text={user['username']}")
def test_profile_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible("text=Posts")
assert page.is_visible("text=Level")
assert page.is_visible("text=Stars")
def test_profile_level_bar(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible("text=Progress to next level")
level_bar = page.locator(".bar")
assert level_bar.is_visible()
def test_profile_badges(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
badges = page.locator(".profile-badge")
assert badges.count() >= 1
def test_profile_tabs(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
tabs = ["Posts", "Projects", "Activity"]
for tab in tabs:
assert page.is_visible(f"a:has-text('{tab}')")
def test_profile_posts_tab(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}?tab=posts")
def test_profile_projects_tab_content(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}?tab=projects")
def test_profile_info_section(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible("text=Bio")
assert page.is_visible("text=Location")
def test_profile_back_link(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
back = page.locator("a:has-text('Back')").first
assert back.is_visible()
back.click()
page.wait_for_url(f"{BASE_URL}/feed")
def test_profile_role_display(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible("text=Member")
def test_profile_avatar(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
avatar = page.locator(".avatar-lg")
assert avatar.is_visible()
def test_profile_nav_from_topbar(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed")
page.locator(".topnav-user").click()
page.wait_for_timeout(300)
page.goto(f"{BASE_URL}/profile/{user['username']}")
assert page.is_visible(f"text={user['username']}")
def test_profile_edit_bio(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
page.evaluate("document.getElementById('input-bio').style.display = 'block'")
page.locator("textarea[name='bio']").fill("Test bio for testing", force=True)
page.click("button:has-text('Save Changes')")
page.wait_for_timeout(500)
assert page.is_visible("text=Test bio for testing")
def test_profile_edit_location(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
page.evaluate("document.getElementById('input-location').style.display = 'block'")
page.locator("input[name='location']").fill("Amsterdam", force=True)
page.click("button:has-text('Save Changes')")
page.wait_for_timeout(500)
assert page.is_visible("text=Amsterdam")
def test_profile_edit_all_fields(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}")
page.evaluate("""
document.getElementById('input-bio').style.display = 'block';
document.getElementById('input-location').style.display = 'block';
document.getElementById('input-git_link').style.display = 'block';
document.getElementById('input-website').style.display = 'block';
""")
page.locator("textarea[name='bio']").fill("Full stack dev", force=True)
page.locator("input[name='location']").fill("London, UK", force=True)
page.locator("input[name='git_link']").fill("https://git.example.com/alice", force=True)
page.locator("input[name='website']").fill("https://alice.example.com", force=True)
page.click("button:has-text('Save Changes')")
page.wait_for_timeout(500)
assert page.is_visible("text=Full stack dev")
assert page.is_visible("text=London, UK")
assert page.is_visible("text=https://git.example.com/alice")
assert page.is_visible("text=https://alice.example.com")
def test_profile_viewing_other_user(bob, alice):
page_b, user_b = bob
page_b.goto(f"{BASE_URL}/profile/alice_test")
assert page_b.is_visible("text=alice_test")
def test_profile_activity_tab(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}?tab=activity")
page.wait_for_timeout(300)
+128
View File
@@ -0,0 +1,128 @@
from tests.conftest import BASE_URL
def test_projects_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
assert page.is_visible("h2: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")
search = page.locator("input[placeholder='Search projects...']")
assert search.is_visible()
def test_projects_tabs(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
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")
page.click("a:has-text('Most Popular')")
page.wait_for_url(f"{BASE_URL}/projects?tab=popular")
page.click("a:has-text('New This Week')")
page.wait_for_url(f"{BASE_URL}/projects?tab=new")
def test_create_project_button(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
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")
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_timeout(500)
assert page.is_visible("text=Playwright Test Game")
def test_create_project_software(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
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_timeout(500)
assert page.is_visible("text=CLI Tool")
def test_create_project_mobile_app(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
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_timeout(500)
assert page.is_visible("text=Mobile Messenger")
def test_project_search(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
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_timeout(500)
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")
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")
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_projects_count(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects")
count_text = page.text_content(".projects-count")
assert "Showing" in count_text
assert "projects" in count_text