2026-07-26 14:57:18 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
|
|
|
|
|
JSON = {"Accept": "application/json"}
|
|
|
|
|
|
|
|
|
|
LAYOUT = ".feed-layout"
|
|
|
|
|
LEFT = ".feed-layout > .sidebar-card"
|
|
|
|
|
CENTRE = ".feed-layout > .feed-main"
|
|
|
|
|
RIGHT = ".feed-layout > .feed-right"
|
|
|
|
|
NAV_ENTRY = ".topnav-link[href='/quizzes']"
|
|
|
|
|
NEW_QUIZ_BUTTON = ".quiz-actions a[href='/quizzes/new']"
|
|
|
|
|
DEVII_BUTTON = ".quiz-actions button[data-devii-open]"
|
|
|
|
|
SCOREBOARD = ".quiz-scoreboard-card"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_quiz(title="Hub e2e quiz"):
|
|
|
|
|
session = requests.Session()
|
|
|
|
|
name = f"hub{abs(hash(title)) % 10**9}"
|
|
|
|
|
session.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",
|
2026-07-26 14:57:18 +02:00
|
|
|
},
|
|
|
|
|
allow_redirects=True,
|
|
|
|
|
)
|
|
|
|
|
slug = session.post(
|
|
|
|
|
f"{BASE_URL}/quizzes/create", data={"title": title, "description": "d"}, headers=JSON
|
|
|
|
|
).json()["data"]["slug"]
|
|
|
|
|
session.post(
|
|
|
|
|
f"{BASE_URL}/quizzes/{slug}/questions",
|
|
|
|
|
data={
|
|
|
|
|
"kind": "single_choice",
|
|
|
|
|
"prompt": "Which journal mode allows concurrent readers?",
|
|
|
|
|
"points": 2,
|
|
|
|
|
"options": "DELETE\nWAL",
|
|
|
|
|
"correct_indexes": "1",
|
|
|
|
|
},
|
|
|
|
|
headers=JSON,
|
|
|
|
|
)
|
|
|
|
|
session.post(f"{BASE_URL}/quizzes/{slug}/publish", headers=JSON)
|
|
|
|
|
return slug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_hub_renders_the_three_column_layout(page, app_server):
|
|
|
|
|
_seed_quiz("Layout e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(LAYOUT).wait_for(state="visible")
|
|
|
|
|
assert page.locator(LEFT).count() == 1
|
|
|
|
|
assert page.locator(CENTRE).count() == 1
|
|
|
|
|
assert page.locator(RIGHT).count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_hub_has_a_visible_heading(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
heading = page.locator("h1.quiz-page-title")
|
|
|
|
|
heading.wait_for(state="visible")
|
|
|
|
|
assert heading.inner_text().strip() == "Quizzes"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_nav_entry_is_visible_to_a_guest_and_active(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
entry = page.locator(NAV_ENTRY)
|
|
|
|
|
entry.wait_for(state="visible")
|
|
|
|
|
assert "active" in (entry.get_attribute("class") or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_guest_sees_disabled_create_controls(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".quiz-actions").wait_for(state="visible")
|
|
|
|
|
assert page.locator(NEW_QUIZ_BUTTON).get_attribute("aria-disabled") == "true"
|
|
|
|
|
assert page.locator(DEVII_BUTTON).is_disabled()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_guest_still_sees_the_list_and_the_scoreboard(page, app_server):
|
|
|
|
|
_seed_quiz("Guest visible quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(SCOREBOARD).wait_for(state="visible")
|
|
|
|
|
assert page.locator(".quiz-card").count() >= 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_member_sees_enabled_create_controls(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(NEW_QUIZ_BUTTON).wait_for(state="visible")
|
|
|
|
|
assert page.locator(NEW_QUIZ_BUTTON).get_attribute("aria-disabled") is None
|
|
|
|
|
assert page.locator(DEVII_BUTTON).is_enabled()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_member_sees_the_personal_filters(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".sidebar-nav").wait_for(state="visible")
|
|
|
|
|
for target in ("todo", "done", "mine", "drafts"):
|
|
|
|
|
assert page.locator(f".sidebar-link[href='/quizzes?filter={target}']").count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_guest_sees_only_the_all_filter(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".sidebar-nav").wait_for(state="visible")
|
|
|
|
|
assert page.locator(".sidebar-link[href='/quizzes?filter=todo']").count() == 0
|
|
|
|
|
assert page.locator(".sidebar-link[href='/quizzes?filter=all']").count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_quiz_card_shows_its_state_badge(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug = _seed_quiz("Badge e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
card = page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").first
|
|
|
|
|
card.wait_for(state="visible")
|
|
|
|
|
assert card.locator(".quiz-state-todo").count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_quiz_card_shows_its_meta_chips(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Meta chips e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
card = page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").first
|
|
|
|
|
card.wait_for(state="visible")
|
|
|
|
|
assert "1 questions" in card.inner_text()
|
|
|
|
|
assert "2 points" in card.inner_text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_search_box_filters_the_list(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Unmistakable pelican quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes?search=pelican", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".feed-posts").wait_for(state="visible")
|
|
|
|
|
assert page.locator(f".quiz-card:has(a[href='/quizzes/{slug}'])").count() == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_new_quiz_button_opens_the_create_form(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(NEW_QUIZ_BUTTON).click()
|
|
|
|
|
page.wait_for_url("**/quizzes/new", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("form.quiz-form").wait_for(state="visible")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_devii_button_seeds_the_terminal_without_sending(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
trigger = page.locator(DEVII_BUTTON)
|
|
|
|
|
trigger.wait_for(state="visible")
|
|
|
|
|
prompt = trigger.get_attribute("data-devii-prompt")
|
|
|
|
|
assert prompt and "import_quiz" in prompt
|
|
|
|
|
trigger.click()
|
|
|
|
|
terminal_input = page.locator("devii-terminal .devii-input")
|
|
|
|
|
terminal_input.wait_for(state="visible", timeout=10000)
|
|
|
|
|
assert terminal_input.input_value() == prompt
|
|
|
|
|
assert page.locator("devii-terminal .devii-line-user").count() == 0
|
|
|
|
|
page.evaluate("window.app && window.app.devii && window.app.devii.close()")
|
|
|
|
|
page.evaluate("window.localStorage.clear()")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_opening_a_card_reaches_the_detail_page(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Card link e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(f".quiz-card a.card-link[href='/quizzes/{slug}']").first.click()
|
|
|
|
|
page.wait_for_url(f"**/quizzes/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("h1.quiz-detail-title").wait_for(state="visible")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_detail_page_shows_the_stats_strip(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Stats strip e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".quiz-stats-strip").wait_for(state="visible")
|
|
|
|
|
assert page.locator(".quiz-stat").count() == 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_detail_page_offers_a_start_button_to_a_member(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug = _seed_quiz("Start button e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
button = page.locator(".quiz-detail-actions button:has-text('Start quiz')")
|
|
|
|
|
button.wait_for(state="visible")
|
|
|
|
|
assert button.is_enabled()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_detail_page_disables_start_for_a_guest(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Guest start e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
button = page.locator(".quiz-detail-actions button:has-text('Start quiz')")
|
|
|
|
|
button.wait_for(state="visible")
|
|
|
|
|
assert button.is_disabled()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_the_detail_page_carries_a_comment_section(page, app_server):
|
|
|
|
|
slug = _seed_quiz("Comment section e2e quiz")
|
|
|
|
|
page.goto(f"{BASE_URL}/quizzes/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".comments-section").wait_for(state="visible")
|