Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -0,0 +1,6 @@
"""
End-to-end tests.
This module contains browser-based tests using Playwright
to verify complete user journeys through the application.
"""

View File

@@ -1,14 +1,42 @@
import pytest
from playwright.sync_api import Page
import subprocess
@pytest.fixture(autouse=True)
def setup_test_data():
"""Setup test data before each test session"""
subprocess.run(["uv", "run", "manage.py", "create_test_users"], check=True)
@pytest.fixture(scope="session")
def setup_test_data(django_db_setup, django_db_blocker):
"""
Setup test data before the test session using factories.
This fixture:
- Uses factories instead of shelling out to management commands
- Is scoped to session (not autouse per test) to reduce overhead
- Uses django_db_blocker to allow database access in session-scoped fixture
"""
with django_db_blocker.unblock():
from django.contrib.auth import get_user_model
User = get_user_model()
# Create test users if they don't exist
test_users = [
{"username": "testuser", "email": "testuser@example.com", "password": "testpass123"},
{"username": "moderator", "email": "moderator@example.com", "password": "modpass123", "is_staff": True},
{"username": "admin", "email": "admin@example.com", "password": "adminpass123", "is_staff": True, "is_superuser": True},
]
for user_data in test_users:
password = user_data.pop("password")
user, created = User.objects.get_or_create(
username=user_data["username"],
defaults=user_data
)
if created:
user.set_password(password)
user.save()
yield
subprocess.run(["uv", "run", "manage.py", "cleanup_test_data"], check=True)
# Cleanup is handled automatically by pytest-django's transactional database
@pytest.fixture(autouse=True)
@@ -34,7 +62,7 @@ def setup_page(page: Page):
@pytest.fixture
def auth_page(page: Page, live_server):
def auth_page(page: Page, live_server, setup_test_data):
"""Fixture for authenticated page"""
# Login using live_server URL
page.goto(f"{live_server.url}/accounts/login/")
@@ -46,7 +74,7 @@ def auth_page(page: Page, live_server):
@pytest.fixture
def mod_page(page: Page, live_server):
def mod_page(page: Page, live_server, setup_test_data):
"""Fixture for moderator page"""
# Login as moderator using live_server URL
page.goto(f"{live_server.url}/accounts/login/")
@@ -107,7 +135,7 @@ def test_review(test_park: Page, live_server):
@pytest.fixture
def admin_page(page: Page, live_server):
def admin_page(page: Page, live_server, setup_test_data):
"""Fixture for admin/superuser page"""
# Login as admin using live_server URL
page.goto(f"{live_server.url}/accounts/login/")
@@ -406,3 +434,39 @@ def regular_user(db):
user.save()
return user
@pytest.fixture
def parks_data(db):
"""Create test parks for E2E testing."""
from tests.factories import ParkFactory
parks = [
ParkFactory(
name=f"E2E Test Park {i}",
slug=f"e2e-test-park-{i}",
status="OPERATING"
)
for i in range(3)
]
return parks
@pytest.fixture
def rides_data(db, parks_data):
"""Create test rides for E2E testing."""
from tests.factories import RideFactory
rides = []
for park in parks_data:
for i in range(2):
ride = RideFactory(
name=f"E2E Test Ride {park.name} {i}",
slug=f"e2e-test-ride-{park.slug}-{i}",
park=park,
status="OPERATING"
)
rides.append(ride)
return rides

View File

