Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
This commit is contained in:
@@ -15,18 +15,66 @@ TEST_CHANNEL_LAYERS = {
|
||||
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
|
||||
class RoomConsumerTest(SimpleTestCase):
|
||||
async def test_can_connect_and_disconnect(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/test-room/")
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_gate_update_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/test-room/")
|
||||
async def test_receives_role_select_start_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_test-room",
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "role_select_start", "slot_order": [1, 2, 3, 4, 5, 6]},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "role_select_start")
|
||||
self.assertEqual(response["slot_order"], [1, 2, 3, 4, 5, 6])
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_turn_changed_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "turn_changed", "active_slot": 2},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "turn_changed")
|
||||
self.assertEqual(response["active_slot"], 2)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_roles_revealed_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "roles_revealed", "assignments": {"1": "PC", "2": "BC"}},
|
||||
)
|
||||
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response["type"], "roles_revealed")
|
||||
self.assertIn("assignments", response)
|
||||
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_receives_gate_update_broadcast(self):
|
||||
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
|
||||
await communicator.connect()
|
||||
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
"room_00000000-0000-0000-0000-000000000001",
|
||||
{"type": "gate_update", "gate_state": "some_state"},
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.lyric.models import Token, User
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, debit_token, select_token
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat, debit_token, select_token
|
||||
|
||||
|
||||
class RoomCreationTest(TestCase):
|
||||
@@ -132,6 +132,62 @@ class SelectTokenTest(TestCase):
|
||||
self.assertEqual(token.token_type, Token.PASS)
|
||||
|
||||
|
||||
class RoomTableStatusTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
||||
|
||||
def test_table_status_defaults_to_blank(self):
|
||||
self.room.refresh_from_db()
|
||||
self.assertFalse(self.room.table_status)
|
||||
|
||||
def test_room_has_role_select_constant(self):
|
||||
self.assertEqual(Room.ROLE_SELECT, "ROLE_SELECT")
|
||||
|
||||
def test_room_has_sig_select_constant(self):
|
||||
self.assertEqual(Room.SIG_SELECT, "SIG_SELECT")
|
||||
|
||||
def test_room_has_in_game_constant(self):
|
||||
self.assertEqual(Room.IN_GAME, "IN_GAME")
|
||||
|
||||
def test_table_status_accepts_role_select(self):
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
|
||||
|
||||
|
||||
class TableSeatModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.owner)
|
||||
|
||||
def test_table_seat_can_be_created(self):
|
||||
seat = TableSeat.objects.create(
|
||||
room=self.room,
|
||||
gamer=self.owner,
|
||||
slot_number=1,
|
||||
)
|
||||
self.assertEqual(seat.slot_number, 1)
|
||||
self.assertIsNone(seat.role)
|
||||
self.assertFalse(seat.role_revealed)
|
||||
self.assertIsNone(seat.seat_position)
|
||||
|
||||
def test_table_seat_role_choices_cover_all_six(self):
|
||||
role_codes = [c[0] for c in TableSeat.ROLE_CHOICES]
|
||||
for code in ["PC", "BC", "SC", "AC", "NC", "EC"]:
|
||||
self.assertIn(code, role_codes)
|
||||
|
||||
def test_partner_map_pairs_are_mutual(self):
|
||||
for a, b in [(TableSeat.PC, TableSeat.BC), (TableSeat.SC, TableSeat.AC), (TableSeat.NC, TableSeat.EC)]:
|
||||
self.assertEqual(TableSeat.PARTNER_MAP[a], b)
|
||||
self.assertEqual(TableSeat.PARTNER_MAP[b], a)
|
||||
|
||||
def test_room_table_seats_reverse_relation(self):
|
||||
TableSeat.objects.create(room=self.room, gamer=self.owner, slot_number=1)
|
||||
self.assertEqual(self.room.table_seats.count(), 1)
|
||||
|
||||
|
||||
class RoomInviteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@example.com")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.lyric.models import Token, User
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat
|
||||
|
||||
|
||||
class RoomCreationViewTest(TestCase):
|
||||
@@ -346,6 +348,298 @@ class ConfirmTokenPriorityViewTest(TestCase):
|
||||
self.assertIsNone(self.coin.current_room)
|
||||
|
||||
|
||||
class RoleSelectRenderingTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
self.gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
self.gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i)
|
||||
|
||||
def test_room_view_includes_card_stack_when_role_select(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "card-stack")
|
||||
|
||||
def test_card_stack_eligible_for_slot1_gamer(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-state="eligible"')
|
||||
|
||||
def test_card_stack_ineligible_for_slot2_gamer(self):
|
||||
self.client.force_login(self.gamers[1])
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-state="ineligible"')
|
||||
|
||||
def test_card_stack_ineligible_shows_fa_ban(self):
|
||||
self.client.force_login(self.gamers[1])
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "fa-ban")
|
||||
|
||||
def test_card_stack_eligible_omits_fa_ban(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertNotContains(response, "fa-ban")
|
||||
|
||||
def test_gatekeeper_overlay_absent_when_role_select(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertNotContains(response, "gate-overlay")
|
||||
|
||||
def test_six_table_seats_rendered(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "table-seat", count=6)
|
||||
|
||||
def test_active_table_seat_has_active_class(self):
|
||||
self.client.force_login(self.founder) # slot 1 is active
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'class="table-seat active"')
|
||||
|
||||
def test_inactive_table_seat_lacks_active_class(self):
|
||||
self.client.force_login(self.founder)
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
# Slots 2–6 are not active, so at least one plain table-seat exists
|
||||
self.assertContains(response, 'class="table-seat"')
|
||||
|
||||
def test_card_stack_has_data_user_slots_for_eligible_gamer(self):
|
||||
self.client.force_login(self.founder) # founder is slot 1 only
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-user-slots="1"')
|
||||
|
||||
def test_card_stack_has_data_user_slots_for_ineligible_gamer(self):
|
||||
self.client.force_login(self.gamers[1]) # slot 2 gamer
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, 'data-user-slots="2"')
|
||||
|
||||
|
||||
class PickRolesViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.client.force_login(self.founder)
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
for i in range(1, 7):
|
||||
gamer = self.founder if i == 1 else User.objects.create(email=f"g{i}@test.io")
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
|
||||
def test_pick_roles_transitions_room_to_role_select(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
|
||||
|
||||
def test_pick_roles_creates_one_table_seat_per_filled_slot(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.assertEqual(TableSeat.objects.filter(room=self.room).count(), 6)
|
||||
|
||||
def test_pick_roles_table_seats_carry_gamer_and_slot_number(self):
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=1)
|
||||
self.assertEqual(seat.gamer, self.founder)
|
||||
|
||||
def test_only_open_room_can_start_role_select(self):
|
||||
self.room.gate_status = Room.GATHERING
|
||||
self.room.save()
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
self.room.refresh_from_db()
|
||||
self.assertIsNone(self.room.table_status)
|
||||
|
||||
def test_pick_roles_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(
|
||||
reverse("epic:pick_roles", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/accounts/login/", response.url)
|
||||
|
||||
def test_pick_roles_redirects_to_room(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:pick_roles", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
def test_pick_roles_notifies_channel_layer(self):
|
||||
with patch("apps.epic.views._notify_role_select_start") as mock_notify:
|
||||
self.client.post(reverse("epic:pick_roles", kwargs={"room_id": self.room.id}))
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
|
||||
class SelectRoleViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
self.gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
self.gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
slot = self.room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.save()
|
||||
for i, gamer in enumerate(self.gamers, start=1):
|
||||
TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i)
|
||||
self.client.force_login(self.founder)
|
||||
|
||||
def test_select_role_records_choice(self):
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=1)
|
||||
self.assertEqual(seat.role, "PC")
|
||||
|
||||
def test_select_role_wrong_turn_makes_no_change(self):
|
||||
self.client.force_login(self.gamers[1]) # slot 2 — not their turn
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "BC"},
|
||||
)
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=2)
|
||||
self.assertIsNone(seat.role)
|
||||
|
||||
def test_turn_advances_after_selection(self):
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
next_active = TableSeat.objects.filter(
|
||||
room=self.room, role__isnull=True
|
||||
).order_by("slot_number").first()
|
||||
self.assertEqual(next_active.slot_number, 2)
|
||||
|
||||
def test_all_selected_sets_sig_select(self):
|
||||
roles = ["PC", "BC", "SC", "AC", "NC"]
|
||||
for i, role in enumerate(roles):
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
|
||||
seat.role = role
|
||||
seat.save()
|
||||
self.client.force_login(self.gamers[5]) # slot 6 — last
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "EC"},
|
||||
)
|
||||
self.room.refresh_from_db()
|
||||
self.assertEqual(self.room.table_status, Room.SIG_SELECT)
|
||||
|
||||
def test_select_role_notifies_turn_changed(self):
|
||||
with patch("apps.epic.views._notify_turn_changed") as mock_notify:
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
def test_select_role_notifies_roles_revealed_when_last(self):
|
||||
roles = ["PC", "BC", "SC", "AC", "NC"]
|
||||
for i, role in enumerate(roles):
|
||||
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
|
||||
seat.role = role
|
||||
seat.save()
|
||||
self.client.force_login(self.gamers[5])
|
||||
with patch("apps.epic.views._notify_roles_revealed") as mock_notify:
|
||||
self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "EC"},
|
||||
)
|
||||
mock_notify.assert_called_once_with(self.room.id)
|
||||
|
||||
def test_select_role_requires_login(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("/accounts/login/", response.url)
|
||||
|
||||
def test_select_role_redirects_to_room(self):
|
||||
response = self.client.post(
|
||||
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
|
||||
data={"role": "PC"},
|
||||
)
|
||||
self.assertRedirects(
|
||||
response, reverse("epic:gatekeeper", args=[self.room.id])
|
||||
)
|
||||
|
||||
|
||||
class RevealPhaseRenderingTest(TestCase):
|
||||
def setUp(self):
|
||||
self.founder = User.objects.create(email="founder@test.io")
|
||||
self.room = Room.objects.create(name="Test Room", owner=self.founder)
|
||||
gamers = [self.founder]
|
||||
for i in range(2, 7):
|
||||
gamers.append(User.objects.create(email=f"g{i}@test.io"))
|
||||
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
|
||||
for i, (gamer, role) in enumerate(zip(gamers, roles), start=1):
|
||||
TableSeat.objects.create(
|
||||
room=self.room, gamer=gamer, slot_number=i,
|
||||
role=role, role_revealed=True,
|
||||
)
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.table_status = Room.SIG_SELECT
|
||||
self.room.save()
|
||||
self.client.force_login(self.founder)
|
||||
|
||||
def test_face_up_role_cards_rendered_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "face-up")
|
||||
|
||||
def test_inv_role_card_slot_present(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "id_inv_role_card")
|
||||
|
||||
def test_partner_indicator_present_when_sig_select(self):
|
||||
response = self.client.get(
|
||||
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
|
||||
)
|
||||
self.assertContains(response, "partner-indicator")
|
||||
|
||||
|
||||
class RoomActionsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create(email="owner@test.io")
|
||||
|
||||
Reference in New Issue
Block a user