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:
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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("═══════════════════════════════════════════════════")
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user