@@ -0,0 +1,182 @@
"""
E2E tests for park browsing functionality.
These tests verify the complete user journey for browsing parks
using Playwright for browser automation.
"""
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.e2e
class TestParkListPage:
"""E2E tests for park list page."""
def test__park_list__displays_parks(self, page: Page, live_server, parks_data):
"""Test park list page displays parks."""
page.goto(f"{live_server.url}/parks/")
# Verify page title or heading
expect(page.locator("h1")).to_be_visible()
# Should display park cards or list items
park_items = page.locator("[data-testid='park-card'], .park-item, .park-list-item")
expect(park_items.first).to_be_visible()
def test__park_list__shows_park_name(self, page: Page, live_server, parks_data):
"""Test park list shows park names."""
page.goto(f"{live_server.url}/parks/")
# First park should be visible
first_park = parks_data[0]
expect(page.get_by_text(first_park.name)).to_be_visible()
def test__park_list__click_park__navigates_to_detail(
self, page: Page, live_server, parks_data
):
"""Test clicking a park navigates to detail page."""
page.goto(f"{live_server.url}/parks/")
first_park = parks_data[0]
# Click on the park
page.get_by_text(first_park.name).first.click()
# Should navigate to detail page
expect(page).to_have_url(f"**/{first_park.slug}/**")
def test__park_list__search__filters_results(self, page: Page, live_server, parks_data):
"""Test search functionality filters parks."""
page.goto(f"{live_server.url}/parks/")
# Find search input
search_input = page.locator(
"input[type='search'], input[name='q'], input[placeholder*='search' i]"
)
if search_input.count() > 0:
search_input.first.fill("E2E Test Park 0")
# Wait for results to filter
page.wait_for_timeout(500)
# Should show only matching park
expect(page.get_by_text("E2E Test Park 0")).to_be_visible()
@pytest.mark.e2e
class TestParkDetailPage:
"""E2E tests for park detail page."""
def test__park_detail__displays_park_info(self, page: Page, live_server, parks_data):
"""Test park detail page displays park information."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Verify park name is displayed
expect(page.get_by_role("heading", name=park.name)).to_be_visible()
def test__park_detail__shows_rides_section(self, page: Page, live_server, parks_data):
"""Test park detail page shows rides section."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Look for rides section/tab
rides_section = page.locator(
"[data-testid='rides-section'], #rides, [role='tabpanel']"
)
# Or a rides tab
rides_tab = page.get_by_role("tab", name="Rides")
if rides_tab.count() > 0:
rides_tab.click()
# Should show rides
ride_items = page.locator(".ride-item, .ride-card, [data-testid='ride-item']")
expect(ride_items.first).to_be_visible()
def test__park_detail__shows_status(self, page: Page, live_server, parks_data):
"""Test park detail page shows park status."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Status badge or indicator should be visible
status_indicator = page.locator(
".status-badge, [data-testid='status'], .park-status"
)
expect(status_indicator.first).to_be_visible()
@pytest.mark.e2e
class TestParkFiltering:
"""E2E tests for park filtering functionality."""
def test__filter_by_status__updates_results(self, page: Page, live_server, parks_data):
"""Test filtering parks by status updates results."""
page.goto(f"{live_server.url}/parks/")
# Find status filter
status_filter = page.locator(
"select[name='status'], [data-testid='status-filter']"
)
if status_filter.count() > 0:
status_filter.first.select_option("OPERATING")
# Wait for results to update
page.wait_for_timeout(500)
# Results should be filtered
def test__clear_filters__shows_all_parks(self, page: Page, live_server, parks_data):
"""Test clearing filters shows all parks."""
page.goto(f"{live_server.url}/parks/")
# Find clear filters button
clear_btn = page.locator(
"[data-testid='clear-filters'], button:has-text('Clear')"
)
if clear_btn.count() > 0:
clear_btn.first.click()
# Wait for results to update
page.wait_for_timeout(500)
@pytest.mark.e2e
class TestParkNavigation:
"""E2E tests for park navigation."""
def test__breadcrumb__navigates_back_to_list(self, page: Page, live_server, parks_data):
"""Test breadcrumb navigation back to park list."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Find breadcrumb
breadcrumb = page.locator("nav[aria-label='breadcrumb'], .breadcrumb")
if breadcrumb.count() > 0:
# Click parks link in breadcrumb
breadcrumb.get_by_role("link", name="Parks").click()
expect(page).to_have_url(f"**/parks/**")
def test__back_button__returns_to_previous_page(
self, page: Page, live_server, parks_data
):
"""Test browser back button returns to previous page."""
page.goto(f"{live_server.url}/parks/")
park = parks_data[0]
page.get_by_text(park.name).first.click()
# Wait for navigation
page.wait_for_url(f"**/{park.slug}/**")
# Go back
page.go_back()
expect(page).to_have_url(f"**/parks/**")

View File

