forked from retoor/devplacepy
docs: document DEVPLACE_DISABLE_SERVICES env var, news router, modal class toggle, and news service behavior
This commit is contained in:
@@ -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")
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user