mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 03:31:09 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
6
backend/tests/e2e/__init__.py
Normal file
6
backend/tests/e2e/__init__.py
Normal 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.
|
||||
"""
|
||||
@@ -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
|
||||
|
||||
182
backend/tests/e2e/test_park_browsing.py
Normal file
182
backend/tests/e2e/test_park_browsing.py
Normal 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/**")
|
||||
372
backend/tests/e2e/test_review_submission.py
Normal file
372
backend/tests/e2e/test_review_submission.py
Normal 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)
|
||||
280
backend/tests/e2e/test_user_registration.py
Normal file
280
backend/tests/e2e/test_user_registration.py
Normal 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/**")
|
||||
Reference in New Issue
Block a user