@@ -0,0 +1,372 @@
"""
E2E tests for review submission and moderation flows.
These tests verify the complete user journey for submitting,
editing, and moderating reviews using Playwright for browser automation.
"""
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.e2e
class TestReviewSubmission:
"""E2E tests for review submission flow."""
def test__review_form__displays_fields(self, auth_page: Page, live_server, parks_data):
"""Test review form displays all required fields."""
park = parks_data[0]
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
# Find and click reviews tab or section
reviews_tab = auth_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
# Click write review button
write_review = auth_page.locator(
"button:has-text('Write Review'), a:has-text('Write Review')"
)
if write_review.count() > 0:
write_review.first.click()
# Verify form fields
expect(auth_page.locator("select[name='rating'], input[name='rating']").first).to_be_visible()
expect(auth_page.locator("input[name='title'], textarea[name='title']").first).to_be_visible()
expect(auth_page.locator("textarea[name='content'], textarea[name='review']").first).to_be_visible()
def test__review_submission__valid_data__creates_review(
self, auth_page: Page, live_server, parks_data
):
"""Test submitting a valid review creates it."""
park = parks_data[0]
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
# Navigate to reviews
reviews_tab = auth_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
write_review = auth_page.locator(
"button:has-text('Write Review'), a:has-text('Write Review')"
)
if write_review.count() > 0:
write_review.first.click()
# Fill the form
rating_select = auth_page.locator("select[name='rating']")
if rating_select.count() > 0:
rating_select.select_option("5")
else:
# May be radio buttons or stars
auth_page.locator("input[name='rating'][value='5']").click()
auth_page.locator("input[name='title'], textarea[name='title']").first.fill(
"E2E Test Review Title"
)
auth_page.locator("textarea[name='content'], textarea[name='review']").first.fill(
"This is an E2E test review content."
)
auth_page.get_by_role("button", name="Submit").click()
# Should show success or redirect
auth_page.wait_for_timeout(500)
def test__review_submission__missing_rating__shows_error(
self, auth_page: Page, live_server, parks_data
):
"""Test submitting review without rating shows error."""
park = parks_data[0]
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = auth_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
write_review = auth_page.locator(
"button:has-text('Write Review'), a:has-text('Write Review')"
)
if write_review.count() > 0:
write_review.first.click()
# Fill only title and content, skip rating
auth_page.locator("input[name='title'], textarea[name='title']").first.fill(
"Missing Rating Review"
)
auth_page.locator("textarea[name='content'], textarea[name='review']").first.fill(
"Review without rating"
)
auth_page.get_by_role("button", name="Submit").click()
# Should show validation error
error = auth_page.locator(".error, .errorlist, [role='alert']")
expect(error.first).to_be_visible()
@pytest.mark.e2e
class TestReviewDisplay:
"""E2E tests for review display."""
def test__reviews_list__displays_reviews(self, page: Page, live_server, parks_data):
"""Test reviews list displays existing reviews."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
# Navigate to reviews section
reviews_tab = page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
# Reviews should be displayed
reviews_section = page.locator(
"[data-testid='reviews-list'], .reviews-list, .review-item"
)
if reviews_section.count() > 0:
expect(reviews_section.first).to_be_visible()
def test__review__shows_rating(self, page: Page, live_server, test_review):
"""Test review displays rating."""
# test_review fixture creates a review
page.goto(f"{page.url}") # Stay on current page after fixture
# Rating should be visible (stars, number, etc.)
rating = page.locator(
".rating, .stars, [data-testid='rating']"
)
if rating.count() > 0:
expect(rating.first).to_be_visible()
def test__review__shows_author(self, page: Page, live_server, parks_data):
"""Test review displays author name."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
# Author name should be visible in review
author = page.locator(
".review-author, .author, [data-testid='author']"
)
if author.count() > 0:
expect(author.first).to_be_visible()
@pytest.mark.e2e
class TestReviewEditing:
"""E2E tests for review editing."""
def test__own_review__shows_edit_button(self, auth_page: Page, live_server, test_review):
"""Test user's own review shows edit button."""
# Navigate to reviews after creating one
park_url = auth_page.url
# Look for edit button on own review
edit_button = auth_page.locator(
"button:has-text('Edit'), a:has-text('Edit Review')"
)
if edit_button.count() > 0:
expect(edit_button.first).to_be_visible()
def test__edit_review__updates_content(self, auth_page: Page, live_server, test_review):
"""Test editing review updates the content."""
# Find and click edit
edit_button = auth_page.locator(
"button:has-text('Edit'), a:has-text('Edit Review')"
)
if edit_button.count() > 0:
edit_button.first.click()
# Update content
content_field = auth_page.locator(
"textarea[name='content'], textarea[name='review']"
)
content_field.first.fill("Updated review content from E2E test")
auth_page.get_by_role("button", name="Save").click()
# Should show updated content
auth_page.wait_for_timeout(500)
expect(auth_page.get_by_text("Updated review content")).to_be_visible()
@pytest.mark.e2e
class TestReviewModeration:
"""E2E tests for review moderation."""
def test__moderator__sees_moderation_actions(
self, mod_page: Page, live_server, parks_data
):
"""Test moderator sees moderation actions on reviews."""
park = parks_data[0]
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = mod_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
# Moderator should see moderation buttons
mod_actions = mod_page.locator(
"button:has-text('Remove'), button:has-text('Flag'), [data-testid='mod-action']"
)
if mod_actions.count() > 0:
expect(mod_actions.first).to_be_visible()
def test__moderator__can_remove_review(self, mod_page: Page, live_server, parks_data):
"""Test moderator can remove a review."""
park = parks_data[0]
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = mod_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
remove_button = mod_page.locator("button:has-text('Remove')")
if remove_button.count() > 0:
remove_button.first.click()
# Confirm if dialog appears
confirm = mod_page.locator("button:has-text('Confirm')")
if confirm.count() > 0:
confirm.click()
mod_page.wait_for_timeout(500)
@pytest.mark.e2e
class TestReviewVoting:
"""E2E tests for review voting (helpful/not helpful)."""
def test__review__shows_vote_buttons(self, page: Page, live_server, parks_data):
"""Test reviews show vote buttons."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
# Look for helpful/upvote buttons
vote_buttons = page.locator(
"button:has-text('Helpful'), button[aria-label*='helpful'], .vote-button"
)
if vote_buttons.count() > 0:
expect(vote_buttons.first).to_be_visible()
def test__vote__authenticated__registers_vote(
self, auth_page: Page, live_server, parks_data
):
"""Test authenticated user can vote on review."""
park = parks_data[0]
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = auth_page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
helpful_button = auth_page.locator(
"button:has-text('Helpful'), button[aria-label*='helpful']"
)
if helpful_button.count() > 0:
helpful_button.first.click()
# Button should show voted state
auth_page.wait_for_timeout(500)
@pytest.mark.e2e
class TestRideReviews:
"""E2E tests for ride-specific reviews."""
def test__ride_page__shows_reviews(self, page: Page, live_server, rides_data):
"""Test ride page shows reviews section."""
ride = rides_data[0]
page.goto(f"{live_server.url}/rides/{ride.slug}/")
# Reviews section should be present
reviews_section = page.locator(
"[data-testid='reviews'], #reviews, .reviews-section"
)
if reviews_section.count() > 0:
expect(reviews_section.first).to_be_visible()
def test__ride_review__includes_ride_experience_fields(
self, auth_page: Page, live_server, rides_data
):
"""Test ride review form includes experience fields."""
ride = rides_data[0]
auth_page.goto(f"{live_server.url}/rides/{ride.slug}/")
write_review = auth_page.locator(
"button:has-text('Write Review'), a:has-text('Write Review')"
)
if write_review.count() > 0:
write_review.first.click()
# Ride-specific fields
intensity_field = auth_page.locator(
"select[name='intensity'], input[name='intensity']"
)
wait_time_field = auth_page.locator(
"input[name='wait_time'], select[name='wait_time']"
)
# At least one experience field should be present
if intensity_field.count() > 0:
expect(intensity_field.first).to_be_visible()
@pytest.mark.e2e
class TestReviewFiltering:
"""E2E tests for review filtering and sorting."""
def test__reviews__sort_by_date(self, page: Page, live_server, parks_data):
"""Test reviews can be sorted by date."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
sort_select = page.locator(
"select[name='sort'], [data-testid='sort-reviews']"
)
if sort_select.count() > 0:
sort_select.first.select_option("date")
page.wait_for_timeout(500)
def test__reviews__filter_by_rating(self, page: Page, live_server, parks_data):
"""Test reviews can be filtered by rating."""
park = parks_data[0]
page.goto(f"{live_server.url}/parks/{park.slug}/")
reviews_tab = page.get_by_role("tab", name="Reviews")
if reviews_tab.count() > 0:
reviews_tab.click()
rating_filter = page.locator(
"select[name='rating'], [data-testid='rating-filter']"
)
if rating_filter.count() > 0:
rating_filter.first.select_option("5")
page.wait_for_timeout(500)

