1027 lines
36 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
from tests.conftest import BASE_URL
import time
import requests
from playwright.sync_api import expect
from devplacepy.database import (
CUSTOMIZATION_GLOBAL_SCOPE,
get_customization_prefs,
get_table,
list_custom_overrides,
set_custom_override,
set_customization_pref,
)
from devplacepy.utils import clear_user_cache
def _user_customization_toggle(username):
return get_table("users").find_one(username=username)
def _seed_global_css(uid, sentinel):
set_custom_override(
"user", uid, CUSTOMIZATION_GLOBAL_SCOPE, "css", f".{sentinel} {{ color: red; }}"
)
def _profile(page, username):
page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
def _click_toggle(page, category):
page.locator(f"[data-customization-toggle='{category}']").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
def _goto_feed_until(page, predicate, timeout=5.0):
deadline = time.time() + timeout
content = ""
while time.time() < deadline:
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
content = page.content()
if predicate(content):
return content
time.sleep(0.2)
return content
import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
2026-08-09 11:39:53 +02:00
from tests.conftest import (
BASE_URL,
assert_no_horizontal_overflow,
assert_share_copies,
)
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def _seed_posts(count):
suite = uuid4().hex[:8]
topic = f"pag{suite}"
users = get_table("users")
posts = get_table("posts")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
for i in range(count):
owner = str(uuid4())
users.insert(
{
"uid": owner,
"username": f"pag_{suite}_{i}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{suite}_{i}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
content = f"Paginated post {i}"
posts.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(content, uid),
"title": None,
"content": content,
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return topic
def _seed_post_with_comments(comment_texts, topic="devlog"):
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"seed_{owner[:8]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{owner[:8]}@seed.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
post_uid = str(uuid4())
marker = f"seedpost-{post_uid[:8]}"
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": post_uid,
"user_uid": owner,
"slug": make_combined_slug(marker, post_uid),
"title": None,
"content": marker,
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
comments = get_table("comments")
base = datetime.now(timezone.utc)
for i, text in enumerate(comment_texts):
comments.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": str(uuid4()),
"target_type": "post",
"target_uid": post_uid,
"post_uid": post_uid,
"user_uid": owner,
"content": text,
"parent_uid": None,
"created_at": (base + timedelta(seconds=i)).isoformat(),
}
)
return marker, post_uid
def _create_post_ui(page, content, topic="devlog"):
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)
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def _seed_searchable_posts():
token = uuid4().hex[:10]
users = get_table("users")
posts = get_table("posts")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
match_title = f"Findable post {token}"
other_title = f"Unrelated post {uuid4().hex[:10]}"
for offset, title in ((0, match_title), (1, other_title)):
owner = str(uuid4())
users.insert(
{
"uid": owner,
"username": f"srch_{owner[:8]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
posts.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"content": f"Body for {title}",
"topic": "devlog",
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=offset)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return token, match_title, other_title
from devplacepy.utils import generate_uid
DEFAULT_MAINTENANCE_MESSAGE = (
"DevPlace is undergoing scheduled maintenance. Please check back shortly."
)
OPERATIONAL_FIELDS = (
"rate_limit_per_minute",
"rate_limit_window_seconds",
"session_max_age_days",
"session_remember_days",
"registration_open",
"maintenance_mode",
"maintenance_message",
)
def _save_settings(page, **fields):
page.goto(f"{BASE_URL}/admin/settings", wait_until="domcontentloaded")
for name, value in fields.items():
locator = page.locator(f"#{name}")
tag = locator.evaluate("el => el.tagName.toLowerCase()")
if tag == "select":
page.select_option(f"#{name}", value)
else:
locator.fill(value)
page.click("button:has-text('Save Settings')")
page.wait_for_url("**/admin/settings", wait_until="domcontentloaded")
def create_post(page, topic="random", content="Test post content", 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(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def _profile_stars(page, username):
page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
value = page.locator(
".profile-stat:has(.profile-stat-label:has-text('Stars')) .profile-stat-value"
).first
return int(value.text_content().strip())
def _key_role_visibility(username):
return get_table("users").find_one(username=username)["api_key"]
_author_key = []
def _author():
# A dedicated Member author so seeding posts never pollutes alice/bob state
# (notifications, post counts, XP) that other test files assert on.
if _author_key:
return _author_key[0]
name = "rolevis_author"
requests.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
row = get_table("users").find_one(username=name)
get_table("users").update({"uid": row["uid"], "role": "Member"}, ["uid"])
_author_key.append(row["api_key"])
return _author_key[0]
def _seed_post_role_visibility():
requests.post(
f"{BASE_URL}/posts/create",
headers={"X-API-KEY": _author()},
data={
"content": "role visibility seed post body text",
"title": "RoleVis",
"topic": "random",
},
)
def _seed_news_seo():
from datetime import datetime, timezone
from devplacepy.database import get_table
from devplacepy.utils import generate_uid, make_combined_slug
uid = generate_uid()
title = f"SEO Test News Article {uid.split('-')[-1]}"
slug = make_combined_slug(title, uid)
get_table("news").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"slug": slug,
"title": title,
"description": "A seeded news article for SEO tests.",
"content": "Body content for the seeded article.",
"url": "https://example.com/article",
"source_name": "ExampleSource",
"status": "published",
"synced_at": datetime.now(timezone.utc).isoformat(),
"show_on_landing": 0,
"grade": 8,
}
)
return slug, uid
def _seed_owner():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
uid = str(uuid4())
get_table("users").insert(
{
"uid": uid,
"username": f"seo_{uid[:8]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return uid
def _seed_post_seo(image=None):
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Post", uid)
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": slug,
"title": "SEO Detail Post",
"content": "Body text for the SEO detail post.",
"topic": "general",
"project_uid": None,
"image": image,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return slug, uid
def _seed_gist():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Gist", uid)
get_table("gists").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": slug,
"title": "SEO Detail Gist",
"description": "Gist description for SEO tests.",
"source_code": "print('seo')",
"language": "python",
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return slug, uid
def _seed_project():
from datetime import datetime, timezone
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
uid = str(uuid4())
slug = make_combined_slug("SEO Detail Project", uid)
get_table("projects").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": slug,
"title": "SEO Detail Project",
"description": "Project description for SEO tests.",
"project_type": "software",
"platforms": "Linux",
"status": "Released",
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return slug, uid
def _seed_news_image(news_uid):
from devplacepy.database import get_table
get_table("news_images").insert(
{
"deleted_at": None,
"deleted_by": None,
"news_uid": news_uid,
"url": "https://example.com/seo-news-image.jpg",
}
)
def _seed_feed_posts(count):
from datetime import datetime, timezone, timedelta
from uuid import uuid4
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
owner = _seed_owner()
topic = f"seopag{owner[:8]}"
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
posts = get_table("posts")
for i in range(count):
uid = str(uuid4())
posts.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(f"seo pag {i}", uid),
"title": None,
"content": f"seo pag post {i}",
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
}
)
return topic
def test_issues_footer_link(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
link = page.locator("a:has-text('Issue Report')")
assert link.is_visible()
link.click()
page.wait_for_url(f"{BASE_URL}/issues", wait_until="domcontentloaded")
def test_global_toggle_suppresses_rendering_and_persists(bob):
page, user = bob
uid = _user_customization_toggle(user["username"])["uid"]
set_customization_pref(uid, "global", disabled=False)
sentinel = f"sentinel-{int(time.time() * 1000)}"
_seed_global_css(uid, sentinel)
assert sentinel in _goto_feed_until(page, lambda c: sentinel in c)
_profile(page, user["username"])
_click_toggle(page, "global")
expect(page.locator("[data-customization-toggle='global']")).to_have_text("Enable")
assert sentinel not in _goto_feed_until(page, lambda c: sentinel not in c)
_profile(page, user["username"])
expect(page.locator("[data-customization-toggle='global']")).to_have_text("Enable")
expect(
page.locator("form[action$='/customization/global'] input[name='value']")
).to_have_value("1")
_click_toggle(page, "global")
expect(page.locator("[data-customization-toggle='global']")).to_have_text("Disable")
assert sentinel in _goto_feed_until(page, lambda c: sentinel in c)
def test_pagetype_toggle_is_independent(bob):
page, user = bob
uid = _user_customization_toggle(user["username"])["uid"]
set_customization_pref(uid, "global", disabled=False)
set_customization_pref(uid, "pagetype", disabled=False)
global_sentinel = f"glob-{int(time.time() * 1000)}"
page_sentinel = f"page-{int(time.time() * 1000)}"
_seed_global_css(uid, global_sentinel)
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page_type = page.locator("main[data-page-type]").get_attribute("data-page-type")
set_custom_override(
"user", uid, page_type, "css", f".{page_sentinel} {{ color: blue; }}"
)
body = _goto_feed_until(
page, lambda c: global_sentinel in c and page_sentinel in c
)
assert global_sentinel in body
assert page_sentinel in body
_profile(page, user["username"])
_click_toggle(page, "pagetype")
body = _goto_feed_until(page, lambda c: page_sentinel not in c)
assert global_sentinel in body
assert page_sentinel not in body
def test_feed_page_loads(alice):
page, user = alice
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=Devlog")
assert page.is_visible("text=Showcase")
def test_feed_nav_tabs(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
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", wait_until="domcontentloaded")
page.click("a:has-text('Trending')")
page.wait_for_url(f"{BASE_URL}/feed?tab=trending", wait_until="domcontentloaded")
page.click("a:has-text('Recent')")
page.wait_for_url(f"{BASE_URL}/feed?tab=recent", wait_until="domcontentloaded")
def test_feed_topic_filter_sidebar(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
topics = ["Devlog", "Showcase", "Question", "Rant", "Fun"]
for topic in topics:
link = page.locator(f".sidebar-nav 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", wait_until="domcontentloaded")
page.click("a:has-text('Devlog')")
page.wait_for_url(f"{BASE_URL}/feed?topic=devlog", wait_until="domcontentloaded")
def test_feed_search_bar_visible(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
search = page.locator("input[name='search'][placeholder='Search posts...']")
assert search.is_visible()
def test_feed_search_filters(alice):
page, user = alice
token, match_title, other_title = _seed_searchable_posts()
page.goto(f"{BASE_URL}/feed?search={token}", wait_until="domcontentloaded")
assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
assert page.locator(f".post-title:has-text('{other_title}')").count() == 0
def test_feed_search_no_match(alice):
page, user = alice
_seed_searchable_posts()
page.goto(
f"{BASE_URL}/feed?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
)
assert page.is_visible(".empty-state")
assert page.locator(".post-title").count() == 0
def test_feed_search_preserves_topic(alice):
page, user = alice
token, match_title, _ = _seed_searchable_posts()
page.goto(
f"{BASE_URL}/feed?topic=devlog&search={token}", wait_until="domcontentloaded"
)
assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
preserved = page.locator(
".sidebar-card form input[type='hidden'][name='topic'][value='devlog']"
)
assert preserved.count() == 1
def test_feed_community_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
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", wait_until="domcontentloaded")
assert page.is_visible("text=Top Authors")
def _resources_panel(page):
return page.locator(".sidebar-card .sidebar-section").filter(
has=page.locator(".sidebar-heading:has-text('Resources')")
)
def test_feed_resources_panel_visible(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
panel = _resources_panel(page)
panel.wait_for(state="visible")
expect(panel.locator(".sidebar-heading")).to_have_text("Resources")
expect(panel.locator("a.sidebar-link:has-text('Docs')")).to_be_visible()
expect(panel.locator("a.sidebar-link:has-text('Issues')")).to_be_visible()
expect(panel.locator("a.sidebar-link[data-devii-open]")).to_be_visible()
def test_feed_resources_docs_link(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
docs = _resources_panel(page).locator("a.sidebar-link:has-text('Docs')")
assert docs.get_attribute("href") == "/docs"
docs.click()
page.wait_for_url("**/docs/index.html", wait_until="domcontentloaded")
def test_feed_resources_issues_link(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
issues = _resources_panel(page).locator("a.sidebar-link:has-text('Issues')")
assert issues.get_attribute("href") == "/issues"
issues.click()
page.wait_for_url(f"{BASE_URL}/issues", wait_until="domcontentloaded")
def test_feed_resources_devii_opens_assistant(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
devii = _resources_panel(page).locator("a.sidebar-link[data-devii-open]")
expect(devii).to_be_visible()
assert devii.get_attribute("href") == "/devii/"
expect(devii).to_have_text(re.compile("Devii"))
def test_feed_daily_topic(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("text=Daily Topic")
def test_create_post_fab(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
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("#create-post-modal button:has-text('Post')")
def test_topnav_notification_bell(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
bell = page.locator(".topnav-icon[href='/notifications']")
assert bell.is_visible()
def test_topnav_user_menu(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible(f"text={user['username']}")
def test_feed_inline_comment_placeholder(alice):
page, _ = alice
marker = f"placeholder-{uuid4().hex[:8]}"
_create_post_ui(page, f"{marker} placeholder check post")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
inline_input = card.locator(".comment-form textarea[name='content']")
expect(inline_input).to_be_visible()
assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."
def test_feed_shows_last_three_comments(page, app_server):
m = uuid4().hex[:6]
marker, _ = _seed_post_with_comments(
[f"{m}-one", f"{m}-two", f"{m}-three", f"{m}-four"]
)
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
previews = card.locator(".post-card-comments .comment-text")
expect(previews).to_have_count(3)
expect(previews.nth(0)).to_contain_text(f"{m}-two")
expect(previews.nth(1)).to_contain_text(f"{m}-three")
expect(previews.nth(2)).to_contain_text(f"{m}-four")
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{m}-one")
def test_feed_guest_vote_disabled(page, app_server):
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator(".post-action-btn.vote-up").first.is_disabled()
def test_feed_guest_reply_disabled(page, app_server):
marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator(".post-card-comments [data-action='reply']").first.is_disabled()
def test_feed_politics_topic(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
politics_link = page.locator(".sidebar-nav a:has-text('Politics')")
assert politics_link.is_visible()
def test_create_post_cancel_modal(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".feed-fab").click()
cancel = page.locator("#create-post-modal button:has-text('Cancel')").first
cancel.click()
modal = page.locator("#create-post-modal")
assert not modal.is_visible()
def test_feed_pagination_first_page(alice):
page, _ = alice
topic = _seed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
assert page.locator(".post-card").count() == 25
assert page.is_visible(".load-more-wrap")
def test_feed_pagination_load_more(alice):
page, _ = alice
topic = _seed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
page.click(".load-more-wrap a")
page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
assert f"topic={topic}" in page.url
assert page.locator(".post-card").count() == 1
assert not page.is_visible(".load-more-wrap")
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_fab_links_to_login(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
fab = page.locator(".feed-fab")
assert fab.count() == 1
assert "/auth/login" in (fab.first.get_attribute("href") or "")
def test_feed_guest_inline_comment_disabled(page, app_server):
_seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator(".post-card").count() > 0
form = page.locator(".comment-form.comment-form-guest").first
form.wait_for(state="attached")
assert form.locator("textarea").first.is_disabled()
def test_leaderboard_nav_link(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.is_visible("a.topnav-icon[href='/leaderboard']")
def test_mobile_hamburger_nav(alice):
page, _ = alice
page.set_viewport_size({"width": 390, "height": 844})
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
btn = page.locator("#hamburger-btn")
btn.wait_for(state="visible")
btn.click()
page.locator("#mobile-panel.open").wait_for(state="visible")
assert "open" in (page.locator("#mobile-panel").get_attribute("class") or "")
def test_notifications_bell_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
bell = page.locator(".topnav-icon[href='/notifications']")
assert bell.is_visible()
def test_maintenance_mode_blocks_guests(alice):
page, _ = alice
try:
_save_settings(page, maintenance_mode="1", maintenance_message="Down for tests")
deadline = time.time() + 5.0
response = requests.get(f"{BASE_URL}/feed")
while response.status_code != 503 and time.time() < deadline:
time.sleep(0.2)
response = requests.get(f"{BASE_URL}/feed")
assert response.status_code == 503
assert "Down for tests" in response.text
finally:
_save_settings(
page, maintenance_mode="0", maintenance_message=DEFAULT_MAINTENANCE_MESSAGE
)
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", wait_until="domcontentloaded")
def test_delete_own_post(alice):
page, _ = alice
create_post(page, "random", "Post to be deleted permanently here")
post_url = page.url
page.locator(".post-action-btn:has-text('Delete')").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")
resp = page.goto(post_url, wait_until="domcontentloaded")
assert resp.status == 404
def test_profile_nav_from_topbar(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".topnav-user").click()
profile_link = page.locator(".dropdown-menu a:has-text('Profile')")
profile_link.wait_for(state="visible")
profile_link.click()
page.wait_for_url(
f"**/profile/{user['username']}", wait_until="domcontentloaded"
)
assert page.is_visible(f"text={user['username']}")
def test_guest_has_no_admin_nav(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator("a[href='/admin']").count() == 0
assert page.locator("a[href='/auth/login']").count() >= 1
def test_guest_sees_no_role_badges(page, seeded_db):
_seed_post_role_visibility()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible")
assert page.locator(".post-author-role").count() == 0
def test_guest_action_controls_disabled(page, seeded_db):
_seed_post_role_visibility()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").first
card.wait_for(state="visible")
assert card.locator(".vote-up").first.is_disabled()
fab = page.locator(".feed-fab").first
assert "/auth/login" in (fab.get_attribute("href") or "")
def test_member_has_no_admin_nav(bob):
page, _ = bob
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator("a[href='/admin']").count() == 0
def test_member_sees_no_role_badges(bob):
page, _ = bob
_seed_post_role_visibility()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible")
assert page.locator(".post-author-role").count() == 0
def test_member_can_act(bob):
page, _ = bob
_seed_post_role_visibility()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
fab = page.locator(".feed-fab").first
fab.wait_for(state="visible")
assert (fab.get_attribute("data-modal") or "") == "create-post-modal"
card = page.locator(".post-card").first
card.wait_for(state="visible")
assert not card.locator(".vote-up").first.is_disabled()
def test_admin_has_admin_nav(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
assert page.locator("a[href='/admin']").count() >= 1
def test_feed_shows_author_level(alice):
page, _ = alice
_seed_post_role_visibility()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible")
assert page.locator(".post-author-level").count() >= 1
def test_feed_canonical_preserves_pagination(page, app_server):
page.goto(f"{BASE_URL}/feed?page=2", wait_until="domcontentloaded")
href = page.locator('link[rel="canonical"]').get_attribute("href")
assert href.endswith("?page=2"), href
def test_feed_canonical_drops_non_pagination_params(page, app_server):
page.goto(f"{BASE_URL}/feed?sort=new", wait_until="domcontentloaded")
href = page.locator('link[rel="canonical"]').get_attribute("href")
assert href.endswith("/feed"), href
def test_default_og_image_is_raster(page, app_server):
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
og = page.locator('meta[property="og:image"]').get_attribute("content")
assert og.endswith(".png"), og
def test_feed_pagination_emits_rel_next(page, app_server):
topic = _seed_feed_posts(26)
page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
nxt = page.locator('link[rel="next"]')
assert nxt.count() == 1
href = nxt.get_attribute("href")
assert "before=" in href and f"topic={topic}" in href, href
def test_feed_online_now_widget_lists_current_user(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
widget = page.locator(".online-users")
expect(widget).to_be_visible()
expect(
page.locator(f".online-user[title='{user['username']}']")
).to_have_count(1)
expect(page.locator(".online-users .presence-dot.online").first).to_be_visible()
2026-07-07 15:28:28 +02:00
def _scroll_and_open_post(page):
_seed_feed_posts(30)
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible")
page.evaluate("window.scrollTo(0, 1200)")
page.wait_for_function("window.scrollY > 1000")
card_link = page.locator(".post-card .card-link").nth(5)
card_link.scroll_into_view_if_needed()
page.wait_for_timeout(400)
y_before = page.evaluate("window.scrollY")
assert y_before > 0
card_link.click()
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
return y_before
def test_feed_scroll_restored_via_back_link(alice):
page, user = alice
y_before = _scroll_and_open_post(page)
back = page.locator("a.back-link")
back.wait_for(state="visible")
back.click()
page.wait_for_url(f"{BASE_URL}/feed*", wait_until="domcontentloaded")
page.wait_for_function(f"Math.abs(window.scrollY - {y_before}) < 60")
def test_feed_scroll_restored_via_browser_back(alice):
page, user = alice
y_before = _scroll_and_open_post(page)
page.go_back(wait_until="domcontentloaded")
page.wait_for_function(f"Math.abs(window.scrollY - {y_before}) < 60")
def test_feed_scroll_not_restored_on_fresh_visit(alice):
page, user = alice
_scroll_and_open_post(page)
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.wait_for_timeout(600)
assert page.evaluate("window.scrollY") < 60
2026-08-09 11:39:53 +02:00
def _seed_post_with_long_code_line(topic="devlog"):
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"code_{owner[:8]}",
"email": f"{owner[:8]}@code.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"longcode-{uid[:8]}"
line = "unbreakable_identifier_" * 10
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"content": f"```python\n{line}\n```",
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return marker
def test_feed_long_code_line_does_not_widen_layout(alice):
page, user = alice
marker = _seed_post_with_long_code_line()
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
card = page.locator(".post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".feed-layout")
def test_dashboard_long_code_line_does_not_widen_layout(alice):
page, user = alice
marker = _seed_post_with_long_code_line()
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
card = page.locator(".dashboard-post-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".dashboard-main")