Files
devplacepy/tests/e2e/docs.py
T
retoorandClaude Sonnet 5 3c69de9d55 Add Happy 404, featured/related sidebars, and next-post nav to the post page
Happy 404: an HTML 404 (unmatched route, or an explicit not-found inside a
real route) now renders a random existing post instead of the error page,
using the exact same context builder as a real post view. Toggle is the
happy_404_enabled site setting (default on, /admin/settings); JSON/API
requests and a handful of excluded prefixes are never affected. The pool of
candidate slugs is cached in-process and resampled periodically so it stays
fast and eventually cycles the whole posts table; on any internal failure it
falls straight through to the real 404 page.

Applying this everywhere surfaced ~60 existing tests that asserted a literal
404 for a legitimate resource-not-found flow (deleted post, unknown
container, wrong project slug, etc.) - each now disables the setting for the
duration of that specific check and restores it after, so the underlying
not-found behavior stays covered independently of the new feature.

Post page also gained, all built on the same shared post_page_context() so
they render identically on both a real post and a happy-404 page:
- A left sidebar (three separate cards, matching /feed's sidebar-card
  convention) for "Gists from {author}", "Projects from {author}" (private
  projects filtered through the normal visibility check), and "Related
  Discussions" - each cached per author and invalidated on create/edit/
  delete so new content shows up immediately.
- A right column reusing /feed's exact Daily Topic widget class for up to
  three "Featured" articles (the existing but previously-unused `featured`
  news flag), cached as a pool with per-request random sampling.
- A "Next post -> " link beside "Back to Feed", pointing at the next older
  post site-wide (blocked authors skipped). Wired through the same next_url
  mechanism already used for listing pagination, so it emits a real
  backend-rendered <link rel="next"> tag for SEO, not just a visible link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWJy6PrMMt5hwWxQwia2rd
2026-09-08 03:44:30 +02:00

198 lines
7.5 KiB
Python

