mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:47:04 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -25,15 +25,18 @@ def setup_test_data(django_db_setup, django_db_blocker):
|
||||
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},
|
||||
{
|
||||
"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
|
||||
)
|
||||
user, created = User.objects.get_or_create(username=user_data["username"], defaults=user_data)
|
||||
if created:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
@@ -55,11 +58,7 @@ def setup_page(page: Page):
|
||||
# Listen for console errors
|
||||
page.on(
|
||||
"console",
|
||||
lambda msg: (
|
||||
print(f"Browser console {msg.type}: {msg.text}")
|
||||
if msg.type == "error"
|
||||
else None
|
||||
),
|
||||
lambda msg: (print(f"Browser console {msg.type}: {msg.text}") if msg.type == "error" else None),
|
||||
)
|
||||
|
||||
yield page
|
||||
@@ -98,37 +97,356 @@ def test_images():
|
||||
"""
|
||||
# Minimal valid JPEG image (1x1 red pixel)
|
||||
# This is a valid JPEG that any image library will accept
|
||||
jpeg_bytes = bytes([
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
|
||||
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
|
||||
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
|
||||
0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
|
||||
0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
|
||||
0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
|
||||
0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
|
||||
0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
|
||||
0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00,
|
||||
0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03,
|
||||
0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
|
||||
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
|
||||
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
|
||||
0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72,
|
||||
0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
||||
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45,
|
||||
0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
||||
0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
||||
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3,
|
||||
0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
|
||||
0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
|
||||
0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
||||
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
|
||||
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
|
||||
0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xBA, 0xAE, 0xAF,
|
||||
0xDA, 0xAD, 0x28, 0xA6, 0x02, 0x8A, 0x28, 0x03, 0xFF, 0xD9
|
||||
])
|
||||
jpeg_bytes = bytes(
|
||||
[
|
||||
0xFF,
|
||||
0xD8,
|
||||
0xFF,
|
||||
0xE0,
|
||||
0x00,
|
||||
0x10,
|
||||
0x4A,
|
||||
0x46,
|
||||
0x49,
|
||||
0x46,
|
||||
0x00,
|
||||
0x01,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFF,
|
||||
0xDB,
|
||||
0x00,
|
||||
0x43,
|
||||
0x00,
|
||||
0x08,
|
||||
0x06,
|
||||
0x06,
|
||||
0x07,
|
||||
0x06,
|
||||
0x05,
|
||||
0x08,
|
||||
0x07,
|
||||
0x07,
|
||||
0x07,
|
||||
0x09,
|
||||
0x09,
|
||||
0x08,
|
||||
0x0A,
|
||||
0x0C,
|
||||
0x14,
|
||||
0x0D,
|
||||
0x0C,
|
||||
0x0B,
|
||||
0x0B,
|
||||
0x0C,
|
||||
0x19,
|
||||
0x12,
|
||||
0x13,
|
||||
0x0F,
|
||||
0x14,
|
||||
0x1D,
|
||||
0x1A,
|
||||
0x1F,
|
||||
0x1E,
|
||||
0x1D,
|
||||
0x1A,
|
||||
0x1C,
|
||||
0x1C,
|
||||
0x20,
|
||||
0x24,
|
||||
0x2E,
|
||||
0x27,
|
||||
0x20,
|
||||
0x22,
|
||||
0x2C,
|
||||
0x23,
|
||||
0x1C,
|
||||
0x1C,
|
||||
0x28,
|
||||
0x37,
|
||||
0x29,
|
||||
0x2C,
|
||||
0x30,
|
||||
0x31,
|
||||
0x34,
|
||||
0x34,
|
||||
0x34,
|
||||
0x1F,
|
||||
0x27,
|
||||
0x39,
|
||||
0x3D,
|
||||
0x38,
|
||||
0x32,
|
||||
0x3C,
|
||||
0x2E,
|
||||
0x33,
|
||||
0x34,
|
||||
0x32,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x00,
|
||||
0x0B,
|
||||
0x08,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x11,
|
||||
0x00,
|
||||
0xFF,
|
||||
0xC4,
|
||||
0x00,
|
||||
0x1F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x05,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x02,
|
||||
0x03,
|
||||
0x04,
|
||||
0x05,
|
||||
0x06,
|
||||
0x07,
|
||||
0x08,
|
||||
0x09,
|
||||
0x0A,
|
||||
0x0B,
|
||||
0xFF,
|
||||
0xC4,
|
||||
0x00,
|
||||
0xB5,
|
||||
0x10,
|
||||
0x00,
|
||||
0x02,
|
||||
0x01,
|
||||
0x03,
|
||||
0x03,
|
||||
0x02,
|
||||
0x04,
|
||||
0x03,
|
||||
0x05,
|
||||
0x05,
|
||||
0x04,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x7D,
|
||||
0x01,
|
||||
0x02,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x11,
|
||||
0x05,
|
||||
0x12,
|
||||
0x21,
|
||||
0x31,
|
||||
0x41,
|
||||
0x06,
|
||||
0x13,
|
||||
0x51,
|
||||
0x61,
|
||||
0x07,
|
||||
0x22,
|
||||
0x71,
|
||||
0x14,
|
||||
0x32,
|
||||
0x81,
|
||||
0x91,
|
||||
0xA1,
|
||||
0x08,
|
||||
0x23,
|
||||
0x42,
|
||||
0xB1,
|
||||
0xC1,
|
||||
0x15,
|
||||
0x52,
|
||||
0xD1,
|
||||
0xF0,
|
||||
0x24,
|
||||
0x33,
|
||||
0x62,
|
||||
0x72,
|
||||
0x82,
|
||||
0x09,
|
||||
0x0A,
|
||||
0x16,
|
||||
0x17,
|
||||
0x18,
|
||||
0x19,
|
||||
0x1A,
|
||||
0x25,
|
||||
0x26,
|
||||
0x27,
|
||||
0x28,
|
||||
0x29,
|
||||
0x2A,
|
||||
0x34,
|
||||
0x35,
|
||||
0x36,
|
||||
0x37,
|
||||
0x38,
|
||||
0x39,
|
||||
0x3A,
|
||||
0x43,
|
||||
0x44,
|
||||
0x45,
|
||||
0x46,
|
||||
0x47,
|
||||
0x48,
|
||||
0x49,
|
||||
0x4A,
|
||||
0x53,
|
||||
0x54,
|
||||
0x55,
|
||||
0x56,
|
||||
0x57,
|
||||
0x58,
|
||||
0x59,
|
||||
0x5A,
|
||||
0x63,
|
||||
0x64,
|
||||
0x65,
|
||||
0x66,
|
||||
0x67,
|
||||
0x68,
|
||||
0x69,
|
||||
0x6A,
|
||||
0x73,
|
||||
0x74,
|
||||
0x75,
|
||||
0x76,
|
||||
0x77,
|
||||
0x78,
|
||||
0x79,
|
||||
0x7A,
|
||||
0x83,
|
||||
0x84,
|
||||
0x85,
|
||||
0x86,
|
||||
0x87,
|
||||
0x88,
|
||||
0x89,
|
||||
0x8A,
|
||||
0x92,
|
||||
0x93,
|
||||
0x94,
|
||||
0x95,
|
||||
0x96,
|
||||
0x97,
|
||||
0x98,
|
||||
0x99,
|
||||
0x9A,
|
||||
0xA2,
|
||||
0xA3,
|
||||
0xA4,
|
||||
0xA5,
|
||||
0xA6,
|
||||
0xA7,
|
||||
0xA8,
|
||||
0xA9,
|
||||
0xAA,
|
||||
0xB2,
|
||||
0xB3,
|
||||
0xB4,
|
||||
0xB5,
|
||||
0xB6,
|
||||
0xB7,
|
||||
0xB8,
|
||||
0xB9,
|
||||
0xBA,
|
||||
0xC2,
|
||||
0xC3,
|
||||
0xC4,
|
||||
0xC5,
|
||||
0xC6,
|
||||
0xC7,
|
||||
0xC8,
|
||||
0xC9,
|
||||
0xCA,
|
||||
0xD2,
|
||||
0xD3,
|
||||
0xD4,
|
||||
0xD5,
|
||||
0xD6,
|
||||
0xD7,
|
||||
0xD8,
|
||||
0xD9,
|
||||
0xDA,
|
||||
0xE1,
|
||||
0xE2,
|
||||
0xE3,
|
||||
0xE4,
|
||||
0xE5,
|
||||
0xE6,
|
||||
0xE7,
|
||||
0xE8,
|
||||
0xE9,
|
||||
0xEA,
|
||||
0xF1,
|
||||
0xF2,
|
||||
0xF3,
|
||||
0xF4,
|
||||
0xF5,
|
||||
0xF6,
|
||||
0xF7,
|
||||
0xF8,
|
||||
0xF9,
|
||||
0xFA,
|
||||
0xFF,
|
||||
0xDA,
|
||||
0x00,
|
||||
0x08,
|
||||
0x01,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3F,
|
||||
0x00,
|
||||
0xFB,
|
||||
0xD5,
|
||||
0xDB,
|
||||
0x20,
|
||||
0xA8,
|
||||
0xBA,
|
||||
0xAE,
|
||||
0xAF,
|
||||
0xDA,
|
||||
0xAD,
|
||||
0x28,
|
||||
0xA6,
|
||||
0x02,
|
||||
0x8A,
|
||||
0x28,
|
||||
0x03,
|
||||
0xFF,
|
||||
0xD9,
|
||||
]
|
||||
)
|
||||
|
||||
temp_dir = tempfile.mkdtemp(prefix="thrillwiki_test_")
|
||||
temp_path = Path(temp_dir)
|
||||
@@ -146,6 +464,7 @@ def test_images():
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -222,10 +541,7 @@ def submission_pending(db):
|
||||
User = get_user_model()
|
||||
|
||||
# Get or create test user
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="fsm_test_submitter",
|
||||
defaults={"email": "fsm_test@example.com"}
|
||||
)
|
||||
user, _ = User.objects.get_or_create(username="fsm_test_submitter", defaults={"email": "fsm_test@example.com"})
|
||||
user.set_password("testpass123")
|
||||
user.save()
|
||||
|
||||
@@ -243,7 +559,7 @@ def submission_pending(db):
|
||||
submission_type="EDIT",
|
||||
changes={"description": "FSM test submission"},
|
||||
reason="FSM e2e test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
yield submission
|
||||
@@ -265,8 +581,7 @@ def submission_approved(db):
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="fsm_test_submitter_approved",
|
||||
defaults={"email": "fsm_approved@example.com"}
|
||||
username="fsm_test_submitter_approved", defaults={"email": "fsm_approved@example.com"}
|
||||
)
|
||||
|
||||
park = Park.objects.first()
|
||||
@@ -282,7 +597,7 @@ def submission_approved(db):
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Already approved"},
|
||||
reason="FSM approved test",
|
||||
status="APPROVED"
|
||||
status="APPROVED",
|
||||
)
|
||||
|
||||
yield submission
|
||||
@@ -296,11 +611,7 @@ def park_operating(db):
|
||||
"""Create an operating Park for FSM testing."""
|
||||
from tests.factories import ParkFactory
|
||||
|
||||
park = ParkFactory(
|
||||
name="FSM Test Park Operating",
|
||||
slug="fsm-test-park-operating",
|
||||
status="OPERATING"
|
||||
)
|
||||
park = ParkFactory(name="FSM Test Park Operating", slug="fsm-test-park-operating", status="OPERATING")
|
||||
|
||||
yield park
|
||||
|
||||
@@ -310,11 +621,7 @@ def park_closed_temp(db):
|
||||
"""Create a temporarily closed Park for FSM testing."""
|
||||
from tests.factories import ParkFactory
|
||||
|
||||
park = ParkFactory(
|
||||
name="FSM Test Park Closed Temp",
|
||||
slug="fsm-test-park-closed-temp",
|
||||
status="CLOSED_TEMP"
|
||||
)
|
||||
park = ParkFactory(name="FSM Test Park Closed Temp", slug="fsm-test-park-closed-temp", status="CLOSED_TEMP")
|
||||
|
||||
yield park
|
||||
|
||||
@@ -330,7 +637,7 @@ def park_closed_perm(db):
|
||||
name="FSM Test Park Closed Perm",
|
||||
slug="fsm-test-park-closed-perm",
|
||||
status="CLOSED_PERM",
|
||||
closing_date=date.today() - timedelta(days=365)
|
||||
closing_date=date.today() - timedelta(days=365),
|
||||
)
|
||||
|
||||
yield park
|
||||
@@ -342,10 +649,7 @@ def ride_operating(db, park_operating):
|
||||
from tests.factories import RideFactory
|
||||
|
||||
ride = RideFactory(
|
||||
name="FSM Test Ride Operating",
|
||||
slug="fsm-test-ride-operating",
|
||||
park=park_operating,
|
||||
status="OPERATING"
|
||||
name="FSM Test Ride Operating", slug="fsm-test-ride-operating", park=park_operating, status="OPERATING"
|
||||
)
|
||||
|
||||
yield ride
|
||||
@@ -356,12 +660,7 @@ def ride_sbno(db, park_operating):
|
||||
"""Create an SBNO Ride for FSM testing."""
|
||||
from tests.factories import RideFactory
|
||||
|
||||
ride = RideFactory(
|
||||
name="FSM Test Ride SBNO",
|
||||
slug="fsm-test-ride-sbno",
|
||||
park=park_operating,
|
||||
status="SBNO"
|
||||
)
|
||||
ride = RideFactory(name="FSM Test Ride SBNO", slug="fsm-test-ride-sbno", park=park_operating, status="SBNO")
|
||||
|
||||
yield ride
|
||||
|
||||
@@ -378,7 +677,7 @@ def ride_closed_perm(db, park_operating):
|
||||
slug="fsm-test-ride-closed-perm",
|
||||
park=park_operating,
|
||||
status="CLOSED_PERM",
|
||||
closing_date=date.today() - timedelta(days=365)
|
||||
closing_date=date.today() - timedelta(days=365),
|
||||
)
|
||||
|
||||
yield ride
|
||||
@@ -393,10 +692,7 @@ def queue_item_pending(db):
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="fsm_queue_flagger",
|
||||
defaults={"email": "fsm_queue@example.com"}
|
||||
)
|
||||
user, _ = User.objects.get_or_create(username="fsm_queue_flagger", defaults={"email": "fsm_queue@example.com"})
|
||||
|
||||
queue_item = ModerationQueue.objects.create(
|
||||
item_type="CONTENT_REVIEW",
|
||||
@@ -404,7 +700,7 @@ def queue_item_pending(db):
|
||||
priority="MEDIUM",
|
||||
title="FSM Test Queue Item",
|
||||
description="Queue item for FSM e2e testing",
|
||||
flagged_by=user
|
||||
flagged_by=user,
|
||||
)
|
||||
|
||||
yield queue_item
|
||||
@@ -423,8 +719,7 @@ def bulk_operation_pending(db):
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="fsm_bulk_creator",
|
||||
defaults={"email": "fsm_bulk@example.com", "is_staff": True}
|
||||
username="fsm_bulk_creator", defaults={"email": "fsm_bulk@example.com", "is_staff": True}
|
||||
)
|
||||
|
||||
operation = BulkOperation.objects.create(
|
||||
@@ -434,7 +729,7 @@ def bulk_operation_pending(db):
|
||||
description="FSM Test Bulk Operation",
|
||||
parameters={"test": True},
|
||||
created_by=user,
|
||||
total_items=10
|
||||
total_items=10,
|
||||
)
|
||||
|
||||
yield operation
|
||||
@@ -455,6 +750,7 @@ def live_server(live_server_url):
|
||||
Note: This fixture is provided by pytest-django. The live_server_url
|
||||
fixture provides the URL as a string.
|
||||
"""
|
||||
|
||||
class LiveServer:
|
||||
url = live_server_url
|
||||
|
||||
@@ -469,11 +765,7 @@ def moderator_user(db):
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="moderator",
|
||||
defaults={
|
||||
"email": "moderator@example.com",
|
||||
"is_staff": True
|
||||
}
|
||||
username="moderator", defaults={"email": "moderator@example.com", "is_staff": True}
|
||||
)
|
||||
user.set_password("modpass123")
|
||||
user.save()
|
||||
@@ -488,10 +780,7 @@ def regular_user(db):
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testuser",
|
||||
defaults={"email": "testuser@example.com"}
|
||||
)
|
||||
user, _ = User.objects.get_or_create(username="testuser", defaults={"email": "testuser@example.com"})
|
||||
user.set_password("testpass123")
|
||||
user.save()
|
||||
|
||||
@@ -503,14 +792,7 @@ 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)
|
||||
]
|
||||
parks = [ParkFactory(name=f"E2E Test Park {i}", slug=f"e2e-test-park-{i}", status="OPERATING") for i in range(3)]
|
||||
|
||||
return parks
|
||||
|
||||
@@ -527,7 +809,7 @@ def rides_data(db, parks_data):
|
||||
name=f"E2E Test Ride {park.name} {i}",
|
||||
slug=f"e2e-test-ride-{park.slug}-{i}",
|
||||
park=park,
|
||||
status="OPERATING"
|
||||
status="OPERATING",
|
||||
)
|
||||
rides.append(ride)
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ from playwright.sync_api import Page, expect
|
||||
class TestInvalidTransitionErrors:
|
||||
"""Tests for error handling when attempting invalid transitions."""
|
||||
|
||||
def test_invalid_transition_shows_error_toast(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_invalid_transition_shows_error_toast(self, mod_page: Page, live_server, db):
|
||||
"""Test that attempting an invalid transition shows an error toast."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -42,7 +40,8 @@ class TestInvalidTransitionErrors:
|
||||
|
||||
# Attempt an invalid transition via direct API call
|
||||
# For example, trying to reopen an already operating park
|
||||
response = mod_page.evaluate(f"""
|
||||
response = mod_page.evaluate(
|
||||
f"""
|
||||
async () => {{
|
||||
const response = await fetch('/core/fsm/parks/park/{park.pk}/transition/transition_to_operating/', {{
|
||||
method: 'POST',
|
||||
@@ -58,21 +57,20 @@ class TestInvalidTransitionErrors:
|
||||
hxTrigger: response.headers.get('HX-Trigger')
|
||||
}};
|
||||
}}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Should return error status (400)
|
||||
if response:
|
||||
assert response.get('status') in [400, 403]
|
||||
assert response.get("status") in [400, 403]
|
||||
|
||||
# Check for error toast in HX-Trigger header
|
||||
hx_trigger = response.get('hxTrigger')
|
||||
hx_trigger = response.get("hxTrigger")
|
||||
if hx_trigger:
|
||||
assert 'showToast' in hx_trigger
|
||||
assert 'error' in hx_trigger.lower()
|
||||
assert "showToast" in hx_trigger
|
||||
assert "error" in hx_trigger.lower()
|
||||
|
||||
def test_already_transitioned_shows_error(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_already_transitioned_shows_error(self, mod_page: Page, live_server, db):
|
||||
"""Test that trying to approve an already-approved submission shows error."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -84,8 +82,7 @@ class TestInvalidTransitionErrors:
|
||||
|
||||
# Create an already-approved submission
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testsubmitter2",
|
||||
defaults={"email": "testsubmitter2@example.com"}
|
||||
username="testsubmitter2", defaults={"email": "testsubmitter2@example.com"}
|
||||
)
|
||||
|
||||
park = Park.objects.first()
|
||||
@@ -101,7 +98,7 @@ class TestInvalidTransitionErrors:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Already approved"},
|
||||
reason="Already approved test",
|
||||
status="APPROVED" # Already approved
|
||||
status="APPROVED", # Already approved
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -109,7 +106,8 @@ class TestInvalidTransitionErrors:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Try to approve again via direct API call
|
||||
response = mod_page.evaluate(f"""
|
||||
response = mod_page.evaluate(
|
||||
f"""
|
||||
async () => {{
|
||||
const response = await fetch('/core/fsm/moderation/editsubmission/{submission.pk}/transition/transition_to_approved/', {{
|
||||
method: 'POST',
|
||||
@@ -125,18 +123,17 @@ class TestInvalidTransitionErrors:
|
||||
hxTrigger: response.headers.get('HX-Trigger')
|
||||
}};
|
||||
}}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Should return error status
|
||||
if response:
|
||||
assert response.get('status') in [400, 403]
|
||||
assert response.get("status") in [400, 403]
|
||||
|
||||
finally:
|
||||
submission.delete()
|
||||
|
||||
def test_nonexistent_transition_shows_error(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_nonexistent_transition_shows_error(self, mod_page: Page, live_server, db):
|
||||
"""Test that requesting a non-existent transition shows error."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -148,7 +145,8 @@ class TestInvalidTransitionErrors:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Try to call a non-existent transition
|
||||
response = mod_page.evaluate(f"""
|
||||
response = mod_page.evaluate(
|
||||
f"""
|
||||
async () => {{
|
||||
const response = await fetch('/core/fsm/parks/park/{park.pk}/transition/nonexistent_transition/', {{
|
||||
method: 'POST',
|
||||
@@ -164,19 +162,18 @@ class TestInvalidTransitionErrors:
|
||||
hxTrigger: response.headers.get('HX-Trigger')
|
||||
}};
|
||||
}}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Should return error status (400 or 404)
|
||||
if response:
|
||||
assert response.get('status') in [400, 404]
|
||||
assert response.get("status") in [400, 404]
|
||||
|
||||
|
||||
class TestLoadingIndicators:
|
||||
"""Tests for loading indicator visibility during transitions."""
|
||||
|
||||
def test_loading_indicator_appears_during_transition(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_loading_indicator_appears_during_transition(self, mod_page: Page, live_server, db):
|
||||
"""Verify loading spinner appears during HTMX transition."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -187,19 +184,14 @@ class TestLoadingIndicators:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
|
||||
# Add a route to slow down the request so we can see loading state
|
||||
mod_page.route("**/core/fsm/**", lambda route: (
|
||||
mod_page.wait_for_timeout(500),
|
||||
route.continue_()
|
||||
))
|
||||
mod_page.route("**/core/fsm/**", lambda route: (mod_page.wait_for_timeout(500), route.continue_()))
|
||||
|
||||
# Handle confirmation dialog
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
@@ -212,19 +204,17 @@ class TestLoadingIndicators:
|
||||
|
||||
# The loading indicator should appear (may be brief)
|
||||
# We wait a short time for it to appear
|
||||
try:
|
||||
try: # noqa: SIM105
|
||||
expect(loading_indicator.first).to_be_visible(timeout=1000)
|
||||
except Exception:
|
||||
# Loading indicator may have already disappeared if response was fast
|
||||
pass
|
||||
|
||||
# Wait for transition to complete
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
def test_button_disabled_during_transition(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_button_disabled_during_transition(self, mod_page: Page, live_server, db):
|
||||
"""Test that transition button is disabled during request."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -235,19 +225,14 @@ class TestLoadingIndicators:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
|
||||
# Add a route to slow down the request
|
||||
mod_page.route("**/core/fsm/**", lambda route: (
|
||||
mod_page.wait_for_timeout(1000),
|
||||
route.continue_()
|
||||
))
|
||||
mod_page.route("**/core/fsm/**", lambda route: (mod_page.wait_for_timeout(1000), route.continue_()))
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
|
||||
@@ -261,9 +246,7 @@ class TestLoadingIndicators:
|
||||
class TestNetworkErrorHandling:
|
||||
"""Tests for handling network errors during transitions."""
|
||||
|
||||
def test_network_error_shows_error_toast(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_network_error_shows_error_toast(self, mod_page: Page, live_server, db):
|
||||
"""Test that network errors show appropriate error toast."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -277,10 +260,8 @@ class TestNetworkErrorHandling:
|
||||
# Abort network requests to simulate network error
|
||||
mod_page.route("**/core/fsm/**", lambda route: route.abort("failed"))
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
@@ -295,7 +276,7 @@ class TestNetworkErrorHandling:
|
||||
error_indicator = mod_page.locator('[data-toast].error, .htmx-error, [class*="error"]')
|
||||
|
||||
# May show as toast or inline error
|
||||
try:
|
||||
try: # noqa: SIM105
|
||||
expect(error_indicator.first).to_be_visible(timeout=5000)
|
||||
except Exception:
|
||||
# Error may be handled differently
|
||||
@@ -305,9 +286,7 @@ class TestNetworkErrorHandling:
|
||||
park.refresh_from_db()
|
||||
assert park.status == "OPERATING"
|
||||
|
||||
def test_server_error_shows_user_friendly_message(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_server_error_shows_user_friendly_message(self, mod_page: Page, live_server, db):
|
||||
"""Test that server errors show user-friendly messages."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -319,17 +298,18 @@ class TestNetworkErrorHandling:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Return 500 error to simulate server error
|
||||
mod_page.route("**/core/fsm/**", lambda route: route.fulfill(
|
||||
status=500,
|
||||
headers={"HX-Trigger": '{"showToast": {"message": "An unexpected error occurred", "type": "error"}}'},
|
||||
body=""
|
||||
))
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
mod_page.route(
|
||||
"**/core/fsm/**",
|
||||
lambda route: route.fulfill(
|
||||
status=500,
|
||||
headers={"HX-Trigger": '{"showToast": {"message": "An unexpected error occurred", "type": "error"}}'},
|
||||
body="",
|
||||
),
|
||||
)
|
||||
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
|
||||
@@ -338,7 +318,7 @@ class TestNetworkErrorHandling:
|
||||
close_temp_btn.click()
|
||||
|
||||
# Should show user-friendly error message
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Should not show technical error details to user
|
||||
@@ -349,9 +329,7 @@ class TestNetworkErrorHandling:
|
||||
class TestConfirmationDialogs:
|
||||
"""Tests for confirmation dialogs on dangerous transitions."""
|
||||
|
||||
def test_confirm_dialog_appears_for_reject_transition(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_confirm_dialog_appears_for_reject_transition(self, mod_page: Page, live_server, db):
|
||||
"""Test that confirmation dialog appears for reject transition."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -362,8 +340,7 @@ class TestConfirmationDialogs:
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testsubmitter3",
|
||||
defaults={"email": "testsubmitter3@example.com"}
|
||||
username="testsubmitter3", defaults={"email": "testsubmitter3@example.com"}
|
||||
)
|
||||
|
||||
park = Park.objects.first()
|
||||
@@ -379,7 +356,7 @@ class TestConfirmationDialogs:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Confirm dialog test"},
|
||||
reason="Confirm dialog test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
dialog_shown = {"shown": False}
|
||||
@@ -395,9 +372,7 @@ class TestConfirmationDialogs:
|
||||
|
||||
mod_page.on("dialog", handle_dialog)
|
||||
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-submission-id="{submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')
|
||||
|
||||
if submission_row.is_visible():
|
||||
reject_btn = submission_row.get_by_role("button", name="Reject")
|
||||
@@ -413,9 +388,7 @@ class TestConfirmationDialogs:
|
||||
finally:
|
||||
submission.delete()
|
||||
|
||||
def test_cancel_confirm_dialog_prevents_transition(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_cancel_confirm_dialog_prevents_transition(self, mod_page: Page, live_server, db):
|
||||
"""Test that canceling the confirmation dialog prevents the transition."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -426,8 +399,7 @@ class TestConfirmationDialogs:
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testsubmitter4",
|
||||
defaults={"email": "testsubmitter4@example.com"}
|
||||
username="testsubmitter4", defaults={"email": "testsubmitter4@example.com"}
|
||||
)
|
||||
|
||||
park = Park.objects.first()
|
||||
@@ -443,7 +415,7 @@ class TestConfirmationDialogs:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Cancel confirm test"},
|
||||
reason="Cancel confirm test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -453,9 +425,7 @@ class TestConfirmationDialogs:
|
||||
# Dismiss (cancel) the dialog
|
||||
mod_page.on("dialog", lambda dialog: dialog.dismiss())
|
||||
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-submission-id="{submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')
|
||||
|
||||
if submission_row.is_visible():
|
||||
reject_btn = submission_row.get_by_role("button", name="Reject")
|
||||
@@ -472,9 +442,7 @@ class TestConfirmationDialogs:
|
||||
finally:
|
||||
submission.delete()
|
||||
|
||||
def test_accept_confirm_dialog_executes_transition(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_accept_confirm_dialog_executes_transition(self, mod_page: Page, live_server, db):
|
||||
"""Test that accepting the confirmation dialog executes the transition."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -485,8 +453,7 @@ class TestConfirmationDialogs:
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testsubmitter5",
|
||||
defaults={"email": "testsubmitter5@example.com"}
|
||||
username="testsubmitter5", defaults={"email": "testsubmitter5@example.com"}
|
||||
)
|
||||
|
||||
park = Park.objects.first()
|
||||
@@ -502,7 +469,7 @@ class TestConfirmationDialogs:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Accept confirm test"},
|
||||
reason="Accept confirm test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -512,9 +479,7 @@ class TestConfirmationDialogs:
|
||||
# Accept the dialog
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-submission-id="{submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')
|
||||
|
||||
if submission_row.is_visible():
|
||||
reject_btn = submission_row.get_by_role("button", name="Reject")
|
||||
@@ -522,7 +487,7 @@ class TestConfirmationDialogs:
|
||||
reject_btn.click()
|
||||
|
||||
# Wait for transition to complete
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify submission status WAS changed
|
||||
@@ -536,9 +501,7 @@ class TestConfirmationDialogs:
|
||||
class TestValidationErrors:
|
||||
"""Tests for validation error handling."""
|
||||
|
||||
def test_validation_error_shows_specific_message(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_validation_error_shows_specific_message(self, mod_page: Page, live_server, db):
|
||||
"""Test that validation errors show specific error messages."""
|
||||
# This test depends on having transitions that require additional data
|
||||
# For example, a transition that requires a reason field
|
||||
@@ -563,9 +526,7 @@ class TestValidationErrors:
|
||||
class TestToastNotificationBehavior:
|
||||
"""Tests for toast notification appearance and behavior."""
|
||||
|
||||
def test_success_toast_auto_dismisses(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_success_toast_auto_dismisses(self, mod_page: Page, live_server, db):
|
||||
"""Test that success toast auto-dismisses after timeout."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -576,10 +537,8 @@ class TestToastNotificationBehavior:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
@@ -589,16 +548,14 @@ class TestToastNotificationBehavior:
|
||||
close_temp_btn.click()
|
||||
|
||||
# Toast should appear
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Toast should auto-dismiss after timeout (typically 3-5 seconds)
|
||||
# Wait for auto-dismiss
|
||||
expect(toast).not_to_be_visible(timeout=10000)
|
||||
|
||||
def test_error_toast_has_correct_styling(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_error_toast_has_correct_styling(self, mod_page: Page, live_server, db):
|
||||
"""Test that error toast has correct red/danger styling."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -610,19 +567,18 @@ class TestToastNotificationBehavior:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Simulate an error response
|
||||
mod_page.route("**/core/fsm/**", lambda route: route.fulfill(
|
||||
status=400,
|
||||
headers={
|
||||
"HX-Trigger": '{"showToast": {"message": "Test error message", "type": "error"}}'
|
||||
},
|
||||
body=""
|
||||
))
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
mod_page.route(
|
||||
"**/core/fsm/**",
|
||||
lambda route: route.fulfill(
|
||||
status=400,
|
||||
headers={"HX-Trigger": '{"showToast": {"message": "Test error message", "type": "error"}}'},
|
||||
body="",
|
||||
),
|
||||
)
|
||||
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
|
||||
@@ -631,15 +587,13 @@ class TestToastNotificationBehavior:
|
||||
close_temp_btn.click()
|
||||
|
||||
# Error toast should appear with error styling
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Should have error/danger styling (red)
|
||||
expect(toast).to_have_class(re.compile(r"error|danger|bg-red|text-red"))
|
||||
|
||||
def test_success_toast_has_correct_styling(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_success_toast_has_correct_styling(self, mod_page: Page, live_server, db):
|
||||
"""Test that success toast has correct green/success styling."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -654,10 +608,8 @@ class TestToastNotificationBehavior:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
pytest.skip("Close Temporarily button not visible")
|
||||
@@ -667,7 +619,7 @@ class TestToastNotificationBehavior:
|
||||
close_temp_btn.click()
|
||||
|
||||
# Success toast should appear with success styling
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Should have success styling (green)
|
||||
|
||||
@@ -22,9 +22,7 @@ from playwright.sync_api import Page, expect
|
||||
class TestUnauthenticatedUserPermissions:
|
||||
"""Tests for unauthenticated user permission guards."""
|
||||
|
||||
def test_unauthenticated_user_cannot_see_moderation_dashboard(
|
||||
self, page: Page, live_server
|
||||
):
|
||||
def test_unauthenticated_user_cannot_see_moderation_dashboard(self, page: Page, live_server):
|
||||
"""Test that unauthenticated users are redirected from moderation dashboard."""
|
||||
# Navigate to moderation dashboard without logging in
|
||||
response = page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
@@ -34,9 +32,7 @@ class TestUnauthenticatedUserPermissions:
|
||||
current_url = page.url
|
||||
assert "login" in current_url or "denied" in current_url or response.status == 403
|
||||
|
||||
def test_unauthenticated_user_cannot_see_transition_buttons(
|
||||
self, page: Page, live_server, db
|
||||
):
|
||||
def test_unauthenticated_user_cannot_see_transition_buttons(self, page: Page, live_server, db):
|
||||
"""Test that unauthenticated users cannot see transition buttons on park detail."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -48,18 +44,14 @@ class TestUnauthenticatedUserPermissions:
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Status action buttons should NOT be visible
|
||||
status_actions = page.locator('[data-park-status-actions]')
|
||||
status_actions = page.locator("[data-park-status-actions]")
|
||||
|
||||
# Either the section doesn't exist or the buttons are not there
|
||||
if status_actions.is_visible():
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
expect(close_temp_btn).not_to_be_visible()
|
||||
|
||||
def test_unauthenticated_direct_post_returns_403(
|
||||
self, page: Page, live_server, db
|
||||
):
|
||||
def test_unauthenticated_direct_post_returns_403(self, page: Page, live_server, db):
|
||||
"""Test that direct POST to FSM endpoint returns 403 for unauthenticated user."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -70,7 +62,7 @@ class TestUnauthenticatedUserPermissions:
|
||||
# Attempt to POST directly to FSM transition endpoint
|
||||
response = page.request.post(
|
||||
f"{live_server.url}/core/fsm/parks/park/{park.pk}/transition/transition_to_closed_temp/",
|
||||
headers={"HX-Request": "true"}
|
||||
headers={"HX-Request": "true"},
|
||||
)
|
||||
|
||||
# Should get 403 Forbidden
|
||||
@@ -84,9 +76,7 @@ class TestUnauthenticatedUserPermissions:
|
||||
class TestRegularUserPermissions:
|
||||
"""Tests for regular (non-moderator) user permission guards."""
|
||||
|
||||
def test_regular_user_cannot_approve_submission(
|
||||
self, auth_page: Page, live_server, db
|
||||
):
|
||||
def test_regular_user_cannot_approve_submission(self, auth_page: Page, live_server, db):
|
||||
"""Test that regular users cannot approve submissions."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -114,7 +104,7 @@ class TestRegularUserPermissions:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Test change"},
|
||||
reason="Permission test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -126,9 +116,7 @@ class TestRegularUserPermissions:
|
||||
|
||||
# If somehow on dashboard, verify no approve button
|
||||
if "dashboard" in current_url:
|
||||
submission_row = auth_page.locator(
|
||||
f'[data-submission-id="{submission.pk}"]'
|
||||
)
|
||||
submission_row = auth_page.locator(f'[data-submission-id="{submission.pk}"]')
|
||||
if submission_row.is_visible():
|
||||
approve_btn = submission_row.get_by_role("button", name="Approve")
|
||||
expect(approve_btn).not_to_be_visible()
|
||||
@@ -136,7 +124,7 @@ class TestRegularUserPermissions:
|
||||
# Try direct POST - should be denied
|
||||
response = auth_page.request.post(
|
||||
f"{live_server.url}/core/fsm/moderation/editsubmission/{submission.pk}/transition/transition_to_approved/",
|
||||
headers={"HX-Request": "true"}
|
||||
headers={"HX-Request": "true"},
|
||||
)
|
||||
|
||||
# Should be denied (403 or 302 redirect)
|
||||
@@ -149,9 +137,7 @@ class TestRegularUserPermissions:
|
||||
finally:
|
||||
submission.delete()
|
||||
|
||||
def test_regular_user_cannot_change_park_status(
|
||||
self, auth_page: Page, live_server, db
|
||||
):
|
||||
def test_regular_user_cannot_change_park_status(self, auth_page: Page, live_server, db):
|
||||
"""Test that regular users cannot change park status."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -163,18 +149,16 @@ class TestRegularUserPermissions:
|
||||
auth_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Status action buttons should NOT be visible to regular user
|
||||
status_actions = auth_page.locator('[data-park-status-actions]')
|
||||
status_actions = auth_page.locator("[data-park-status-actions]")
|
||||
|
||||
if status_actions.is_visible():
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
expect(close_temp_btn).not_to_be_visible()
|
||||
|
||||
# Try direct POST - should be denied
|
||||
response = auth_page.request.post(
|
||||
f"{live_server.url}/core/fsm/parks/park/{park.pk}/transition/transition_to_closed_temp/",
|
||||
headers={"HX-Request": "true"}
|
||||
headers={"HX-Request": "true"},
|
||||
)
|
||||
|
||||
# Should be denied
|
||||
@@ -184,9 +168,7 @@ class TestRegularUserPermissions:
|
||||
park.refresh_from_db()
|
||||
assert park.status == "OPERATING"
|
||||
|
||||
def test_regular_user_cannot_change_ride_status(
|
||||
self, auth_page: Page, live_server, db
|
||||
):
|
||||
def test_regular_user_cannot_change_ride_status(self, auth_page: Page, live_server, db):
|
||||
"""Test that regular users cannot change ride status."""
|
||||
from apps.rides.models import Ride
|
||||
|
||||
@@ -194,24 +176,20 @@ class TestRegularUserPermissions:
|
||||
if not ride:
|
||||
pytest.skip("No operating ride available")
|
||||
|
||||
auth_page.goto(
|
||||
f"{live_server.url}/parks/{ride.park.slug}/rides/{ride.slug}/"
|
||||
)
|
||||
auth_page.goto(f"{live_server.url}/parks/{ride.park.slug}/rides/{ride.slug}/")
|
||||
auth_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Status action buttons should NOT be visible to regular user
|
||||
status_actions = auth_page.locator('[data-ride-status-actions]')
|
||||
status_actions = auth_page.locator("[data-ride-status-actions]")
|
||||
|
||||
if status_actions.is_visible():
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
expect(close_temp_btn).not_to_be_visible()
|
||||
|
||||
# Try direct POST - should be denied
|
||||
response = auth_page.request.post(
|
||||
f"{live_server.url}/core/fsm/rides/ride/{ride.pk}/transition/transition_to_closed_temp/",
|
||||
headers={"HX-Request": "true"}
|
||||
headers={"HX-Request": "true"},
|
||||
)
|
||||
|
||||
# Should be denied
|
||||
@@ -225,9 +203,7 @@ class TestRegularUserPermissions:
|
||||
class TestModeratorPermissions:
|
||||
"""Tests for moderator-specific permission guards."""
|
||||
|
||||
def test_moderator_can_approve_submission(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_moderator_can_approve_submission(self, mod_page: Page, live_server, db):
|
||||
"""Test that moderators CAN see and use approve button."""
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -240,11 +216,7 @@ class TestModeratorPermissions:
|
||||
# Create a pending submission
|
||||
user = User.objects.filter(username="testuser").first()
|
||||
if not user:
|
||||
user = User.objects.create_user(
|
||||
username="testuser",
|
||||
email="testuser@example.com",
|
||||
password="testpass123"
|
||||
)
|
||||
user = User.objects.create_user(username="testuser", email="testuser@example.com", password="testpass123")
|
||||
|
||||
park = Park.objects.first()
|
||||
if not park:
|
||||
@@ -259,7 +231,7 @@ class TestModeratorPermissions:
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Test change for moderator"},
|
||||
reason="Moderator permission test",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -267,9 +239,7 @@ class TestModeratorPermissions:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Moderator should be able to see the submission
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-submission-id="{submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{submission.pk}"]')
|
||||
|
||||
if submission_row.is_visible():
|
||||
# Should see approve button
|
||||
@@ -279,9 +249,7 @@ class TestModeratorPermissions:
|
||||
finally:
|
||||
submission.delete()
|
||||
|
||||
def test_moderator_can_change_park_status(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_moderator_can_change_park_status(self, mod_page: Page, live_server, db):
|
||||
"""Test that moderators CAN see and use park status change buttons."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -293,18 +261,14 @@ class TestModeratorPermissions:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Status action buttons SHOULD be visible to moderator
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
if status_actions.is_visible():
|
||||
# Should see close temporarily button
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
expect(close_temp_btn).to_be_visible()
|
||||
|
||||
def test_moderator_cannot_access_admin_only_transitions(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_moderator_cannot_access_admin_only_transitions(self, mod_page: Page, live_server, db):
|
||||
"""Test that moderators CANNOT access admin-only transitions."""
|
||||
# This test verifies that certain transitions require admin privileges
|
||||
# Specific transitions depend on the FSM configuration
|
||||
@@ -327,22 +291,18 @@ class TestModeratorPermissions:
|
||||
|
||||
# Check for admin-only buttons (if any are configured)
|
||||
# The specific buttons that should be hidden depend on the FSM configuration
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
# If there are admin-only transitions, verify they're hidden
|
||||
# This is a placeholder - actual admin-only transitions depend on configuration
|
||||
admin_only_btn = status_actions.get_by_role(
|
||||
"button", name="Force Delete" # Example admin-only action
|
||||
)
|
||||
admin_only_btn = status_actions.get_by_role("button", name="Force Delete") # Example admin-only action
|
||||
expect(admin_only_btn).not_to_be_visible()
|
||||
|
||||
|
||||
class TestPermissionDeniedErrorHandling:
|
||||
"""Tests for error handling when permission is denied."""
|
||||
|
||||
def test_permission_denied_shows_error_toast(
|
||||
self, auth_page: Page, live_server, db
|
||||
):
|
||||
def test_permission_denied_shows_error_toast(self, auth_page: Page, live_server, db):
|
||||
"""Test that permission denied errors show appropriate toast."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -355,9 +315,12 @@ class TestPermissionDeniedErrorHandling:
|
||||
auth_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Make the request programmatically with HTMX header
|
||||
response = auth_page.evaluate("""
|
||||
response = auth_page.evaluate(
|
||||
"""
|
||||
async () => {
|
||||
const response = await fetch('/core/fsm/parks/park/""" + str(park.pk) + """/transition/transition_to_closed_temp/', {
|
||||
const response = await fetch('/core/fsm/parks/park/"""
|
||||
+ str(park.pk)
|
||||
+ """/transition/transition_to_closed_temp/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'HX-Request': 'true',
|
||||
@@ -370,18 +333,17 @@ class TestPermissionDeniedErrorHandling:
|
||||
hxTrigger: response.headers.get('HX-Trigger')
|
||||
};
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Check if error toast was triggered
|
||||
if response and response.get('status') in [400, 403]:
|
||||
hx_trigger = response.get('hxTrigger')
|
||||
if response and response.get("status") in [400, 403]:
|
||||
hx_trigger = response.get("hxTrigger")
|
||||
if hx_trigger:
|
||||
assert 'showToast' in hx_trigger
|
||||
assert 'error' in hx_trigger.lower() or 'denied' in hx_trigger.lower()
|
||||
assert "showToast" in hx_trigger
|
||||
assert "error" in hx_trigger.lower() or "denied" in hx_trigger.lower()
|
||||
|
||||
def test_database_state_unchanged_on_permission_denied(
|
||||
self, auth_page: Page, live_server, db
|
||||
):
|
||||
def test_database_state_unchanged_on_permission_denied(self, auth_page: Page, live_server, db):
|
||||
"""Test that database state is unchanged when permission is denied."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -395,9 +357,12 @@ class TestPermissionDeniedErrorHandling:
|
||||
auth_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
auth_page.wait_for_load_state("networkidle")
|
||||
|
||||
auth_page.evaluate("""
|
||||
auth_page.evaluate(
|
||||
"""
|
||||
async () => {
|
||||
await fetch('/core/fsm/parks/park/""" + str(park.pk) + """/transition/transition_to_closed_temp/', {
|
||||
await fetch('/core/fsm/parks/park/"""
|
||||
+ str(park.pk)
|
||||
+ """/transition/transition_to_closed_temp/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'HX-Request': 'true',
|
||||
@@ -406,7 +371,8 @@ class TestPermissionDeniedErrorHandling:
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Verify database state did NOT change
|
||||
park.refresh_from_db()
|
||||
@@ -416,9 +382,7 @@ class TestPermissionDeniedErrorHandling:
|
||||
class TestTransitionButtonVisibility:
|
||||
"""Tests for correct transition button visibility based on permissions and state."""
|
||||
|
||||
def test_transition_button_hidden_when_state_invalid(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_transition_button_hidden_when_state_invalid(self, mod_page: Page, live_server, db):
|
||||
"""Test that transition buttons are hidden when the current state is invalid."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -430,7 +394,7 @@ class TestTransitionButtonVisibility:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
# Reopen button should NOT be visible for operating park
|
||||
# (can't reopen something that's already operating)
|
||||
@@ -442,9 +406,7 @@ class TestTransitionButtonVisibility:
|
||||
demolish_btn = status_actions.get_by_role("button", name="Mark as Demolished")
|
||||
expect(demolish_btn).not_to_be_visible()
|
||||
|
||||
def test_correct_buttons_shown_for_closed_temp_state(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_correct_buttons_shown_for_closed_temp_state(self, mod_page: Page, live_server, db):
|
||||
"""Test that correct buttons are shown for temporarily closed state."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -461,21 +423,17 @@ class TestTransitionButtonVisibility:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
# Reopen button SHOULD be visible
|
||||
reopen_btn = status_actions.get_by_role("button", name="Reopen")
|
||||
expect(reopen_btn).to_be_visible()
|
||||
|
||||
# Close Temporarily should NOT be visible (already closed)
|
||||
close_temp_btn = status_actions.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_actions.get_by_role("button", name="Close Temporarily")
|
||||
expect(close_temp_btn).not_to_be_visible()
|
||||
|
||||
def test_correct_buttons_shown_for_closed_perm_state(
|
||||
self, mod_page: Page, live_server, db
|
||||
):
|
||||
def test_correct_buttons_shown_for_closed_perm_state(self, mod_page: Page, live_server, db):
|
||||
"""Test that correct buttons are shown for permanently closed state."""
|
||||
from apps.parks.models import Park
|
||||
|
||||
@@ -492,7 +450,7 @@ class TestTransitionButtonVisibility:
|
||||
mod_page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_actions = mod_page.locator('[data-park-status-actions]')
|
||||
status_actions = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
# Demolish/Relocate buttons SHOULD be visible
|
||||
demolish_btn = status_actions.get_by_role("button", name="Mark as Demolished")
|
||||
|
||||
@@ -30,10 +30,7 @@ def pending_submission(db):
|
||||
User = get_user_model()
|
||||
|
||||
# Get or create test user
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testsubmitter",
|
||||
defaults={"email": "testsubmitter@example.com"}
|
||||
)
|
||||
user, _ = User.objects.get_or_create(username="testsubmitter", defaults={"email": "testsubmitter@example.com"})
|
||||
user.set_password("testpass123")
|
||||
user.save()
|
||||
|
||||
@@ -51,7 +48,7 @@ def pending_submission(db):
|
||||
submission_type="EDIT",
|
||||
changes={"description": "Updated park description for testing"},
|
||||
reason="E2E test submission",
|
||||
status="PENDING"
|
||||
status="PENDING",
|
||||
)
|
||||
|
||||
yield submission
|
||||
@@ -73,8 +70,7 @@ def pending_photo_submission(db):
|
||||
|
||||
# Get or create test user
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testphotosubmitter",
|
||||
defaults={"email": "testphotosubmitter@example.com"}
|
||||
username="testphotosubmitter", defaults={"email": "testphotosubmitter@example.com"}
|
||||
)
|
||||
user.set_password("testpass123")
|
||||
user.save()
|
||||
@@ -89,6 +85,7 @@ def pending_photo_submission(db):
|
||||
# Check if CloudflareImage model exists and has entries
|
||||
try:
|
||||
from django_cloudflareimages_toolkit.models import CloudflareImage
|
||||
|
||||
photo = CloudflareImage.objects.first()
|
||||
if not photo:
|
||||
pytest.skip("No CloudflareImage available for testing")
|
||||
@@ -96,12 +93,7 @@ def pending_photo_submission(db):
|
||||
pytest.skip("CloudflareImage not available")
|
||||
|
||||
submission = PhotoSubmission.objects.create(
|
||||
user=user,
|
||||
content_type=content_type,
|
||||
object_id=park.pk,
|
||||
photo=photo,
|
||||
caption="E2E test photo",
|
||||
status="PENDING"
|
||||
user=user, content_type=content_type, object_id=park.pk, photo=photo, caption="E2E test photo", status="PENDING"
|
||||
)
|
||||
|
||||
yield submission
|
||||
@@ -113,9 +105,7 @@ def pending_photo_submission(db):
|
||||
class TestEditSubmissionTransitions:
|
||||
"""Tests for EditSubmission FSM transitions via HTMX."""
|
||||
|
||||
def test_submission_approve_transition_as_moderator(
|
||||
self, mod_page: Page, pending_submission, live_server
|
||||
):
|
||||
def test_submission_approve_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
|
||||
"""Test approving an EditSubmission as a moderator."""
|
||||
# Navigate to moderation dashboard
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
@@ -127,7 +117,7 @@ class TestEditSubmissionTransitions:
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
|
||||
|
||||
# Verify initial status is pending
|
||||
status_badge = submission_row.locator('[data-status-badge]')
|
||||
status_badge = submission_row.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Pending")
|
||||
|
||||
# Click the approve button
|
||||
@@ -140,7 +130,7 @@ class TestEditSubmissionTransitions:
|
||||
approve_btn.click()
|
||||
|
||||
# Wait for toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("approved")
|
||||
|
||||
@@ -151,9 +141,7 @@ class TestEditSubmissionTransitions:
|
||||
pending_submission.refresh_from_db()
|
||||
assert pending_submission.status == "APPROVED"
|
||||
|
||||
def test_submission_reject_transition_as_moderator(
|
||||
self, mod_page: Page, pending_submission, live_server
|
||||
):
|
||||
def test_submission_reject_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
|
||||
"""Test rejecting an EditSubmission as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
@@ -161,7 +149,7 @@ class TestEditSubmissionTransitions:
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
|
||||
|
||||
# Verify initial status
|
||||
status_badge = submission_row.locator('[data-status-badge]')
|
||||
status_badge = submission_row.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Pending")
|
||||
|
||||
# Click reject button
|
||||
@@ -173,7 +161,7 @@ class TestEditSubmissionTransitions:
|
||||
reject_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("rejected")
|
||||
|
||||
@@ -184,9 +172,7 @@ class TestEditSubmissionTransitions:
|
||||
pending_submission.refresh_from_db()
|
||||
assert pending_submission.status == "REJECTED"
|
||||
|
||||
def test_submission_escalate_transition_as_moderator(
|
||||
self, mod_page: Page, pending_submission, live_server
|
||||
):
|
||||
def test_submission_escalate_transition_as_moderator(self, mod_page: Page, pending_submission, live_server):
|
||||
"""Test escalating an EditSubmission as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
@@ -194,7 +180,7 @@ class TestEditSubmissionTransitions:
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
|
||||
|
||||
# Verify initial status
|
||||
status_badge = submission_row.locator('[data-status-badge]')
|
||||
status_badge = submission_row.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Pending")
|
||||
|
||||
# Click escalate button
|
||||
@@ -206,7 +192,7 @@ class TestEditSubmissionTransitions:
|
||||
escalate_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("escalated")
|
||||
|
||||
@@ -221,9 +207,7 @@ class TestEditSubmissionTransitions:
|
||||
class TestPhotoSubmissionTransitions:
|
||||
"""Tests for PhotoSubmission FSM transitions via HTMX."""
|
||||
|
||||
def test_photo_submission_approve_transition(
|
||||
self, mod_page: Page, pending_photo_submission, live_server
|
||||
):
|
||||
def test_photo_submission_approve_transition(self, mod_page: Page, pending_photo_submission, live_server):
|
||||
"""Test approving a PhotoSubmission as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
@@ -234,9 +218,7 @@ class TestPhotoSubmissionTransitions:
|
||||
photos_tab.click()
|
||||
|
||||
# Find the photo submission row
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-photo-submission-id="{pending_photo_submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-photo-submission-id="{pending_photo_submission.pk}"]')
|
||||
|
||||
if not submission_row.is_visible():
|
||||
pytest.skip("Photo submission not visible in dashboard")
|
||||
@@ -248,7 +230,7 @@ class TestPhotoSubmissionTransitions:
|
||||
approve_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("approved")
|
||||
|
||||
@@ -256,9 +238,7 @@ class TestPhotoSubmissionTransitions:
|
||||
pending_photo_submission.refresh_from_db()
|
||||
assert pending_photo_submission.status == "APPROVED"
|
||||
|
||||
def test_photo_submission_reject_transition(
|
||||
self, mod_page: Page, pending_photo_submission, live_server
|
||||
):
|
||||
def test_photo_submission_reject_transition(self, mod_page: Page, pending_photo_submission, live_server):
|
||||
"""Test rejecting a PhotoSubmission as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
@@ -269,9 +249,7 @@ class TestPhotoSubmissionTransitions:
|
||||
photos_tab.click()
|
||||
|
||||
# Find the photo submission row
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-photo-submission-id="{pending_photo_submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-photo-submission-id="{pending_photo_submission.pk}"]')
|
||||
|
||||
if not submission_row.is_visible():
|
||||
pytest.skip("Photo submission not visible in dashboard")
|
||||
@@ -283,7 +261,7 @@ class TestPhotoSubmissionTransitions:
|
||||
reject_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("rejected")
|
||||
|
||||
@@ -304,10 +282,7 @@ class TestModerationQueueTransitions:
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testflagger",
|
||||
defaults={"email": "testflagger@example.com"}
|
||||
)
|
||||
user, _ = User.objects.get_or_create(username="testflagger", defaults={"email": "testflagger@example.com"})
|
||||
|
||||
queue_item = ModerationQueue.objects.create(
|
||||
item_type="CONTENT_REVIEW",
|
||||
@@ -315,16 +290,14 @@ class TestModerationQueueTransitions:
|
||||
priority="MEDIUM",
|
||||
title="E2E Test Queue Item",
|
||||
description="Queue item for E2E testing",
|
||||
flagged_by=user
|
||||
flagged_by=user,
|
||||
)
|
||||
|
||||
yield queue_item
|
||||
|
||||
queue_item.delete()
|
||||
|
||||
def test_moderation_queue_start_transition(
|
||||
self, mod_page: Page, pending_queue_item, live_server
|
||||
):
|
||||
def test_moderation_queue_start_transition(self, mod_page: Page, pending_queue_item, live_server):
|
||||
"""Test starting work on a ModerationQueue item."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/queue/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
@@ -340,16 +313,14 @@ class TestModerationQueueTransitions:
|
||||
start_btn.click()
|
||||
|
||||
# Verify status updated to IN_PROGRESS
|
||||
status_badge = queue_row.locator('[data-status-badge]')
|
||||
status_badge = queue_row.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("In Progress", timeout=5000)
|
||||
|
||||
# Verify database state
|
||||
pending_queue_item.refresh_from_db()
|
||||
assert pending_queue_item.status == "IN_PROGRESS"
|
||||
|
||||
def test_moderation_queue_complete_transition(
|
||||
self, mod_page: Page, pending_queue_item, live_server
|
||||
):
|
||||
def test_moderation_queue_complete_transition(self, mod_page: Page, pending_queue_item, live_server):
|
||||
"""Test completing a ModerationQueue item."""
|
||||
# First set status to IN_PROGRESS
|
||||
pending_queue_item.status = "IN_PROGRESS"
|
||||
@@ -370,7 +341,7 @@ class TestModerationQueueTransitions:
|
||||
complete_btn.click()
|
||||
|
||||
# Verify toast and status
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
pending_queue_item.refresh_from_db()
|
||||
@@ -390,8 +361,7 @@ class TestBulkOperationTransitions:
|
||||
User = get_user_model()
|
||||
|
||||
user, _ = User.objects.get_or_create(
|
||||
username="testadmin",
|
||||
defaults={"email": "testadmin@example.com", "is_staff": True}
|
||||
username="testadmin", defaults={"email": "testadmin@example.com", "is_staff": True}
|
||||
)
|
||||
|
||||
operation = BulkOperation.objects.create(
|
||||
@@ -401,24 +371,20 @@ class TestBulkOperationTransitions:
|
||||
description="E2E Test Bulk Operation",
|
||||
parameters={"test": True},
|
||||
created_by=user,
|
||||
total_items=10
|
||||
total_items=10,
|
||||
)
|
||||
|
||||
yield operation
|
||||
|
||||
operation.delete()
|
||||
|
||||
def test_bulk_operation_cancel_transition(
|
||||
self, mod_page: Page, pending_bulk_operation, live_server
|
||||
):
|
||||
def test_bulk_operation_cancel_transition(self, mod_page: Page, pending_bulk_operation, live_server):
|
||||
"""Test canceling a BulkOperation."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/bulk-operations/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Find the operation row
|
||||
operation_row = mod_page.locator(
|
||||
f'[data-bulk-operation-id="{pending_bulk_operation.pk}"]'
|
||||
)
|
||||
operation_row = mod_page.locator(f'[data-bulk-operation-id="{pending_bulk_operation.pk}"]')
|
||||
|
||||
if not operation_row.is_visible():
|
||||
pytest.skip("Bulk operation not visible")
|
||||
@@ -430,7 +396,7 @@ class TestBulkOperationTransitions:
|
||||
cancel_btn.click()
|
||||
|
||||
# Verify toast
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
expect(toast).to_contain_text("cancel")
|
||||
|
||||
@@ -442,16 +408,12 @@ class TestBulkOperationTransitions:
|
||||
class TestTransitionLoadingStates:
|
||||
"""Tests for loading indicators during FSM transitions."""
|
||||
|
||||
def test_loading_indicator_appears_during_transition(
|
||||
self, mod_page: Page, pending_submission, live_server
|
||||
):
|
||||
def test_loading_indicator_appears_during_transition(self, mod_page: Page, pending_submission, live_server):
|
||||
"""Verify loading spinner appears during HTMX transition."""
|
||||
mod_page.goto(f"{live_server.url}/moderation/dashboard/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
submission_row = mod_page.locator(
|
||||
f'[data-submission-id="{pending_submission.pk}"]'
|
||||
)
|
||||
submission_row = mod_page.locator(f'[data-submission-id="{pending_submission.pk}"]')
|
||||
|
||||
# Get approve button and associated loading indicator
|
||||
approve_btn = submission_row.get_by_role("button", name="Approve")
|
||||
@@ -466,8 +428,8 @@ class TestTransitionLoadingStates:
|
||||
|
||||
# Check for htmx-indicator visibility (may be brief)
|
||||
# The indicator should become visible during the request
|
||||
submission_row.locator('.htmx-indicator')
|
||||
submission_row.locator(".htmx-indicator")
|
||||
|
||||
# Wait for transition to complete
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
@@ -32,9 +32,7 @@ class TestParkListPage:
|
||||
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
|
||||
):
|
||||
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/")
|
||||
|
||||
@@ -51,9 +49,7 @@ class TestParkListPage:
|
||||
page.goto(f"{live_server.url}/parks/")
|
||||
|
||||
# Find search input
|
||||
search_input = page.locator(
|
||||
"input[type='search'], input[name='q'], input[placeholder*='search' i]"
|
||||
)
|
||||
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")
|
||||
@@ -83,9 +79,7 @@ class TestParkDetailPage:
|
||||
page.goto(f"{live_server.url}/parks/{park.slug}/")
|
||||
|
||||
# Look for rides section/tab
|
||||
page.locator(
|
||||
"[data-testid='rides-section'], #rides, [role='tabpanel']"
|
||||
)
|
||||
page.locator("[data-testid='rides-section'], #rides, [role='tabpanel']")
|
||||
|
||||
# Or a rides tab
|
||||
rides_tab = page.get_by_role("tab", name="Rides")
|
||||
@@ -103,9 +97,7 @@ class TestParkDetailPage:
|
||||
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"
|
||||
)
|
||||
status_indicator = page.locator(".status-badge, [data-testid='status'], .park-status")
|
||||
expect(status_indicator.first).to_be_visible()
|
||||
|
||||
|
||||
@@ -118,9 +110,7 @@ class TestParkFiltering:
|
||||
page.goto(f"{live_server.url}/parks/")
|
||||
|
||||
# Find status filter
|
||||
status_filter = page.locator(
|
||||
"select[name='status'], [data-testid='status-filter']"
|
||||
)
|
||||
status_filter = page.locator("select[name='status'], [data-testid='status-filter']")
|
||||
|
||||
if status_filter.count() > 0:
|
||||
status_filter.first.select_option("OPERATING")
|
||||
@@ -135,9 +125,7 @@ class TestParkFiltering:
|
||||
page.goto(f"{live_server.url}/parks/")
|
||||
|
||||
# Find clear filters button
|
||||
clear_btn = page.locator(
|
||||
"[data-testid='clear-filters'], button:has-text('Clear')"
|
||||
)
|
||||
clear_btn = page.locator("[data-testid='clear-filters'], button:has-text('Clear')")
|
||||
|
||||
if clear_btn.count() > 0:
|
||||
clear_btn.first.click()
|
||||
@@ -164,9 +152,7 @@ class TestParkNavigation:
|
||||
|
||||
expect(page).to_have_url("**/parks/**")
|
||||
|
||||
def test__back_button__returns_to_previous_page(
|
||||
self, page: Page, live_server, parks_data
|
||||
):
|
||||
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/")
|
||||
|
||||
|
||||
@@ -26,11 +26,7 @@ def operating_park(db):
|
||||
from tests.factories import ParkFactory
|
||||
|
||||
# Use factory to create a complete park
|
||||
park = ParkFactory(
|
||||
name="E2E Test Park",
|
||||
slug="e2e-test-park",
|
||||
status="OPERATING"
|
||||
)
|
||||
park = ParkFactory(name="E2E Test Park", slug="e2e-test-park", status="OPERATING")
|
||||
|
||||
yield park
|
||||
|
||||
@@ -42,12 +38,7 @@ def operating_ride(db, operating_park):
|
||||
"""Create an operating Ride for testing status transitions."""
|
||||
from tests.factories import RideFactory
|
||||
|
||||
ride = RideFactory(
|
||||
name="E2E Test Ride",
|
||||
slug="e2e-test-ride",
|
||||
park=operating_park,
|
||||
status="OPERATING"
|
||||
)
|
||||
ride = RideFactory(name="E2E Test Ride", slug="e2e-test-ride", park=operating_park, status="OPERATING")
|
||||
|
||||
yield ride
|
||||
|
||||
@@ -55,31 +46,25 @@ def operating_ride(db, operating_park):
|
||||
class TestParkStatusTransitions:
|
||||
"""Tests for Park FSM status transitions via HTMX."""
|
||||
|
||||
def test_park_close_temporarily_as_moderator(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_park_close_temporarily_as_moderator(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test closing a park temporarily as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Verify initial status badge shows Operating
|
||||
status_section = mod_page.locator('[data-park-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-park-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Operating")
|
||||
|
||||
# Find and click "Close Temporarily" button
|
||||
close_temp_btn = status_section.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_section.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
if not close_temp_btn.is_visible():
|
||||
# May be in a dropdown menu
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
close_temp_btn = mod_page.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = mod_page.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
# Handle confirmation dialog
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
@@ -87,7 +72,7 @@ class TestParkStatusTransitions:
|
||||
close_temp_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -97,9 +82,7 @@ class TestParkStatusTransitions:
|
||||
operating_park.refresh_from_db()
|
||||
assert operating_park.status == "CLOSED_TEMP"
|
||||
|
||||
def test_park_reopen_from_closed_temp(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_park_reopen_from_closed_temp(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test reopening a temporarily closed park."""
|
||||
# First close the park temporarily
|
||||
operating_park.status = "CLOSED_TEMP"
|
||||
@@ -109,18 +92,18 @@ class TestParkStatusTransitions:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Verify initial status badge shows Temporarily Closed
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Temporarily Closed")
|
||||
|
||||
# Find and click "Reopen" button
|
||||
status_section = mod_page.locator('[data-park-status-actions]')
|
||||
status_section = mod_page.locator("[data-park-status-actions]")
|
||||
reopen_btn = status_section.get_by_role("button", name="Reopen")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
reopen_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -130,34 +113,28 @@ class TestParkStatusTransitions:
|
||||
operating_park.refresh_from_db()
|
||||
assert operating_park.status == "OPERATING"
|
||||
|
||||
def test_park_close_permanently_as_moderator(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_park_close_permanently_as_moderator(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test closing a park permanently as a moderator."""
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-park-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-park-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Close Permanently" button
|
||||
close_perm_btn = status_section.get_by_role(
|
||||
"button", name="Close Permanently"
|
||||
)
|
||||
close_perm_btn = status_section.get_by_role("button", name="Close Permanently")
|
||||
|
||||
if not close_perm_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
close_perm_btn = mod_page.get_by_role(
|
||||
"button", name="Close Permanently"
|
||||
)
|
||||
close_perm_btn = mod_page.get_by_role("button", name="Close Permanently")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
close_perm_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -167,9 +144,7 @@ class TestParkStatusTransitions:
|
||||
operating_park.refresh_from_db()
|
||||
assert operating_park.status == "CLOSED_PERM"
|
||||
|
||||
def test_park_demolish_from_closed_perm(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_park_demolish_from_closed_perm(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test transitioning a permanently closed park to demolished."""
|
||||
# Set park to permanently closed
|
||||
operating_park.status = "CLOSED_PERM"
|
||||
@@ -179,25 +154,23 @@ class TestParkStatusTransitions:
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-park-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-park-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Mark as Demolished" button
|
||||
demolish_btn = status_section.get_by_role("button", name="Mark as Demolished")
|
||||
|
||||
if not demolish_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
demolish_btn = mod_page.get_by_role(
|
||||
"button", name="Mark as Demolished"
|
||||
)
|
||||
demolish_btn = mod_page.get_by_role("button", name="Mark as Demolished")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
demolish_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -207,24 +180,18 @@ class TestParkStatusTransitions:
|
||||
operating_park.refresh_from_db()
|
||||
assert operating_park.status == "DEMOLISHED"
|
||||
|
||||
def test_park_available_transitions_update(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_park_available_transitions_update(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test that available transitions update based on current state."""
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-park-status-actions]')
|
||||
status_section = mod_page.locator("[data-park-status-actions]")
|
||||
|
||||
# Operating park should have Close Temporarily and Close Permanently
|
||||
expect(
|
||||
status_section.get_by_role("button", name="Close Temporarily")
|
||||
).to_be_visible()
|
||||
expect(status_section.get_by_role("button", name="Close Temporarily")).to_be_visible()
|
||||
|
||||
# Should NOT have Reopen (not applicable for Operating state)
|
||||
expect(
|
||||
status_section.get_by_role("button", name="Reopen")
|
||||
).not_to_be_visible()
|
||||
expect(status_section.get_by_role("button", name="Reopen")).not_to_be_visible()
|
||||
|
||||
# Now close temporarily and verify buttons change
|
||||
operating_park.status = "CLOSED_TEMP"
|
||||
@@ -234,37 +201,29 @@ class TestParkStatusTransitions:
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Now should have Reopen button
|
||||
expect(
|
||||
status_section.get_by_role("button", name="Reopen")
|
||||
).to_be_visible()
|
||||
expect(status_section.get_by_role("button", name="Reopen")).to_be_visible()
|
||||
|
||||
|
||||
class TestRideStatusTransitions:
|
||||
"""Tests for Ride FSM status transitions via HTMX."""
|
||||
|
||||
def test_ride_close_temporarily_as_moderator(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_close_temporarily_as_moderator(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test closing a ride temporarily as a moderator."""
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Operating")
|
||||
|
||||
# Find and click "Close Temporarily" button
|
||||
close_temp_btn = status_section.get_by_role(
|
||||
"button", name="Close Temporarily"
|
||||
)
|
||||
close_temp_btn = status_section.get_by_role("button", name="Close Temporarily")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
close_temp_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -274,23 +233,19 @@ class TestRideStatusTransitions:
|
||||
operating_ride.refresh_from_db()
|
||||
assert operating_ride.status == "CLOSED_TEMP"
|
||||
|
||||
def test_ride_mark_sbno_as_moderator(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_mark_sbno_as_moderator(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test marking a ride as Standing But Not Operating (SBNO)."""
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Mark SBNO" button
|
||||
sbno_btn = status_section.get_by_role("button", name="Mark SBNO")
|
||||
|
||||
if not sbno_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
sbno_btn = mod_page.get_by_role("button", name="Mark SBNO")
|
||||
@@ -299,7 +254,7 @@ class TestRideStatusTransitions:
|
||||
sbno_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -309,21 +264,17 @@ class TestRideStatusTransitions:
|
||||
operating_ride.refresh_from_db()
|
||||
assert operating_ride.status == "SBNO"
|
||||
|
||||
def test_ride_reopen_from_closed_temp(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_reopen_from_closed_temp(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test reopening a temporarily closed ride."""
|
||||
# First close the ride temporarily
|
||||
operating_ride.status = "CLOSED_TEMP"
|
||||
operating_ride.save()
|
||||
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Reopen" button
|
||||
reopen_btn = status_section.get_by_role("button", name="Reopen")
|
||||
@@ -332,7 +283,7 @@ class TestRideStatusTransitions:
|
||||
reopen_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -342,36 +293,28 @@ class TestRideStatusTransitions:
|
||||
operating_ride.refresh_from_db()
|
||||
assert operating_ride.status == "OPERATING"
|
||||
|
||||
def test_ride_close_permanently_as_moderator(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_close_permanently_as_moderator(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test closing a ride permanently as a moderator."""
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Close Permanently" button
|
||||
close_perm_btn = status_section.get_by_role(
|
||||
"button", name="Close Permanently"
|
||||
)
|
||||
close_perm_btn = status_section.get_by_role("button", name="Close Permanently")
|
||||
|
||||
if not close_perm_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
close_perm_btn = mod_page.get_by_role(
|
||||
"button", name="Close Permanently"
|
||||
)
|
||||
close_perm_btn = mod_page.get_by_role("button", name="Close Permanently")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
close_perm_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -381,41 +324,33 @@ class TestRideStatusTransitions:
|
||||
operating_ride.refresh_from_db()
|
||||
assert operating_ride.status == "CLOSED_PERM"
|
||||
|
||||
def test_ride_demolish_from_closed_perm(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_demolish_from_closed_perm(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test transitioning a permanently closed ride to demolished."""
|
||||
# Set ride to permanently closed
|
||||
operating_ride.status = "CLOSED_PERM"
|
||||
operating_ride.closing_date = date.today() - timedelta(days=365)
|
||||
operating_ride.save()
|
||||
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Mark as Demolished" button
|
||||
demolish_btn = status_section.get_by_role(
|
||||
"button", name="Mark as Demolished"
|
||||
)
|
||||
demolish_btn = status_section.get_by_role("button", name="Mark as Demolished")
|
||||
|
||||
if not demolish_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
demolish_btn = mod_page.get_by_role(
|
||||
"button", name="Mark as Demolished"
|
||||
)
|
||||
demolish_btn = mod_page.get_by_role("button", name="Mark as Demolished")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
demolish_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -425,41 +360,33 @@ class TestRideStatusTransitions:
|
||||
operating_ride.refresh_from_db()
|
||||
assert operating_ride.status == "DEMOLISHED"
|
||||
|
||||
def test_ride_relocate_from_closed_perm(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_relocate_from_closed_perm(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test transitioning a permanently closed ride to relocated."""
|
||||
# Set ride to permanently closed
|
||||
operating_ride.status = "CLOSED_PERM"
|
||||
operating_ride.closing_date = date.today() - timedelta(days=365)
|
||||
operating_ride.save()
|
||||
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
|
||||
# Find and click "Mark as Relocated" button
|
||||
relocate_btn = status_section.get_by_role(
|
||||
"button", name="Mark as Relocated"
|
||||
)
|
||||
relocate_btn = status_section.get_by_role("button", name="Mark as Relocated")
|
||||
|
||||
if not relocate_btn.is_visible():
|
||||
actions_dropdown = status_section.locator('[data-actions-dropdown]')
|
||||
actions_dropdown = status_section.locator("[data-actions-dropdown]")
|
||||
if actions_dropdown.is_visible():
|
||||
actions_dropdown.click()
|
||||
relocate_btn = mod_page.get_by_role(
|
||||
"button", name="Mark as Relocated"
|
||||
)
|
||||
relocate_btn = mod_page.get_by_role("button", name="Mark as Relocated")
|
||||
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
relocate_btn.click()
|
||||
|
||||
# Verify toast notification
|
||||
toast = mod_page.locator('[data-toast]')
|
||||
toast = mod_page.locator("[data-toast]")
|
||||
expect(toast).to_be_visible(timeout=5000)
|
||||
|
||||
# Verify status badge updated
|
||||
@@ -473,28 +400,22 @@ class TestRideStatusTransitions:
|
||||
class TestRideClosingWorkflow:
|
||||
"""Tests for the special CLOSING status workflow with automatic transitions."""
|
||||
|
||||
def test_ride_set_closing_with_future_date(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_set_closing_with_future_date(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test setting a ride to CLOSING status with a future closing date."""
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_section = mod_page.locator('[data-ride-status-actions]')
|
||||
status_section = mod_page.locator("[data-ride-status-actions]")
|
||||
|
||||
# Find and click "Set Closing" button
|
||||
set_closing_btn = status_section.get_by_role(
|
||||
"button", name="Set Closing"
|
||||
)
|
||||
set_closing_btn = status_section.get_by_role("button", name="Set Closing")
|
||||
|
||||
if set_closing_btn.is_visible():
|
||||
mod_page.on("dialog", lambda dialog: dialog.accept())
|
||||
set_closing_btn.click()
|
||||
|
||||
# Verify status badge updated
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Closing", timeout=5000)
|
||||
|
||||
# Verify database state
|
||||
@@ -503,9 +424,7 @@ class TestRideClosingWorkflow:
|
||||
else:
|
||||
pytest.skip("Set Closing button not available")
|
||||
|
||||
def test_ride_closing_shows_countdown(
|
||||
self, mod_page: Page, operating_ride, live_server
|
||||
):
|
||||
def test_ride_closing_shows_countdown(self, mod_page: Page, operating_ride, live_server):
|
||||
"""Test that a ride in CLOSING status shows a countdown to closing date."""
|
||||
# Set ride to CLOSING with future date
|
||||
future_date = date.today() + timedelta(days=30)
|
||||
@@ -513,37 +432,31 @@ class TestRideClosingWorkflow:
|
||||
operating_ride.closing_date = future_date
|
||||
operating_ride.save()
|
||||
|
||||
mod_page.goto(
|
||||
f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/"
|
||||
)
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_ride.park.slug}/rides/{operating_ride.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
# Verify closing countdown is displayed
|
||||
closing_info = mod_page.locator('[data-closing-countdown]')
|
||||
closing_info = mod_page.locator("[data-closing-countdown]")
|
||||
if closing_info.is_visible():
|
||||
expect(closing_info).to_contain_text("30")
|
||||
else:
|
||||
# May just show the status badge
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_contain_text("Closing")
|
||||
|
||||
|
||||
class TestStatusBadgeStyling:
|
||||
"""Tests for correct status badge styling based on state."""
|
||||
|
||||
def test_operating_status_badge_style(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_operating_status_badge_style(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test that Operating status has correct green styling."""
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_have_class(re.compile(r"bg-green|text-green|success"))
|
||||
|
||||
def test_closed_temp_status_badge_style(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_closed_temp_status_badge_style(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test that Temporarily Closed status has correct yellow/warning styling."""
|
||||
operating_park.status = "CLOSED_TEMP"
|
||||
operating_park.save()
|
||||
@@ -551,12 +464,10 @@ class TestStatusBadgeStyling:
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_have_class(re.compile(r"bg-yellow|text-yellow|warning"))
|
||||
|
||||
def test_closed_perm_status_badge_style(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_closed_perm_status_badge_style(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test that Permanently Closed status has correct red/danger styling."""
|
||||
operating_park.status = "CLOSED_PERM"
|
||||
operating_park.save()
|
||||
@@ -564,12 +475,10 @@ class TestStatusBadgeStyling:
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_have_class(re.compile(r"bg-red|text-red|danger"))
|
||||
|
||||
def test_demolished_status_badge_style(
|
||||
self, mod_page: Page, operating_park, live_server
|
||||
):
|
||||
def test_demolished_status_badge_style(self, mod_page: Page, operating_park, live_server):
|
||||
"""Test that Demolished status has correct gray styling."""
|
||||
operating_park.status = "DEMOLISHED"
|
||||
operating_park.save()
|
||||
@@ -577,5 +486,5 @@ class TestStatusBadgeStyling:
|
||||
mod_page.goto(f"{live_server.url}/parks/{operating_park.slug}/")
|
||||
mod_page.wait_for_load_state("networkidle")
|
||||
|
||||
status_badge = mod_page.locator('[data-status-badge]')
|
||||
status_badge = mod_page.locator("[data-status-badge]")
|
||||
expect(status_badge).to_have_class(re.compile(r"bg-gray|text-gray|muted"))
|
||||
|
||||
@@ -24,9 +24,7 @@ class TestReviewSubmission:
|
||||
reviews_tab.click()
|
||||
|
||||
# Click write review button
|
||||
write_review = auth_page.locator(
|
||||
"button:has-text('Write Review'), a:has-text('Write Review')"
|
||||
)
|
||||
write_review = auth_page.locator("button:has-text('Write Review'), a:has-text('Write Review')")
|
||||
|
||||
if write_review.count() > 0:
|
||||
write_review.first.click()
|
||||
@@ -36,9 +34,7 @@ class TestReviewSubmission:
|
||||
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
|
||||
):
|
||||
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}/")
|
||||
@@ -48,9 +44,7 @@ class TestReviewSubmission:
|
||||
if reviews_tab.count() > 0:
|
||||
reviews_tab.click()
|
||||
|
||||
write_review = auth_page.locator(
|
||||
"button:has-text('Write Review'), a:has-text('Write Review')"
|
||||
)
|
||||
write_review = auth_page.locator("button:has-text('Write Review'), a:has-text('Write Review')")
|
||||
|
||||
if write_review.count() > 0:
|
||||
write_review.first.click()
|
||||
@@ -63,9 +57,7 @@ class TestReviewSubmission:
|
||||
# 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("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."
|
||||
)
|
||||
@@ -75,9 +67,7 @@ class TestReviewSubmission:
|
||||
# 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
|
||||
):
|
||||
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}/")
|
||||
@@ -86,20 +76,14 @@ class TestReviewSubmission:
|
||||
if reviews_tab.count() > 0:
|
||||
reviews_tab.click()
|
||||
|
||||
write_review = auth_page.locator(
|
||||
"button:has-text('Write Review'), a:has-text('Write Review')"
|
||||
)
|
||||
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.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()
|
||||
|
||||
@@ -123,9 +107,7 @@ class TestReviewDisplay:
|
||||
reviews_tab.click()
|
||||
|
||||
# Reviews should be displayed
|
||||
reviews_section = page.locator(
|
||||
"[data-testid='reviews-list'], .reviews-list, .review-item"
|
||||
)
|
||||
reviews_section = page.locator("[data-testid='reviews-list'], .reviews-list, .review-item")
|
||||
|
||||
if reviews_section.count() > 0:
|
||||
expect(reviews_section.first).to_be_visible()
|
||||
@@ -136,9 +118,7 @@ class TestReviewDisplay:
|
||||
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']"
|
||||
)
|
||||
rating = page.locator(".rating, .stars, [data-testid='rating']")
|
||||
|
||||
if rating.count() > 0:
|
||||
expect(rating.first).to_be_visible()
|
||||
@@ -153,9 +133,7 @@ class TestReviewDisplay:
|
||||
reviews_tab.click()
|
||||
|
||||
# Author name should be visible in review
|
||||
author = page.locator(
|
||||
".review-author, .author, [data-testid='author']"
|
||||
)
|
||||
author = page.locator(".review-author, .author, [data-testid='author']")
|
||||
|
||||
if author.count() > 0:
|
||||
expect(author.first).to_be_visible()
|
||||
@@ -170,9 +148,7 @@ class TestReviewEditing:
|
||||
# Navigate to reviews after creating one
|
||||
|
||||
# Look for edit button on own review
|
||||
edit_button = auth_page.locator(
|
||||
"button:has-text('Edit'), a:has-text('Edit 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()
|
||||
@@ -180,17 +156,13 @@ class TestReviewEditing:
|
||||
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')"
|
||||
)
|
||||
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 = 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()
|
||||
@@ -204,9 +176,7 @@ class TestReviewEditing:
|
||||
class TestReviewModeration:
|
||||
"""E2E tests for review moderation."""
|
||||
|
||||
def test__moderator__sees_moderation_actions(
|
||||
self, mod_page: Page, live_server, parks_data
|
||||
):
|
||||
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}/")
|
||||
@@ -216,9 +186,7 @@ class TestReviewModeration:
|
||||
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']"
|
||||
)
|
||||
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()
|
||||
@@ -259,16 +227,12 @@ class TestReviewVoting:
|
||||
reviews_tab.click()
|
||||
|
||||
# Look for helpful/upvote buttons
|
||||
vote_buttons = page.locator(
|
||||
"button:has-text('Helpful'), button[aria-label*='helpful'], .vote-button"
|
||||
)
|
||||
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
|
||||
):
|
||||
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}/")
|
||||
@@ -277,9 +241,7 @@ class TestReviewVoting:
|
||||
if reviews_tab.count() > 0:
|
||||
reviews_tab.click()
|
||||
|
||||
helpful_button = auth_page.locator(
|
||||
"button:has-text('Helpful'), button[aria-label*='helpful']"
|
||||
)
|
||||
helpful_button = auth_page.locator("button:has-text('Helpful'), button[aria-label*='helpful']")
|
||||
|
||||
if helpful_button.count() > 0:
|
||||
helpful_button.first.click()
|
||||
@@ -298,34 +260,24 @@ class TestRideReviews:
|
||||
page.goto(f"{live_server.url}/rides/{ride.slug}/")
|
||||
|
||||
# Reviews section should be present
|
||||
reviews_section = page.locator(
|
||||
"[data-testid='reviews'], #reviews, .reviews-section"
|
||||
)
|
||||
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
|
||||
):
|
||||
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')"
|
||||
)
|
||||
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']"
|
||||
)
|
||||
auth_page.locator(
|
||||
"input[name='wait_time'], select[name='wait_time']"
|
||||
)
|
||||
intensity_field = auth_page.locator("select[name='intensity'], input[name='intensity']")
|
||||
auth_page.locator("input[name='wait_time'], select[name='wait_time']")
|
||||
|
||||
# At least one experience field should be present
|
||||
if intensity_field.count() > 0:
|
||||
@@ -345,9 +297,7 @@ class TestReviewFiltering:
|
||||
if reviews_tab.count() > 0:
|
||||
reviews_tab.click()
|
||||
|
||||
sort_select = page.locator(
|
||||
"select[name='sort'], [data-testid='sort-reviews']"
|
||||
)
|
||||
sort_select = page.locator("select[name='sort'], [data-testid='sort-reviews']")
|
||||
|
||||
if sort_select.count() > 0:
|
||||
sort_select.first.select_option("date")
|
||||
@@ -362,9 +312,7 @@ class TestReviewFiltering:
|
||||
if reviews_tab.count() > 0:
|
||||
reviews_tab.click()
|
||||
|
||||
rating_filter = page.locator(
|
||||
"select[name='rating'], [data-testid='rating-filter']"
|
||||
)
|
||||
rating_filter = page.locator("select[name='rating'], [data-testid='rating-filter']")
|
||||
|
||||
if rating_filter.count() > 0:
|
||||
rating_filter.first.select_option("5")
|
||||
|
||||
@@ -44,9 +44,7 @@ class TestUserRegistration:
|
||||
# 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
|
||||
):
|
||||
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/")
|
||||
|
||||
@@ -100,9 +98,7 @@ class TestUserLogin:
|
||||
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
|
||||
):
|
||||
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/")
|
||||
|
||||
@@ -130,9 +126,7 @@ class TestUserLogin:
|
||||
"""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']"
|
||||
)
|
||||
remember_me = page.locator("input[name='remember'], input[type='checkbox'][id*='remember']")
|
||||
|
||||
if remember_me.count() > 0:
|
||||
expect(remember_me.first).to_be_visible()
|
||||
@@ -147,9 +141,7 @@ class TestUserLogout:
|
||||
# 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')"
|
||||
)
|
||||
logout = auth_page.locator("a[href*='logout'], button:has-text('Log Out'), button:has-text('Sign Out')")
|
||||
|
||||
if logout.count() > 0:
|
||||
logout.first.click()
|
||||
@@ -172,14 +164,10 @@ class TestPasswordReset:
|
||||
"""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']"
|
||||
)
|
||||
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
|
||||
):
|
||||
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/")
|
||||
|
||||
@@ -192,9 +180,7 @@ class TestPasswordReset:
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# Look for success message or confirmation page
|
||||
success = page.locator(
|
||||
".success, .alert-success, [role='alert']"
|
||||
)
|
||||
success = page.locator(".success, .alert-success, [role='alert']")
|
||||
|
||||
# Or check URL changed to done page
|
||||
if success.count() == 0:
|
||||
@@ -216,9 +202,7 @@ class TestUserProfile:
|
||||
"""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')"
|
||||
)
|
||||
edit_link = auth_page.locator("a[href*='edit'], button:has-text('Edit')")
|
||||
|
||||
if edit_link.count() > 0:
|
||||
expect(edit_link.first).to_be_visible()
|
||||
@@ -228,9 +212,7 @@ class TestUserProfile:
|
||||
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']"
|
||||
)
|
||||
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")
|
||||
@@ -245,18 +227,14 @@ class TestUserProfile:
|
||||
class TestProtectedRoutes:
|
||||
"""E2E tests for protected route access."""
|
||||
|
||||
def test__protected_route__unauthenticated__redirects_to_login(
|
||||
self, page: Page, live_server
|
||||
):
|
||||
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
|
||||
):
|
||||
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/")
|
||||
|
||||
@@ -270,9 +248,7 @@ class TestProtectedRoutes:
|
||||
# Should show login or forbidden
|
||||
# Admin login page or 403
|
||||
|
||||
def test__moderator_route__moderator__allows_access(
|
||||
self, mod_page: Page, live_server
|
||||
):
|
||||
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/")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user