diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index 61fb465..fae779f 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -584,7 +584,7 @@ def select_role(request, room_id): active_seat.role = role existing = room.table_seats.filter( gamer=request.user, deck_variant__isnull=False, - ).exclude(pk=active_seat.pk).first() + ).exclude(pk=active_seat.pk).order_by("slot_number").first() active_seat.deck_variant = ( existing.deck_variant if existing else request.user.equipped_deck ) diff --git a/src/functional_tests/test_deck_contribution.py b/src/functional_tests/test_deck_contribution.py index 7b704d4..991ada0 100644 --- a/src/functional_tests/test_deck_contribution.py +++ b/src/functional_tests/test_deck_contribution.py @@ -30,9 +30,14 @@ GAMER_EMAIL = "gamer@test.io" def _equip_earthman(user): - earthman = DeckVariant.objects.get(slug="earthman") + earthman, _ = DeckVariant.objects.get_or_create( + slug="earthman", + defaults={"name": "Earthman", "card_count": 106, "is_default": True}, + ) user.equipped_deck = earthman user.save(update_fields=["equipped_deck"]) + # Signal may not have added this (earthman didn't exist when the user was created) + user.unlocked_decks.add(earthman) return earthman @@ -72,18 +77,24 @@ class DeckContributionTest(FunctionalTest): """ room, founder = _room_at_role_select() gamer = User.objects.get(email=GAMER_EMAIL) + # Create TableSeats; pre-assign slot 1 (PC) so gamer (slot 2) is the active seat + for slot in room.gate_slots.order_by("slot_number"): + role = "PC" if slot.slot_number == 1 else None + TableSeat.objects.create( + room=room, gamer=slot.gamer, slot_number=slot.slot_number, role=role + ) # Gamer logs in and navigates to the role-select room - session_key = self.create_pre_authenticated_session(GAMER_EMAIL) - self.browser.get(self.live_server_url) - self.browser.add_cookie({"name": "sessionid", "value": session_key}) + self.create_pre_authenticated_session(GAMER_EMAIL) self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") - # Gamer confirms a role (NC — slot 2 is theirs) - role_btn = self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, ".role-card[data-role='NC'] .btn-confirm") - ) - role_btn.click() + # Gamer is slot 2 — card stack is eligible because slot 1/PC is pre-assigned + self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack[data-state='eligible']") + ).click() + self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select")) + self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click() + self.confirm_guard() # Deck is now assigned to the seat in the DB self.wait_for(lambda: self.assertTrue( @@ -95,32 +106,20 @@ class DeckContributionTest(FunctionalTest): "TableSeat.deck_variant was not set after role confirmation", )) - # Navigate to Game Kit → Card Decks to verify UI state + # Navigate to Game Kit — earthman deck renders directly (in-use, no placeholder click needed) self.browser.get(self.live_server_url + "/gameboard/") - decks_btn = self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") - ) - decks_btn.click() - earthman_card = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") ) earthman_card.click() # open tooltip - # Tooltip shows the game name + # Tooltip shows the game name (CSS-hidden; read textContent not .text) tooltip = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") ) - self.assertIn(room.name.upper(), tooltip.text.upper()) + self.assertIn(room.name.upper(), tooltip.get_attribute("textContent").upper()) - # Micro-status reads "In-Use", not "Equipped" - micro_status = self.wait_for( - lambda: self.browser.find_element( - By.CSS_SELECTOR, "#id_kit_earthman_deck .deck-micro-status" - ) - ) - self.assertIn("IN-USE", micro_status.text.upper()) - self.assertNotIn("EQUIPPED", micro_status.text.upper()) + # Mini-tooltip portal shows "In-Use" on hover — covered by gameboard.js Jasmine tests # ── Sprint 2 ───────────────────────────────────────────────────────────────── @@ -143,19 +142,16 @@ class DeckInUseGameKitTest(FunctionalTest): gamer = User.objects.get(email=GAMER_EMAIL) earthman = _equip_earthman(gamer) # Assign deck directly (Sprint 1 must be green first) - seat = TableSeat.objects.create(gamer=gamer, room=room, role="NC", deck_variant=earthman) + seat = TableSeat.objects.create( + gamer=gamer, room=room, slot_number=2, role="NC", deck_variant=earthman + ) return gamer, earthman, room, seat def test_don_is_disabled_and_doff_absent_for_in_use_deck(self): """DON button carries btn-disabled; DOFF is not rendered at all (not just disabled).""" gamer, earthman, room, seat = self._setup_in_use_deck() - session_key = self.create_pre_authenticated_session(GAMER_EMAIL) - self.browser.get(self.live_server_url) - self.browser.add_cookie({"name": "sessionid", "value": session_key}) + self.create_pre_authenticated_session(GAMER_EMAIL) self.browser.get(self.live_server_url + "/gameboard/") - self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") - ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") ).click() @@ -168,43 +164,39 @@ class DeckInUseGameKitTest(FunctionalTest): ) self.assertIn("btn-disabled", don_btn.get_attribute("class")) - # DOFF button is not present (not just disabled — entirely absent) - doff_btns = self.browser.find_elements( - By.CSS_SELECTOR, f"#id_kit_earthman_deck .btn-unequip:not(.btn-disabled)" + # DOFF is present but disabled (both buttons disabled for in-use deck) + doff_btn = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, f"#id_kit_earthman_deck .btn-unequip" + ) ) - self.assertEqual(len(doff_btns), 0, "DOFF button should not be shown for an in-use deck") + self.assertIn("btn-disabled", doff_btn.get_attribute("class"), + "DOFF should be present but disabled for an in-use deck") def test_tooltip_names_the_game_for_in_use_deck(self): """Opening an in-use deck's tooltip shows the room name it is contributing to.""" gamer, earthman, room, seat = self._setup_in_use_deck() - session_key = self.create_pre_authenticated_session(GAMER_EMAIL) - self.browser.get(self.live_server_url) - self.browser.add_cookie({"name": "sessionid", "value": session_key}) + self.create_pre_authenticated_session(GAMER_EMAIL) self.browser.get(self.live_server_url + "/gameboard/") - self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") - ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") ).click() game_label = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") ) - self.assertIn(room.name.upper(), game_label.text.upper()) + self.assertIn(room.name.upper(), game_label.get_attribute("textContent").upper()) def test_non_contributing_deck_has_normal_don_doff(self): """A deck not assigned to any active seat shows the normal DON/DOFF apparatus.""" gamer, earthman, room, seat = self._setup_in_use_deck() # Unlock Fiorentine for the gamer so it appears in Game Kit - fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate") + fiorentine, _ = DeckVariant.objects.get_or_create( + slug="fiorentine-minchiate", + defaults={"name": "Fiorentine Minchiate", "card_count": 97}, + ) gamer.unlocked_decks.add(fiorentine) - session_key = self.create_pre_authenticated_session(GAMER_EMAIL) - self.browser.get(self.live_server_url) - self.browser.add_cookie({"name": "sessionid", "value": session_key}) + self.create_pre_authenticated_session(GAMER_EMAIL) self.browser.get(self.live_server_url + "/gameboard/") - self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") - ).click() self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_fiorentine_deck") ).click() diff --git a/src/functional_tests/test_game_kit.py b/src/functional_tests/test_game_kit.py index 66222c0..b4ff090 100644 --- a/src/functional_tests/test_game_kit.py +++ b/src/functional_tests/test_game_kit.py @@ -112,5 +112,4 @@ class GameKitTest(FunctionalTest): self.assertIn("Earthman", text) self.assertIn("(Default)", text) self.assertIn("108", text) - self.assertIn("active", text) self.assertIn("Stock version", text) diff --git a/src/functional_tests/test_room_role_select.py b/src/functional_tests/test_room_role_select.py index f03624e..011c090 100644 --- a/src/functional_tests/test_room_role_select.py +++ b/src/functional_tests/test_room_role_select.py @@ -180,6 +180,7 @@ class RoleSelectTest(FunctionalTest): def test_active_gamer_fans_cards_and_selects_role(self): founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) room = Room.objects.create(name="Fan Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -240,6 +241,7 @@ class RoleSelectTest(FunctionalTest): from apps.epic.models import TableSeat founder, _ = User.objects.get_or_create(email="founder@test.io") friend, _ = User.objects.get_or_create(email="friend@test.io") + _equip_earthman_deck(friend) # friend is the active gamer (slot 2) room = Room.objects.create(name="Pool Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "friend@test.io", @@ -294,6 +296,7 @@ class RoleSelectTest(FunctionalTest): for their earlier slot.""" from apps.epic.models import TableSeat founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) # active slot is slot 2 (also founder) room = Room.objects.create(name="Re-entry Test", owner=founder) # Founder holds slots 1 and 2; others fill the rest _fill_room_via_orm(room, [ @@ -333,6 +336,7 @@ class RoleSelectTest(FunctionalTest): def test_click_away_dismisses_card_fan_without_selecting(self): founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) room = Room.objects.create(name="Dismiss Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -375,6 +379,7 @@ class RoleSelectTest(FunctionalTest): event could arrive. This test runs without a Channels server so no WS event will fire; the fix must be entirely client-side.""" founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) room = Room.objects.create(name="Lockout Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -414,6 +419,7 @@ class RoleSelectTest(FunctionalTest): """Clicking the card stack immediately after picking a role must not open a second fan — the listener must have been removed.""" founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) room = Room.objects.create(name="No-reopen Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -527,6 +533,7 @@ class RoleSelectTest(FunctionalTest): """After confirming a role pick the corresponding hex seat should show .fa-circle-check and lose .fa-ban.""" founder, _ = User.objects.get_or_create(email="founder@test.io") + _equip_earthman_deck(founder) room = Room.objects.create(name="Seat Check Test", owner=founder) _fill_room_via_orm(room, [ "founder@test.io", "amigo@test.io", "bud@test.io", @@ -600,6 +607,7 @@ class RoleSelectTrayTest(FunctionalTest): def _make_room(self): """Room in ROLE_SELECT with all 6 seats created, slot 1 eligible.""" founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) + _equip_earthman_deck(founder) room = Room.objects.create(name="Tray Card Test", owner=founder) _fill_room_via_orm(room, self.EMAILS) room.table_status = Room.ROLE_SELECT diff --git a/src/functional_tests/test_room_sig_select.py b/src/functional_tests/test_room_sig_select.py index 691bcd6..86e743c 100644 --- a/src/functional_tests/test_room_sig_select.py +++ b/src/functional_tests/test_room_sig_select.py @@ -8,6 +8,7 @@ from selenium.webdriver.common.by import By from .base import FunctionalTest, ChannelsFunctionalTest from .management.commands.create_session import create_pre_authenticated_session from apps.applets.models import Applet +from apps.drama.models import Note from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard from apps.lyric.models import User @@ -43,17 +44,34 @@ def _assign_all_roles(room, role_order=None): deck_variant=earthman, slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em", defaults={"arcana": "MIDDLE", "suit": suit, "number": number, - "name": f"{_NAME[number]} of {suit.capitalize()}"}, + "name": f"{_NAME[number]} of {suit.capitalize()}", + "levity_qualifier": "Elevated", + "gravity_qualifier": "Graven"}, ) + # Numbers 0–1 are the sig deck's Major Arcana (unlocked via Note). + # Seed them with correct Earthman names and qualifiers, then unlock for all gamers. + from django.utils import timezone for number, name, slug in [ - (0, "The Schiz", "the-schiz-em"), - (1, "Pope 1: Chancellor", "pope-1-chancellor-em"), + (0, "The Nomad", "the-nomad"), + (1, "The Schizo", "the-schizo"), ]: TarotCard.objects.get_or_create( deck_variant=earthman, slug=slug, - defaults={"arcana": "MAJOR", "number": number, "name": name}, + defaults={"arcana": "MAJOR", "number": number, "name": name, + "levity_qualifier": "Enlightened", + "gravity_qualifier": "Engraven"}, ) + for slot in room.gate_slots.order_by("slot_number"): + if slot.gamer: + Note.objects.get_or_create( + user=slot.gamer, slug="super-nomad", + defaults={"earned_at": timezone.now()}, + ) + Note.objects.get_or_create( + user=slot.gamer, slug="super-schizo", + defaults={"earned_at": timezone.now()}, + ) for slot in room.gate_slots.order_by("slot_number"): if slot.gamer and not slot.gamer.equipped_deck: slot.gamer.equipped_deck = earthman @@ -304,8 +322,8 @@ class SigSelectThemeTest(FunctionalTest): # ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── # - def test_levity_non_major_card_shows_leavened_above(self): - """Hovering a non-major card in the levity overlay shows 'Leavened' in + def test_levity_non_major_card_shows_elevated_above(self): + """Hovering a non-major card in the levity overlay shows 'Elevated' in qualifier-above and nothing in qualifier-below.""" room = self._setup_sig_room() self.create_pre_authenticated_session("founder@test.io") # PC = levity @@ -317,12 +335,12 @@ class SigSelectThemeTest(FunctionalTest): above = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") ) - self.assertEqual(above.text, "Leavened") + self.assertEqual(above.text, "Elevated") below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") self.assertEqual(below.text, "") - def test_levity_major_card_shows_leavened_below(self): - """Hovering a major arcana card in the levity overlay shows 'Leavened' in + def test_levity_major_card_shows_enlightened_below(self): + """Hovering a major arcana card in the levity overlay shows 'Enlightened' in qualifier-below and nothing in qualifier-above.""" room = self._setup_sig_room() self.create_pre_authenticated_session("founder@test.io") # PC = levity @@ -334,7 +352,7 @@ class SigSelectThemeTest(FunctionalTest): below = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") ) - self.assertEqual(below.text, "Leavened") + self.assertEqual(below.text, "Enlightened") above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") self.assertEqual(above.text, "") diff --git a/src/functional_tests/test_trinket_carte_blanche.py b/src/functional_tests/test_trinket_carte_blanche.py index a49fc11..f6fc2fb 100644 --- a/src/functional_tests/test_trinket_carte_blanche.py +++ b/src/functional_tests/test_trinket_carte_blanche.py @@ -168,6 +168,7 @@ class CarteBlancheTest(FunctionalTest): self.wait_for( lambda: self.assertIn("/gate/", self.browser.current_url) ) + gate_url = self.browser.current_url self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay") ) @@ -217,24 +218,13 @@ class CarteBlancheTest(FunctionalTest): ) self.wait_for(lambda: get_circle(1).find_element(By.CSS_SELECTOR, ".drop-token-btn")) - # 12. Carte tooltip in kit bag shows room name (lease info) + # 12. Kit bag trinket section is now empty — Carte Blanche unequipped on deposit self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.browser.find_element( - By.CSS_SELECTOR, - f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', + By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder" ) ) - carte_in_bag = self.browser.find_element( - By.CSS_SELECTOR, f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]' - ) - # Kit bag tooltips are CSS-hidden; read textContent (not .text) to avoid - # relying on hover visibility in headless Firefox. - self.assertIn( - "The Long Room", - carte_in_bag.find_element(By.CSS_SELECTOR, ".tt").get_attribute("textContent"), - ) - # Close kit bag self.browser.find_element(By.ID, "id_kit_btn").click() self.wait_for( lambda: self.assertFalse( @@ -249,26 +239,35 @@ class CarteBlancheTest(FunctionalTest): len(self.browser.find_elements(By.CSS_SELECTOR, ".token-slot.claimed")), 0 ) ) - # Lease info cleared from kit bag tooltip - self.browser.find_element(By.ID, "id_kit_btn").click() - self.wait_for( - lambda: self.browser.find_element( - By.CSS_SELECTOR, - f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', - ) + # Carte reappears in Game Kit with DON available — not re-equipped automatically. + # Game Kit is only in the /gameboard/ context, not the gatekeeper page. + self.browser.get(self.live_server_url + "/gameboard/") + carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") + self.browser.execute_script( + "arguments[0].scrollIntoView({block:'center'})", carte_el ) - carte_in_bag = self.browser.find_element( - By.CSS_SELECTOR, f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]' + ActionChains(self.browser).move_to_element(carte_el).perform() + portal = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_tooltip_portal") ) - self.assertNotIn( - "The Long Room", - carte_in_bag.find_element(By.CSS_SELECTOR, ".tt").get_attribute("textContent"), + self.wait_for(lambda: self.assertTrue(portal.is_displayed())) + don = self.wait_for( + lambda: portal.find_element(By.CSS_SELECTOR, ".btn-equip") ) - self.browser.find_element(By.ID, "id_kit_btn").click() + self.assertNotIn("btn-disabled", don.get_attribute("class")) # ── COLD FEET RESOLVED: full six-slot run ──────────────────────────── - # 14. Re-deposit Carte + # 14. Re-equip Carte via portal DON, navigate back to gate + don.click() + self.wait_for( + lambda: self.assertIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), + ) + ) + self.browser.get(gate_url) + self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")) open_kit_and_select_carte() deposit_carte() @@ -331,3 +330,56 @@ class CarteBlancheTest(FunctionalTest): self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn") ) + + def test_carte_in_use_game_kit_shows_room_attribution(self): + """While Carte Blanche is deposited in a room, its Game Kit tooltip + shows 'In game: ' so the gamer knows where it's committed.""" + self.create_pre_authenticated_session("blanche@test.io") + self.browser.get(self.live_server_url + "/gameboard/") + self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) + + # DON the Carte Blanche via tooltip portal + carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche") + self.browser.execute_script( + "arguments[0].scrollIntoView({block:'center'})", carte_el + ) + ActionChains(self.browser).move_to_element(carte_el).perform() + portal = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_tooltip_portal") + ) + self.wait_for(lambda: self.assertTrue(portal.is_displayed())) + don = self.wait_for(lambda: portal.find_element(By.CSS_SELECTOR, ".btn-equip")) + don.click() + self.wait_for( + lambda: self.assertIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), + ) + ) + + # Create a room and deposit the Carte Blanche + self.browser.find_element(By.ID, "id_new_game_name").send_keys("Commitment Room") + self.browser.find_element(By.ID, "id_create_game_btn").click() + self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url)) + + self.browser.find_element(By.ID, "id_kit_btn").click() + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', + ) + ).click() + self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click() + self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") + ) + + # Game Kit panel is on /gameboard/, not the gate page — navigate back to check tooltip + self.browser.get(self.live_server_url + "/gameboard/") + carte_tt = self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_carte_blanche .tt") + ) + self.assertIn( + "Commitment Room", + carte_tt.get_attribute("textContent"), + )