# retoor <retoor@molodetz.nl>
import html
import json
import re
import time
import pytest
import requests
from tests.conftest import BASE_URL, CACHE_VERSION_PROPAGATION_SECONDS
from devplacepy.database import get_table, set_setting
from devplacepy.docs_api import API_GROUPS
from devplacepy.utils import clear_user_cache
@pytest.fixture(scope="module", autouse=True)
def _disable_happy_404(app_server):
set_setting("happy_404_enabled", "0")
time.sleep(CACHE_VERSION_PROPAGATION_SECONDS)
yield
set_setting("happy_404_enabled", "1")
time.sleep(CACHE_VERSION_PROPAGATION_SECONDS)
def _configs_by_id(text):
configs = re.findall(r"data-config='(.*?)'", text, re.S)
return {
json.loads(html.unescape(cfg))["id"]: json.loads(html.unescape(cfg))
for cfg in configs
}
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",
"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 test_docs_layout_has_sidebar(page, app_server):
page.goto(f"{BASE_URL}/docs/index.html", wait_until="domcontentloaded")
assert (
page.locator(
".sidebar-card a.sidebar-link[href='/docs/authentication.html']"
).count()
== 1
)
def test_docs_auth_examples_use_logged_in_user(alice):
page, user = alice
page.goto(f"{BASE_URL}/docs/authentication.html", wait_until="domcontentloaded")
content = page.content()
key = get_table("users").find_one(username=user["username"])["api_key"]
assert key in content
assert user["username"] in content
def test_docs_widget_code_has_highlight_linenumbers_copy(page, app_server):
page.goto(f"{BASE_URL}/docs/social-actions.html", wait_until="domcontentloaded")
panel = page.locator(".code-block pre.code-panel").first
panel.wait_for(state="visible")
panel.locator(".code-gutter").first.wait_for(state="attached")
assert panel.locator(".code-gutter").count() >= 1
assert panel.locator(".code-copy-btn").count() >= 1
assert panel.locator("code.hljs").count() >= 1
# switching language tab re-highlights (not left as plain text)
page.locator(".code-block .code-tab:has-text('Python')").first.click()
panel.locator("code.hljs").first.wait_for(state="attached")
assert panel.locator("code.hljs").count() >= 1
def test_docs_prose_code_has_linenumbers_copy(page, app_server):
page.goto(f"{BASE_URL}/docs/authentication.html", wait_until="domcontentloaded")
pre = page.locator(".docs-content pre.code-pre").first
pre.wait_for(state="visible")
assert pre.locator(".code-gutter").count() >= 1
assert pre.locator(".code-copy-btn").count() >= 1
assert pre.locator("code.hljs").count() >= 1
def test_docs_panel_format_picker_defaults_json(page, app_server):
page.goto(f"{BASE_URL}/docs/content.html", wait_until="domcontentloaded")
panel = page.locator(".api-tester").first
panel.wait_for(state="visible")
active = panel.locator(".format-option.active").first
active.wait_for(state="visible")
assert active.inner_text().strip() == "JSON"
assert panel.locator(".response-tabs .code-tab:has-text('Expected')").count() == 1
assert (
panel.locator(".response-tabs .code-tab:has-text('Live response')").count() == 1
)
expected = panel.locator(".response-pane.active").first
expected.wait_for(state="visible")
assert expected.locator("pre.response-body code").count() >= 1
def test_docs_panel_html_format_updates_snippet(page, app_server):
page.goto(f"{BASE_URL}/docs/content.html", wait_until="domcontentloaded")
panel = page.locator(".api-tester").first
panel.wait_for(state="visible")
code = panel.locator("pre.code-panel code").first
code.wait_for(state="attached")
assert "application/json" in code.inner_text()
panel.locator(".format-option:has-text('HTML')").first.click()
panel.locator("pre.code-panel code").filter(has_text="text/html").first.wait_for(
timeout=5000
)
def test_docs_panel_live_send_returns_json(page, app_server):
page.goto(f"{BASE_URL}/docs/issues.html", wait_until="domcontentloaded")
panel = page.locator(".api-tester").first
panel.wait_for(state="visible")
send = panel.locator(".try-send").first
send.wait_for(state="visible")
send.click()
status = panel.locator(".response-pane.active .response-status").first
status.wait_for(state="visible", timeout=10000)
assert status.inner_text().strip().startswith("2")
def test_guest_docs_hide_admin(page, app_server):
page.goto(f"{BASE_URL}/docs/index.html", wait_until="domcontentloaded")
assert page.locator("a[href='/docs/admin.html']").count() == 0
assert page.locator("a[href='/docs/services.html']").count() == 0
assert requests.get(f"{BASE_URL}/docs/admin.html").status_code == 404
assert requests.get(f"{BASE_URL}/docs/services.html").status_code == 404
def test_member_docs_hide_admin(bob):
page, _ = bob
assert (
requests.get(
f"{BASE_URL}/docs/admin.html", headers={"X-API-KEY": _key_role_visibility("bob_test")}
).status_code
== 404
)
page.goto(f"{BASE_URL}/docs/index.html", wait_until="domcontentloaded")
assert page.locator("a[href='/docs/admin.html']").count() == 0
def test_admin_docs_access(alice):
page, _ = alice
assert (
requests.get(
f"{BASE_URL}/docs/admin.html", headers={"X-API-KEY": _key_role_visibility("alice_test")}
).status_code
== 200
)
page.goto(f"{BASE_URL}/docs/index.html", wait_until="domcontentloaded")
assert page.locator("a[href='/docs/admin.html']").count() >= 1
def test_docs_search_falls_back_to_bm25_when_devii_unavailable(page, app_server):
# The test server runs with DEVPLACE_DISABLE_SERVICES=1, so agent search is
# unavailable and the docs search page falls back to the classic BM25 results.
page.goto(f"{BASE_URL}/docs/search.html?q=authentication", wait_until="domcontentloaded")
page.locator(".docs-search-bigform").wait_for(state="visible")
assert page.locator("dp-docs-chat").count() == 0
def test_docs_search_bm25_returns_results(page, app_server):
page.goto(f"{BASE_URL}/docs/search.html?q=authentication", wait_until="domcontentloaded")
titles = page.locator(".docs-search-results .docs-search-result-title")
titles.first.wait_for(state="visible")
assert titles.count() >= 1
def test_docs_no_floating_autoopen(page, app_server):
page.goto(f"{BASE_URL}/docs/index.html", wait_until="domcontentloaded")
assert page.locator("[data-devii-autoopen]").count() == 0