test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
|
from playwright.sync_api import expect
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_project():
|
|
|
|
|
"""Create a project seeded directly into DB and return (slug, uid, user_uid)."""
|
|
|
|
|
owner = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"dlowner_{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",
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
"email": f"{owner[:8]}@dl.test",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
slug = make_combined_slug("E2E Devlog Project", uid)
|
|
|
|
|
get_table("projects").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": "E2E Devlog Project",
|
|
|
|
|
"description": "Project for devlog browser tests.",
|
|
|
|
|
"project_type": "software",
|
|
|
|
|
"platforms": "",
|
|
|
|
|
"status": "In Development",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid, owner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_project_post(project_uid, user_uid, order, title=None):
|
|
|
|
|
"""Insert a post linked to a project with precise ordering."""
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
marker = title or f"dlpost-{uid[:8]}"
|
|
|
|
|
get_table("posts").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": user_uid,
|
|
|
|
|
"slug": make_combined_slug(marker, uid),
|
|
|
|
|
"title": marker,
|
|
|
|
|
"content": f"Devlog post content {order}",
|
|
|
|
|
"topic": "devlog",
|
|
|
|
|
"project_uid": project_uid,
|
|
|
|
|
"image": None,
|
|
|
|
|
"stars": 0,
|
Fix circular import, primary-admin NULL trap, and add gateway quota reset
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
2026-07-27 11:17:48 +02:00
|
|
|
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=order)).isoformat(),
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return marker, uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_project_ui(page, title):
|
|
|
|
|
"""Create a project via the UI."""
|
|
|
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("#create-project-btn").click()
|
|
|
|
|
page.fill("#title", title)
|
|
|
|
|
page.fill("#description", "Project for devlog UI test")
|
|
|
|
|
page.click("button:has-text('Create Project')")
|
|
|
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
|
|
|
return page.url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_empty_state_on_project_page(alice):
|
|
|
|
|
"""Project with no linked posts displays 'No devlog posts yet'."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_project_ui(page, "Empty Devlog Project")
|
|
|
|
|
|
|
|
|
|
devlog_section = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog_section).to_be_visible()
|
Dedicate the project page to the project
The project detail page becomes a full project showcase built entirely
from existing platform mechanisms. One encompassing dark card wraps the
page; inner panels (tab bar, sidebar cards, devlog entries, comments)
sit one elevation lighter. The hero opens with a cover banner and an
optional logo tile, both plain attachment references
(cover_attachment_uid/logo_attachment_uid) uploaded through the
standard dp-upload attachment widget and linked via the existing
link_attachments choke point - the route validates each uid belongs to
the actor and is an image, and an empty value on edit keeps the current
one. The title block, type/platform chips and author row overlay the
banner behind a scrim with a dark text shadow, next to an owner-set
Visit Website CTA; website_url and repo_url are normalized in models
and render with rel noopener nofollow.
An anchor tab bar (Overview, Devlog, Screenshots when present,
Comments, Files) navigates the page. The main column keeps About, the
devlog timeline (with devlog_count and an owner Post update button
opening the shared composer preset to the devlog topic + project - the
form now lives once in _post_composer_form.html, included by feed.html
and project_detail.html), a Screenshots gallery built from image
attachments minus the cover/logo (thumbnails, lightbox, 12 rendered),
and the comment thread; the sidebar holds Links, Stats and the Author
card. Owners add gallery images from the More menu via
POST /projects/{slug}/screenshots (owner-only, audit
project.screenshots.add, Devii action project_add_screenshots, docs id
projects-screenshots). comment_count/devlog_count ride
ProjectDetailOut, the new fields ride ProjectOut, and the create/edit
faces (modals, Devii actions, API docs) carry them. The project
comment/files e2e tests scope their locators per the documented
dual-control idiom, and new unit/api/e2e tests cover URL normalization,
the counts, the hero attachment guard, the screenshots flow and the
preset composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:00:12 +02:00
|
|
|
expect(devlog_section.locator("h2:has-text('Devlog')")).to_be_visible()
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
expect(page.locator(".empty-state:has-text('No devlog posts yet.')")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_linked_post_title(alice):
|
|
|
|
|
"""Linked post title renders in the devlog section."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"visible-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
devlog = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog.locator(f"h3:has-text('{marker}')")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_author_info(alice):
|
|
|
|
|
"""Author avatar, name and level appear on devlog posts."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"auth-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
post_card = page.locator(".project-devlog .post-card").first
|
|
|
|
|
expect(post_card.locator(".post-header")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-author-link")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-time")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_vote_and_comment_buttons(alice):
|
|
|
|
|
"""Devlog post renders vote buttons and comment count."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"action-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
post_card = page.locator(".project-devlog .post-card").first
|
Fix circular import, primary-admin NULL trap, and add gateway quota reset
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
2026-07-27 11:17:48 +02:00
|
|
|
expect(post_card.locator(".post-action-btn.vote-up")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-action-btn.vote-down")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-vote-count")).to_be_visible()
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
expect(post_card.locator("form[action*='/posts/delete/']")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_load_more_appears_with_many_posts(alice):
|
|
|
|
|
"""More than PAGE_SIZE posts produces a Load More link."""
|
|
|
|
|
from devplacepy.database.pagination import PAGE_SIZE
|
|
|
|
|
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
count = PAGE_SIZE + 1
|
|
|
|
|
for i in range(count):
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, i, title=f"loadmore-{i}")
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
devlog = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog.locator(".post-card")).to_have_count(PAGE_SIZE)
|
|
|
|
|
expect(devlog.locator(".load-more-wrap")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_guest_sees_devlog_section(app_server):
|
|
|
|
|
"""Unauthenticated visitors can see the devlog section."""
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
slug, _, _ = _seed_project()
|
|
|
|
|
|
|
|
|
|
r = requests.get(f"{BASE_URL}/projects/{slug}")
|
|
|
|
|
assert r.status_code == 200, r.text[:200]
|
|
|
|
|
|
|
|
|
|
assert "No devlog posts yet." in r.text
|
|
|
|
|
assert 'class="project-devlog"' in r.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_multiple_posts_order(alice):
|
|
|
|
|
"""Posts appear newest-first in the devlog."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
markers = []
|
|
|
|
|
for i in range(3):
|
|
|
|
|
marker = f"order-{i}-{uuid4().hex[:8]}"
|
|
|
|
|
markers.append(marker)
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, i, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
titles = page.locator(".project-devlog .post-card .post-title")
|
|
|
|
|
expect(titles).to_have_count(3)
|
|
|
|
|
first_text = titles.nth(0).inner_text()
|
|
|
|
|
assert markers[-1] == first_text, (
|
|
|
|
|
f"Expected newest post first: {markers[-1]}, got: {first_text}"
|
|
|
|
|
)
|
|
|
|
|
|
Dedicate the project page to the project
The project detail page becomes a full project showcase built entirely
from existing platform mechanisms. One encompassing dark card wraps the
page; inner panels (tab bar, sidebar cards, devlog entries, comments)
sit one elevation lighter. The hero opens with a cover banner and an
optional logo tile, both plain attachment references
(cover_attachment_uid/logo_attachment_uid) uploaded through the
standard dp-upload attachment widget and linked via the existing
link_attachments choke point - the route validates each uid belongs to
the actor and is an image, and an empty value on edit keeps the current
one. The title block, type/platform chips and author row overlay the
banner behind a scrim with a dark text shadow, next to an owner-set
Visit Website CTA; website_url and repo_url are normalized in models
and render with rel noopener nofollow.
An anchor tab bar (Overview, Devlog, Screenshots when present,
Comments, Files) navigates the page. The main column keeps About, the
devlog timeline (with devlog_count and an owner Post update button
opening the shared composer preset to the devlog topic + project - the
form now lives once in _post_composer_form.html, included by feed.html
and project_detail.html), a Screenshots gallery built from image
attachments minus the cover/logo (thumbnails, lightbox, 12 rendered),
and the comment thread; the sidebar holds Links, Stats and the Author
card. Owners add gallery images from the More menu via
POST /projects/{slug}/screenshots (owner-only, audit
project.screenshots.add, Devii action project_add_screenshots, docs id
projects-screenshots). comment_count/devlog_count ride
ProjectDetailOut, the new fields ride ProjectOut, and the create/edit
faces (modals, Devii actions, API docs) carry them. The project
comment/files e2e tests scope their locators per the documented
dual-control idiom, and new unit/api/e2e tests cover URL normalization,
the counts, the hero attachment guard, the screenshots flow and the
preset composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:00:12 +02:00
|
|
|
|
|
|
|
|
def test_project_stats_card_visible(alice):
|
|
|
|
|
"""The sidebar Stats card shows stars, updates, comments, files, forks."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
stats = page.locator(".project-stats")
|
|
|
|
|
expect(stats).to_be_visible()
|
|
|
|
|
expect(stats.locator(".project-stat")).to_have_count(5)
|
|
|
|
|
expect(stats.locator(".project-stat:has-text('update')")).to_contain_text("1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_owner_post_update_button_opens_preset_composer(alice):
|
|
|
|
|
"""The owner's Post update button opens the composer preselected to this project."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_project_ui(page, f"Composer Project {uuid4().hex[:6]}")
|
|
|
|
|
|
|
|
|
|
button = page.locator(".project-devlog-post-btn")
|
|
|
|
|
expect(button).to_be_visible()
|
|
|
|
|
button.click()
|
|
|
|
|
|
|
|
|
|
modal = page.locator("#create-post-modal")
|
|
|
|
|
expect(modal).to_be_visible()
|
|
|
|
|
expect(modal.locator("input[name='topic'][value='devlog']")).to_be_checked()
|
|
|
|
|
selected = modal.locator("#project_uid").input_value()
|
|
|
|
|
assert selected != "", "Expected the project preselected in the composer"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_guest_sees_no_post_update_button(app_server):
|
|
|
|
|
"""Guests and non-owners get no Post update control."""
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
slug, _, _ = _seed_project()
|
|
|
|
|
r = requests.get(f"{BASE_URL}/projects/{slug}")
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
assert "project-devlog-post-btn" not in r.text
|