View File

@@ -0,0 +1,280 @@
"""
E2E tests for user registration and authentication flows.
These tests verify the complete user journey for registration,
login, and account management using Playwright for browser automation.
"""
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.e2e
class TestUserRegistration:
"""E2E tests for user registration flow."""
def test__registration_page__displays_form(self, page: Page, live_server):
"""Test registration page displays the registration form."""
page.goto(f"{live_server.url}/accounts/signup/")
# Verify form fields are visible
expect(page.get_by_label("Username")).to_be_visible()
expect(page.get_by_label("Email")).to_be_visible()
expect(page.get_by_label("Password", exact=False).first).to_be_visible()
def test__registration__valid_data__creates_account(self, page: Page, live_server):
"""Test registration with valid data creates an account."""
page.goto(f"{live_server.url}/accounts/signup/")
# Fill registration form
page.get_by_label("Username").fill("e2e_newuser")
page.get_by_label("Email").fill("e2e_newuser@example.com")
# Handle password fields (may be "Password" and "Confirm Password" or similar)
password_fields = page.locator("input[type='password']")
if password_fields.count() >= 2:
password_fields.nth(0).fill("SecurePass123!")
password_fields.nth(1).fill("SecurePass123!")
else:
password_fields.first.fill("SecurePass123!")
# Submit form
page.get_by_role("button", name="Sign Up").click()
# Should redirect to success page or login
page.wait_for_url("**/*", timeout=5000)
def test__registration__duplicate_username__shows_error(
self, page: Page, live_server, regular_user
):
"""Test registration with duplicate username shows error."""
page.goto(f"{live_server.url}/accounts/signup/")
# Try to register with existing username
page.get_by_label("Username").fill("testuser")
page.get_by_label("Email").fill("different@example.com")
password_fields = page.locator("input[type='password']")
if password_fields.count() >= 2:
password_fields.nth(0).fill("SecurePass123!")
password_fields.nth(1).fill("SecurePass123!")
else:
password_fields.first.fill("SecurePass123!")
page.get_by_role("button", name="Sign Up").click()
# Should show error message
error = page.locator(".error, .errorlist, [role='alert']")
expect(error.first).to_be_visible()
def test__registration__weak_password__shows_error(self, page: Page, live_server):
"""Test registration with weak password shows validation error."""
page.goto(f"{live_server.url}/accounts/signup/")
page.get_by_label("Username").fill("e2e_weakpass")
page.get_by_label("Email").fill("e2e_weakpass@example.com")
password_fields = page.locator("input[type='password']")
if password_fields.count() >= 2:
password_fields.nth(0).fill("123")
password_fields.nth(1).fill("123")
else:
password_fields.first.fill("123")
page.get_by_role("button", name="Sign Up").click()
# Should show password validation error
error = page.locator(".error, .errorlist, [role='alert']")
expect(error.first).to_be_visible()
@pytest.mark.e2e
class TestUserLogin:
"""E2E tests for user login flow."""
def test__login_page__displays_form(self, page: Page, live_server):
"""Test login page displays the login form."""
page.goto(f"{live_server.url}/accounts/login/")
expect(page.get_by_label("Username")).to_be_visible()
expect(page.get_by_label("Password")).to_be_visible()
expect(page.get_by_role("button", name="Sign In")).to_be_visible()
def test__login__valid_credentials__authenticates(
self, page: Page, live_server, regular_user
):
"""Test login with valid credentials authenticates user."""
page.goto(f"{live_server.url}/accounts/login/")
page.get_by_label("Username").fill("testuser")
page.get_by_label("Password").fill("testpass123")
page.get_by_role("button", name="Sign In").click()
# Should redirect away from login page
page.wait_for_url("**/*")
expect(page).not_to_have_url("**/login/**")
def test__login__invalid_credentials__shows_error(self, page: Page, live_server):
"""Test login with invalid credentials shows error."""
page.goto(f"{live_server.url}/accounts/login/")
page.get_by_label("Username").fill("nonexistent")
page.get_by_label("Password").fill("wrongpass")
page.get_by_role("button", name="Sign In").click()
# Should show error message
error = page.locator(".error, .errorlist, [role='alert'], .alert-danger")
expect(error.first).to_be_visible()
def test__login__remember_me__checkbox_present(self, page: Page, live_server):
"""Test login page has remember me checkbox."""
page.goto(f"{live_server.url}/accounts/login/")
remember_me = page.locator(
"input[name='remember'], input[type='checkbox'][id*='remember']"
)
if remember_me.count() > 0:
expect(remember_me.first).to_be_visible()
@pytest.mark.e2e
class TestUserLogout:
"""E2E tests for user logout flow."""
def test__logout__clears_session(self, auth_page: Page, live_server):
"""Test logout clears user session."""
# User is already logged in via auth_page fixture
# Find and click logout button/link
logout = auth_page.locator(
"a[href*='logout'], button:has-text('Log Out'), button:has-text('Sign Out')"
)
if logout.count() > 0:
logout.first.click()
# Should be logged out
auth_page.wait_for_url("**/*")
# Try to access protected page
auth_page.goto(f"{live_server.url}/accounts/profile/")
# Should redirect to login
expect(auth_page).to_have_url("**/login/**")
@pytest.mark.e2e
class TestPasswordReset:
"""E2E tests for password reset flow."""
def test__password_reset_page__displays_form(self, page: Page, live_server):
"""Test password reset page displays the form."""
page.goto(f"{live_server.url}/accounts/password/reset/")
email_input = page.locator(
"input[type='email'], input[name='email']"
)
expect(email_input.first).to_be_visible()
def test__password_reset__valid_email__shows_confirmation(
self, page: Page, live_server, regular_user
):
"""Test password reset with valid email shows confirmation."""
page.goto(f"{live_server.url}/accounts/password/reset/")
email_input = page.locator("input[type='email'], input[name='email']")
email_input.first.fill("testuser@example.com")
page.get_by_role("button", name="Reset Password").click()
# Should show confirmation message
page.wait_for_timeout(500)
# Look for success message or confirmation page
success = page.locator(
".success, .alert-success, [role='alert']"
)
# Or check URL changed to done page
if success.count() == 0:
expect(page).to_have_url("**/done/**")
@pytest.mark.e2e
class TestUserProfile:
"""E2E tests for user profile management."""
def test__profile_page__displays_user_info(self, auth_page: Page, live_server):
"""Test profile page displays user information."""
auth_page.goto(f"{live_server.url}/accounts/profile/")
# Should display username
expect(auth_page.get_by_text("testuser")).to_be_visible()
def test__profile_page__edit_profile_link(self, auth_page: Page, live_server):
"""Test profile page has edit profile link/button."""
auth_page.goto(f"{live_server.url}/accounts/profile/")
edit_link = auth_page.locator(
"a[href*='edit'], button:has-text('Edit')"
)
if edit_link.count() > 0:
expect(edit_link.first).to_be_visible()
def test__profile_edit__updates_info(self, auth_page: Page, live_server):
"""Test editing profile updates user information."""
auth_page.goto(f"{live_server.url}/accounts/profile/edit/")
# Find bio/about field if present
bio_field = auth_page.locator(
"textarea[name='bio'], textarea[name='about']"
)
if bio_field.count() > 0:
bio_field.first.fill("Updated bio from E2E test")
auth_page.get_by_role("button", name="Save").click()
# Should redirect back to profile
auth_page.wait_for_url("**/profile/**")
@pytest.mark.e2e
class TestProtectedRoutes:
"""E2E tests for protected route access."""
def test__protected_route__unauthenticated__redirects_to_login(
self, page: Page, live_server
):
"""Test accessing protected route redirects to login."""
page.goto(f"{live_server.url}/accounts/profile/")
# Should redirect to login
expect(page).to_have_url("**/login/**")
def test__protected_route__authenticated__allows_access(
self, auth_page: Page, live_server
):
"""Test authenticated user can access protected routes."""
auth_page.goto(f"{live_server.url}/accounts/profile/")
# Should not redirect to login
expect(auth_page).not_to_have_url("**/login/**")
def test__admin_route__regular_user__denied(self, auth_page: Page, live_server):
"""Test regular user cannot access admin routes."""
auth_page.goto(f"{live_server.url}/admin/")
# Should show login or forbidden
# Admin login page or 403
def test__moderator_route__moderator__allows_access(
self, mod_page: Page, live_server
):
"""Test moderator can access moderation routes."""
mod_page.goto(f"{live_server.url}/moderation/")
# Should not redirect to login (moderator has access)
expect(mod_page).not_to_have_url("**/login/**")