+3
-3
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
@@ -21,7 +21,7 @@ def seed_admin_news(count=3):
|
||||
"status": "draft" if i == 0 else "published",
|
||||
"show_on_landing": 1 if i == 1 else 0,
|
||||
"source_name": "AdminTest",
|
||||
"synced_at": datetime.utcnow().isoformat(),
|
||||
"synced_at": datetime.now(timezone.utc).isoformat(),
|
||||
"description": f"Test article {i} for admin tests.",
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ def seed_extra_users(count=30):
|
||||
users.insert({
|
||||
"uid": uid, "username": f"pagu_{i:04d}", "email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x", "role": "Member", "is_active": True,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from pathlib import Path
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def test_attachment_upload_endpoint(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").click()
|
||||
page.locator("#create-post-modal").wait_for(state="visible")
|
||||
|
||||
|
||||
def test_attachment_store_and_serve(alice):
|
||||
page, user = alice
|
||||
uploads = page.context.request.post(f"{BASE_URL}/attachments/upload")
|
||||
|
||||
|
||||
def test_post_attachments_display(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_project_attachments_display(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_bug_attachments_display(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/bugs", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_attachment_uploader_component_present(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").click()
|
||||
page.locator("#create-post-modal").wait_for(state="visible")
|
||||
page.locator(".attachment-upload-area").wait_for(state="visible", timeout=5000)
|
||||
page.locator(".attachment-upload-btn").wait_for(state="visible", timeout=5000)
|
||||
@@ -1,278 +0,0 @@
|
||||
import re
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def create_post(page, content, topic="random", title=None):
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.check(f"input[value='{topic}']")
|
||||
page.fill("#post-content", content)
|
||||
if title:
|
||||
page.fill("#post-title", title)
|
||||
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||
page.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def signup(page, username, email, password):
|
||||
page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded")
|
||||
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, wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_full_user_journey(browser, app_server):
|
||||
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
pa = ctx_a.new_page()
|
||||
pb = ctx_b.new_page()
|
||||
pa.set_default_timeout(10000)
|
||||
pb.set_default_timeout(10000)
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 1 — LANDING PAGE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Devplace.net")
|
||||
assert pa.is_visible("text=The Developer Social Network")
|
||||
assert pa.is_visible("text=Join DevPlace Free")
|
||||
for feat in ("100% Free Forever", "Zero Ads", "No Censorship", "No Payments"):
|
||||
assert pa.is_visible(f"text={feat}")
|
||||
assert pa.is_visible("text=Daily Topic")
|
||||
assert pa.is_visible("text=Read More")
|
||||
|
||||
# Navigate to signup via link
|
||||
pa.click("a:has-text('Sign Up')", no_wait_after=True)
|
||||
pa.wait_for_url("**/auth/signup", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 2 — TWO USERS SIGN UP
|
||||
# ═══════════════════════════════════════════════
|
||||
signup(pa, "alice_demo", "alice_demo@test.dev", "alice_pass_1")
|
||||
signup(pb, "bob_demo", "bob_demo@test.dev", "bob_pass_1")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 3 — ALICE EXPLORES FEED
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
for tab_text in ("Topics", "All", "Trending", "Recent", "Following",
|
||||
"Devlog", "Showcase", "Question", "Rant", "Fun"):
|
||||
assert pa.is_visible(f"text={tab_text}")
|
||||
assert pa.is_visible("text=Total Members")
|
||||
assert pa.is_visible("text=Top Authors")
|
||||
|
||||
# Switch all feed tabs
|
||||
for tab in ("Trending", "Recent"):
|
||||
pa.click(f"a:has-text('{tab}')")
|
||||
pa.wait_for_url(f"**/feed?tab={tab.lower()}", wait_until="domcontentloaded")
|
||||
pa.click("a.feed-nav-btn:has-text('All')")
|
||||
pa.wait_for_url("**/feed**", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
# Switch all topic filters
|
||||
for topic in ("showcase", "question", "rant", "fun"):
|
||||
pa.click(f"a:has-text('{topic.capitalize()}')")
|
||||
pa.wait_for_url(f"**/feed?topic={topic}", wait_until="domcontentloaded")
|
||||
pa.click("a:has-text('All')")
|
||||
pa.wait_for_url("**/feed**", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 4 — ALICE CREATES 4 POSTS (ALL TOPICS)
|
||||
# ═══════════════════════════════════════════════
|
||||
create_post(pa, "Just shipped a new feature for my REST API! Took me 3 days.", "devlog", "API Progress Update")
|
||||
post1_url = pa.url
|
||||
|
||||
create_post(pa, "Check out this pixel art editor I built in WASM. Runs at 60fps!", "showcase", "Pixel Editor WASM")
|
||||
|
||||
create_post(pa, "Does anyone else think tabs are better than spaces? Fight me.", "rant", "Tabs vs Spaces")
|
||||
|
||||
create_post(pa, "Why do we call it tech debt when really we just cut corners?", "fun")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 5 — ALICE CREATES TWO PROJECTS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
pa.locator("#create-project-btn").click()
|
||||
pa.fill("#title", "N8Nme")
|
||||
pa.fill("#description", "Visual workflow builder with 300+ integrations.")
|
||||
pa.fill("#release_date", "2026-04-15")
|
||||
pa.check("input[value='software']")
|
||||
for plat in ("Linux", "Docker", "Web"):
|
||||
pa.locator("#platforms-input").fill(plat)
|
||||
pa.locator("#platforms-input").press("Enter")
|
||||
pa.click("button:has-text('Create Project')")
|
||||
pa.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=N8Nme")
|
||||
|
||||
# Second project — released
|
||||
pa.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
pa.locator("#create-project-btn").click()
|
||||
pa.fill("#title", "DWN")
|
||||
pa.fill("#description", "Decentralized web node in Rust.")
|
||||
pa.check("input[value='software']")
|
||||
pa.check("input[value='Released']")
|
||||
pa.locator("#platforms-input").fill("Linux")
|
||||
pa.locator("#platforms-input").press("Enter")
|
||||
pa.click("button:has-text('Create Project')")
|
||||
pa.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=DWN")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 6 — ALICE EDITS HER PROFILE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
assert pa.is_visible("text=Member")
|
||||
assert pa.is_visible("text=Posts")
|
||||
assert pa.is_visible("text=Level")
|
||||
assert pa.is_visible("text=Stars")
|
||||
assert pa.is_visible("text=Progress to next level")
|
||||
assert pa.is_visible("text=Bio")
|
||||
|
||||
pa.evaluate("""
|
||||
document.querySelector('textarea[name="bio"]').value = 'Full-stack developer and open source enthusiast.';
|
||||
document.querySelector('input[name="location"]').value = 'Berlin, Germany';
|
||||
document.querySelector('input[name="git_link"]').value = 'https://github.com/alice_demo';
|
||||
document.querySelector('input[name="website"]').value = 'https://alicedemo.dev';
|
||||
document.querySelector('form[action="/profile/update"]').submit();
|
||||
""")
|
||||
pa.wait_for_timeout(500)
|
||||
assert pa.is_visible("text=Full-stack developer")
|
||||
assert pa.is_visible("text=Berlin, Germany")
|
||||
assert pa.is_visible("text=https://github.com/alice_demo")
|
||||
|
||||
# Switch profile tabs
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=N8Nme")
|
||||
assert pa.is_visible("text=DWN")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=posts", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=API Progress Update")
|
||||
assert pa.is_visible("text=Pixel Editor WASM")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 7 — BOB EXPLORES ALICE'S STUFF
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=alice_demo")
|
||||
assert pb.is_visible("text=Full-stack developer")
|
||||
assert pb.is_visible("text=Berlin, Germany")
|
||||
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
assert pb.is_visible("text=DWN")
|
||||
|
||||
# Browse projects, search
|
||||
pb.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=Recently Released")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
pb.fill("input[placeholder='Search projects...']", "N8Nme")
|
||||
pb.locator("input[placeholder='Search projects...']").press("Enter")
|
||||
pb.wait_for_timeout(300)
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
|
||||
# Bob creates his own project
|
||||
pb.locator("#create-project-btn").click()
|
||||
pb.fill("#title", "ClaudeCode")
|
||||
pb.fill("#description", "AI-powered code generation for the terminal.")
|
||||
pb.check("input[value='software']")
|
||||
pb.check("input[value='Released']")
|
||||
pb.locator("#platforms-input").fill("Linux")
|
||||
pb.locator("#platforms-input").press("Enter")
|
||||
pb.click("button:has-text('Create Project')")
|
||||
pb.wait_for_timeout(500)
|
||||
assert pb.is_visible("text=ClaudeCode")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 8 — BOB VOTES AND COMMENTS
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(post1_url)
|
||||
assert pb.is_visible("text=API Progress Update")
|
||||
assert pb.is_visible("text=Just shipped a new feature")
|
||||
|
||||
# Vote
|
||||
pb.locator("button").filter(has_text=re.compile(r"\+")).first.click()
|
||||
pb.wait_for_timeout(300)
|
||||
|
||||
# Comment
|
||||
pb.locator("textarea[name='content']").fill("Nice work! What framework did you use?")
|
||||
pb.click("button:has-text('Post')")
|
||||
pb.wait_for_timeout(500)
|
||||
assert pb.is_visible("text=Nice work! What framework did you use?")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 9 — ALICE REPLIES TO BOB'S COMMENT
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(post1_url)
|
||||
assert pa.is_visible("text=Nice work! What framework did you use?")
|
||||
pa.locator(".comment-form textarea[name='content']").fill("Thanks! I used FastAPI with SQLAlchemy. Vanilla JS frontend.")
|
||||
pa.click("button:has-text('Post')")
|
||||
pa.wait_for_timeout(500)
|
||||
assert pa.is_visible("text=Thanks! I used FastAPI")
|
||||
|
||||
# Alice votes on her own post
|
||||
pa.locator("button").filter(has_text=re.compile(r"\+")).first.click()
|
||||
pa.wait_for_timeout(300)
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 10 — ALICE CHECKS NOTIFICATIONS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("h2:has-text('Notifications')")
|
||||
mark_all = pa.locator("button:has-text('Mark all read')")
|
||||
if mark_all.is_visible():
|
||||
mark_all.click()
|
||||
pa.wait_for_timeout(300)
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 11 — BOB CHECKS NOTIFICATIONS TOO
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 12 — LOGOUT, LOGIN AGAIN
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pa.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# Log back in
|
||||
pa.click("text=Log In")
|
||||
pa.wait_for_url("**/auth/login", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("h2:has-text('Welcome Back')")
|
||||
pa.fill("#email", "alice_demo@test.dev")
|
||||
pa.fill("#password", "alice_pass_1")
|
||||
pa.click("button:has-text('Sign in')")
|
||||
pa.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
# Bob logs out
|
||||
pb.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pb.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 13 — FINAL VERIFICATION
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Topics")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
pa.close()
|
||||
pb.close()
|
||||
ctx_a.close()
|
||||
ctx_b.close()
|
||||
|
||||
print("\n═══════════════════════════════════════════════════")
|
||||
print(" ABSURD DEMO COMPLETE — 13 ACTS, 0 MISTAKES")
|
||||
print("═══════════════════════════════════════════════════")
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
@@ -20,7 +20,7 @@ def seed_news():
|
||||
"external_id": eid, "grade": 8 + i,
|
||||
"status": "published", "show_on_landing": 1,
|
||||
"source_name": "TestSource",
|
||||
"synced_at": datetime.utcnow().isoformat(),
|
||||
"synced_at": datetime.now(timezone.utc).isoformat(),
|
||||
"description": f"Description for test article {i}.",
|
||||
})
|
||||
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
from devplacepy.utils import hash_password, verify_password, generate_uid, slugify, time_ago
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
def test_hash_password_returns_string():
|
||||
@@ -47,36 +47,36 @@ def test_slugify_leading_trailing():
|
||||
|
||||
|
||||
def test_time_ago_just_now():
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
result = time_ago(now)
|
||||
assert result == "just now"
|
||||
|
||||
|
||||
def test_time_ago_minutes():
|
||||
dt = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
|
||||
dt = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "m ago" in result
|
||||
|
||||
|
||||
def test_time_ago_hours():
|
||||
dt = (datetime.utcnow() - timedelta(hours=3)).isoformat()
|
||||
dt = (datetime.now(timezone.utc) - timedelta(hours=3)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "h ago" in result
|
||||
|
||||
|
||||
def test_time_ago_days():
|
||||
dt = (datetime.utcnow() - timedelta(days=5)).isoformat()
|
||||
dt = (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "d ago" in result
|
||||
|
||||
|
||||
def test_time_ago_months():
|
||||
dt = (datetime.utcnow() - timedelta(days=60)).isoformat()
|
||||
dt = (datetime.now(timezone.utc) - timedelta(days=60)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "/" in result
|
||||
|
||||
|
||||
def test_time_ago_years():
|
||||
dt = (datetime.utcnow() - timedelta(days=400)).isoformat()
|
||||
dt = (datetime.now(timezone.utc) - timedelta(days=400)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "/" in result
|
||||
|
||||
Reference in New Issue
Block a user