Perfect state.

This commit is contained in:
2026-05-12 12:45:52 +02:00
parent f54fccb8a9
commit 481cc23a2b
36 changed files with 1232 additions and 301 deletions
+119
View File
@@ -0,0 +1,119 @@
import pytest
from uuid import uuid4
from datetime import datetime
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def seed_admin_news(count=3):
news_table = get_table("news")
for i in range(count):
eid = f"admin_test_news_{i}"
if news_table.find_one(external_id=eid):
continue
uid = str(uuid4())
title = f"Admin News Article {i}"
slug = make_combined_slug(title, uid)
news_table.insert({
"uid": uid, "slug": slug, "title": title,
"external_id": eid, "grade": 5 + i,
"status": "draft" if i == 0 else "published",
"show_on_landing": 1 if i == 1 else 0,
"source_name": "AdminTest",
"synced_at": datetime.utcnow().isoformat(),
"description": f"Test article {i} for admin tests.",
})
def seed_extra_users(count=30):
users = get_table("users")
existing_count = len(list(users.all()))
if existing_count > 5:
return
for i in range(count):
uid = str(uuid4())
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(),
})
def test_admin_redirect_unauth(page, app_server):
page.goto(f"{BASE_URL}/admin", wait_until="domcontentloaded")
assert page.url.rstrip("/") == BASE_URL
def test_admin_users_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/admin/users", wait_until="domcontentloaded")
assert page.is_visible("text=Users")
assert page.is_visible("text=alice_test")
def test_admin_users_pagination(alice):
page, _ = alice
seed_extra_users(30)
page.goto(f"{BASE_URL}/admin/users", wait_until="domcontentloaded")
assert "Page 1" in page.text_content(".pagination-info")
page.goto(f"{BASE_URL}/admin/users?page=2", wait_until="domcontentloaded")
assert "Page 2" in page.text_content(".pagination-info")
def test_admin_news_loads(alice):
page, _ = alice
seed_admin_news()
page.goto(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
assert page.is_visible("text=News")
assert page.is_visible("text=Admin News Article")
def test_admin_news_publish_toggle(alice):
page, _ = alice
seed_admin_news()
page.goto(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
toggle_sel = "form[action*='/publish'] button.admin-toggle-switch"
was_active = "active" in (page.locator(toggle_sel).first.get_attribute("class") or "")
page.locator(toggle_sel).first.click()
page.wait_for_url(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
is_active = "active" in (page.locator(toggle_sel).first.get_attribute("class") or "")
assert is_active != was_active
def test_admin_news_landing_toggle(alice):
page, _ = alice
seed_admin_news()
page.goto(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
toggle_sel = "form[action*='/landing'] button.admin-toggle-switch"
was_active = "active" in (page.locator(toggle_sel).first.get_attribute("class") or "")
page.locator(toggle_sel).first.click()
page.wait_for_url(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
is_active = "active" in (page.locator(toggle_sel).first.get_attribute("class") or "")
assert is_active != was_active
def test_admin_news_delete(alice):
page, _ = alice
seed_admin_news()
page.goto(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
initial_count = len(page.locator("table tbody tr").all())
page.once("dialog", lambda d: d.accept())
page.locator("form[action*='/delete'] button").first.click()
page.wait_for_url(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
page.wait_for_timeout(1000)
new_count = len(page.locator("table tbody tr").all())
assert new_count < initial_count
def test_admin_news_pagination(alice):
page, _ = alice
seed_admin_news(30)
page.goto(f"{BASE_URL}/admin/news", wait_until="domcontentloaded")
assert page.text_content(".pagination-info") is not None
assert "Page 1" in page.text_content(".pagination-info")
next_btn = page.locator("a.pagination-btn:has-text('Next')")
if next_btn.count() > 0:
next_btn.click()
page.wait_for_url(f"{BASE_URL}/admin/news?page=2", wait_until="domcontentloaded")
assert "Page 2" in page.text_content(".pagination-info")
+17
View File
@@ -171,3 +171,20 @@ def test_create_post_cancel_modal(alice):
cancel.click()
modal = page.locator("#create-post-modal")
assert not modal.is_visible()
def test_feed_public_access(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Topics")
assert page.is_visible("text=All")
assert page.is_visible("text=Login")
def test_feed_guest_no_fab(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator(".feed-fab").count() == 0
def test_feed_guest_no_inline_comment(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator(".feed-comment-form").count() == 0
+37 -7
View File
@@ -1,4 +1,8 @@
from uuid import uuid4
from datetime import datetime
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def test_landing_page_loads(page, app_server):
@@ -33,16 +37,10 @@ def test_landing_features(page, app_server):
assert page.is_visible(f"text={feature}")
def test_landing_daily_topic(page, app_server):
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
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}/", wait_until="domcontentloaded")
assert page.is_visible("a:has-text('Home')")
assert page.is_visible("a:has-text('News')")
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')")
@@ -80,3 +78,35 @@ def test_landing_authenticated_redirects_to_feed(page, app_server):
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
def test_landing_footer_links(page, app_server):
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
assert page.is_visible("a:has-text('Bug Report')")
def test_landing_hero_present(page, app_server):
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
assert page.is_visible("text=Log In")
assert page.is_visible("text=Join DevPlace Free")
assert page.is_visible("text=Devplace.net")
def test_landing_news_section(page, app_server):
news_table = get_table("news")
existing = list(news_table.find(show_on_landing=1, _limit=1))
if not existing:
uid = str(uuid4())
title = "Landing News Test"
slug = make_combined_slug(title, uid)
news_table.insert({
"uid": uid, "slug": slug, "title": title,
"external_id": "landing_test", "grade": 9,
"status": "published", "show_on_landing": 1,
"source_name": "LandingTest",
"synced_at": datetime.utcnow().isoformat(),
"description": "Landing page news test article.",
})
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
assert page.is_visible("text=Developer News")
assert page.is_visible("text=Landing News Test")
+77
View File
@@ -0,0 +1,77 @@
import pytest
from uuid import uuid4
from datetime import datetime
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def seed_news():
news_table = get_table("news")
for i in range(3):
eid = f"test_news_{i}"
if news_table.find_one(external_id=eid):
continue
uid = str(uuid4())
title = f"News Test Article {i}"
slug = make_combined_slug(title, uid)
news_table.insert({
"uid": uid, "slug": slug, "title": title,
"external_id": eid, "grade": 8 + i,
"status": "published", "show_on_landing": 1,
"source_name": "TestSource",
"synced_at": datetime.utcnow().isoformat(),
"description": f"Description for test article {i}.",
})
@pytest.fixture
def news_article(app_server):
seed_news()
article = get_table("news").find_one(order_by=["-synced_at"])
return article
def test_news_listing_loads(page, app_server):
seed_news()
page.goto(f"{BASE_URL}/news", wait_until="domcontentloaded")
assert page.is_visible("text=Developer News")
assert page.is_visible("text=News Test Article")
def test_news_listing_authenticated(alice):
page, _ = alice
seed_news()
page.goto(f"{BASE_URL}/news", wait_until="domcontentloaded")
assert page.is_visible("text=Developer News")
def test_news_detail_loads(page, news_article):
slug = news_article.get("slug") or news_article["uid"]
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
assert page.is_visible("text=News Test Article")
assert page.is_visible("text=Comments")
def test_news_detail_404(page, app_server):
page.goto(f"{BASE_URL}/news/nonexistent-article", wait_until="domcontentloaded")
assert page.is_visible("text=not found", timeout=5000)
def test_news_comment(alice, news_article):
page, _ = alice
slug = news_article.get("slug") or news_article["uid"]
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
page.fill("textarea[name='content']", "Test comment on news article")
page.click("button:has-text('Post')")
page.wait_for_url(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
assert page.is_visible("text=Test comment on news article")
def test_news_detail_guest(page, news_article):
slug = news_article.get("slug") or news_article["uid"]
page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded")
assert page.is_visible("text=News Test Article")
assert page.is_visible("text=Comments")
assert page.is_visible("text=Read on TestSource")
+49 -6
View File
@@ -10,9 +10,9 @@ def test_notifications_page_loads(alice):
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()
clear = page.locator("button:has-text('Clear')")
if clear.is_visible():
clear.click()
page.wait_for_timeout(500)
@@ -40,11 +40,11 @@ def test_notifications_empty_state(alice):
assert page.is_visible("h2:has-text('Notifications')")
def test_notifications_header_has_mark_all(alice):
def test_notifications_header_has_clear(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications")
mark_all = page.locator("button:has-text('Mark all read')")
assert mark_all.is_visible()
clear = page.locator("button:has-text('Clear')")
assert clear.is_visible()
def test_notifications_topnav_user(alice):
@@ -58,3 +58,46 @@ def test_notifications_avatar_visible(alice):
page.goto(f"{BASE_URL}/notifications")
avatar = page.locator("img.avatar-img").first
assert avatar.is_visible()
def test_vote_notification_on_post(app_server, browser, seeded_db):
from tests.conftest import login_user
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(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pb.locator(".feed-fab").first.click()
pb.fill("#post-content", "Post for vote notification test")
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pb.url
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
# Check if vote actually worked by looking at Alice's page after vote
pa.wait_for_timeout(500)
pa_body = pa.locator("body").text_content()
assert "Internal Server Error" not in pa_body, f"Alice got 500 after vote: {pa_body[:300]}"
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pb.wait_for_timeout(1500)
body = pb.locator("body").text_content()
assert "Internal Server Error" not in body, f"Got 500 error on notifications page: {body[:500]}"
assert "alice_test" in body, f"Expected 'alice_test' in notifications, got: {body[:500]}"
assert "++'d" in body, f"Expected '++\\'d' in notifications, got: {body[:500]}"
ctx_a.close()
ctx_b.close()
+6 -12
View File
@@ -29,7 +29,10 @@ def test_services_data_json(page, seeded_db):
login_user(page, user)
page.goto(f"{BASE_URL}/admin/services/data", wait_until="domcontentloaded")
text = page.inner_text("pre") if page.locator("pre").count() > 0 else page.content()
assert "news" in text
import json
data = json.loads(text)
assert "services" in data
assert isinstance(data["services"], list)
def test_services_sidebar_link(page, seeded_db):
@@ -42,18 +45,9 @@ def test_services_sidebar_link(page, seeded_db):
assert "active" in (link.get_attribute("class") or "")
def test_services_status_display(page, seeded_db):
def test_services_no_services_message(page, seeded_db):
user = seeded_db["alice"]
_promote_to_admin(user["username"])
login_user(page, user)
page.goto(f"{BASE_URL}/admin/services", wait_until="domcontentloaded")
assert page.is_visible(".service-status")
def test_services_log_display(page, seeded_db):
user = seeded_db["alice"]
_promote_to_admin(user["username"])
login_user(page, user)
page.goto(f"{BASE_URL}/admin/services", wait_until="domcontentloaded")
log = page.locator(".service-log")
assert log.is_visible()
assert page.is_visible("text=No services registered")
+2 -2
View File
@@ -73,10 +73,10 @@ def test_time_ago_days():
def test_time_ago_months():
dt = (datetime.utcnow() - timedelta(days=60)).isoformat()
result = time_ago(dt)
assert "mo ago" in result
assert "/" in result
def test_time_ago_years():
dt = (datetime.utcnow() - timedelta(days=400)).isoformat()
result = time_ago(dt)
assert "y ago" in result
assert "/" in result