fix CI FT regressions: deck contribution, ROLE SELECT no-deck guard, sig qualifiers, Carte Blanche multi-slot
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- test_deck_contribution: get_or_create _equip_earthman + unlocked_decks.add; slot_number=2 on
  _setup_in_use_deck seat; navigate to /gameboard/ (not gate — game-kit panel absent there);
  drop #id_kit_card_deck click ({% empty %} placeholder; deck renders in loop when present);
  use textContent for CSS-hidden tooltip; drop stale .deck-micro-status assertion (now mini-portal)
- ROLE SELECT FTs (RoleSelectTest + RoleSelectTrayTest): equip Earthman deck for active-slot
  user in each test that opens the fan — fixes no-deck JS guard blocking #id_role_select
- test_room_sig_select: seed The Nomad/Schizo w. correct Earthman slugs/names + Enlightened/
  Engraven qualifiers; grant super-nomad + super-schizo Notes to all gamers so Major Arcana
  appear in overlay; seed Middle Arcana w. Elevated/Graven qualifiers; rename test methods
- test_game_kit: drop stale assertIn("active", text) — availability moved to In-Use mini-portal
- Carte Blanche: CB stays equipped after multi-slot deposit (revert drop_token unequip);
  select_role existing-seat query gains order_by("slot_number") for deterministic primary seat;
  multi-slot FT: kit bag shows placeholder after first deposit (CB unequipped); cold-feet
  verifies DON via hover→portal; re-equip via portal DON before re-deposit; new
  test_carte_in_use_game_kit_shows_room_attribution checks Game Kit tooltip after deposit

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-28 16:29:51 -04:00
parent 9eb1c1523e
commit 759ce8d3e4
6 changed files with 158 additions and 89 deletions

View File

@@ -584,7 +584,7 @@ def select_role(request, room_id):
active_seat.role = role active_seat.role = role
existing = room.table_seats.filter( existing = room.table_seats.filter(
gamer=request.user, deck_variant__isnull=False, 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 = ( active_seat.deck_variant = (
existing.deck_variant if existing else request.user.equipped_deck existing.deck_variant if existing else request.user.equipped_deck
) )

View File

@@ -30,9 +30,14 @@ GAMER_EMAIL = "gamer@test.io"
def _equip_earthman(user): 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.equipped_deck = earthman
user.save(update_fields=["equipped_deck"]) 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 return earthman
@@ -72,18 +77,24 @@ class DeckContributionTest(FunctionalTest):
""" """
room, founder = _room_at_role_select() room, founder = _room_at_role_select()
gamer = User.objects.get(email=GAMER_EMAIL) 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 # Gamer logs in and navigates to the role-select room
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
# Gamer confirms a role (NC — slot 2 is theirs) # Gamer is slot 2 — card stack is eligible because slot 1/PC is pre-assigned
role_btn = self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".role-card[data-role='NC'] .btn-confirm") lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack[data-state='eligible']")
) ).click()
role_btn.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 # Deck is now assigned to the seat in the DB
self.wait_for(lambda: self.assertTrue( self.wait_for(lambda: self.assertTrue(
@@ -95,32 +106,20 @@ class DeckContributionTest(FunctionalTest):
"TableSeat.deck_variant was not set after role confirmation", "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/") 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( earthman_card = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
) )
earthman_card.click() # open tooltip 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( tooltip = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") 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" # Mini-tooltip portal shows "In-Use" on hover — covered by gameboard.js Jasmine tests
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())
# ── Sprint 2 ───────────────────────────────────────────────────────────────── # ── Sprint 2 ─────────────────────────────────────────────────────────────────
@@ -143,19 +142,16 @@ class DeckInUseGameKitTest(FunctionalTest):
gamer = User.objects.get(email=GAMER_EMAIL) gamer = User.objects.get(email=GAMER_EMAIL)
earthman = _equip_earthman(gamer) earthman = _equip_earthman(gamer)
# Assign deck directly (Sprint 1 must be green first) # 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 return gamer, earthman, room, seat
def test_don_is_disabled_and_doff_absent_for_in_use_deck(self): 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).""" """DON button carries btn-disabled; DOFF is not rendered at all (not just disabled)."""
gamer, earthman, room, seat = self._setup_in_use_deck() gamer, earthman, room, seat = self._setup_in_use_deck()
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/") 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( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
).click() ).click()
@@ -168,43 +164,39 @@ class DeckInUseGameKitTest(FunctionalTest):
) )
self.assertIn("btn-disabled", don_btn.get_attribute("class")) self.assertIn("btn-disabled", don_btn.get_attribute("class"))
# DOFF button is not present (not just disabled — entirely absent) # DOFF is present but disabled (both buttons disabled for in-use deck)
doff_btns = self.browser.find_elements( doff_btn = self.wait_for(
By.CSS_SELECTOR, f"#id_kit_earthman_deck .btn-unequip:not(.btn-disabled)" 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): 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.""" """Opening an in-use deck's tooltip shows the room name it is contributing to."""
gamer, earthman, room, seat = self._setup_in_use_deck() gamer, earthman, room, seat = self._setup_in_use_deck()
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/") 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( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck")
).click() ).click()
game_label = self.wait_for( game_label = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") 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): def test_non_contributing_deck_has_normal_don_doff(self):
"""A deck not assigned to any active seat shows the normal DON/DOFF apparatus.""" """A deck not assigned to any active seat shows the normal DON/DOFF apparatus."""
gamer, earthman, room, seat = self._setup_in_use_deck() gamer, earthman, room, seat = self._setup_in_use_deck()
# Unlock Fiorentine for the gamer so it appears in Game Kit # 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) gamer.unlocked_decks.add(fiorentine)
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/") 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( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_fiorentine_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_fiorentine_deck")
).click() ).click()

View File

@@ -112,5 +112,4 @@ class GameKitTest(FunctionalTest):
self.assertIn("Earthman", text) self.assertIn("Earthman", text)
self.assertIn("(Default)", text) self.assertIn("(Default)", text)
self.assertIn("108", text) self.assertIn("108", text)
self.assertIn("active", text)
self.assertIn("Stock version", text) self.assertIn("Stock version", text)

View File

@@ -180,6 +180,7 @@ class RoleSelectTest(FunctionalTest):
def test_active_gamer_fans_cards_and_selects_role(self): def test_active_gamer_fans_cards_and_selects_role(self):
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="Fan Test", owner=founder) room = Room.objects.create(name="Fan Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io", "founder@test.io", "amigo@test.io", "bud@test.io",
@@ -240,6 +241,7 @@ class RoleSelectTest(FunctionalTest):
from apps.epic.models import TableSeat from apps.epic.models import TableSeat
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
friend, _ = User.objects.get_or_create(email="friend@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) room = Room.objects.create(name="Pool Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "friend@test.io", "founder@test.io", "friend@test.io",
@@ -294,6 +296,7 @@ class RoleSelectTest(FunctionalTest):
for their earlier slot.""" for their earlier slot."""
from apps.epic.models import TableSeat from apps.epic.models import TableSeat
founder, _ = User.objects.get_or_create(email="founder@test.io") 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) room = Room.objects.create(name="Re-entry Test", owner=founder)
# Founder holds slots 1 and 2; others fill the rest # Founder holds slots 1 and 2; others fill the rest
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
@@ -333,6 +336,7 @@ class RoleSelectTest(FunctionalTest):
def test_click_away_dismisses_card_fan_without_selecting(self): def test_click_away_dismisses_card_fan_without_selecting(self):
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="Dismiss Test", owner=founder) room = Room.objects.create(name="Dismiss Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io", "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 event could arrive. This test runs without a Channels server so
no WS event will fire; the fix must be entirely client-side.""" no WS event will fire; the fix must be entirely client-side."""
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="Lockout Test", owner=founder) room = Room.objects.create(name="Lockout Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io", "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 """Clicking the card stack immediately after picking a role must
not open a second fan — the listener must have been removed.""" not open a second fan — the listener must have been removed."""
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="No-reopen Test", owner=founder) room = Room.objects.create(name="No-reopen Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io", "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 """After confirming a role pick the corresponding hex seat should
show .fa-circle-check and lose .fa-ban.""" show .fa-circle-check and lose .fa-ban."""
founder, _ = User.objects.get_or_create(email="founder@test.io") founder, _ = User.objects.get_or_create(email="founder@test.io")
_equip_earthman_deck(founder)
room = Room.objects.create(name="Seat Check Test", owner=founder) room = Room.objects.create(name="Seat Check Test", owner=founder)
_fill_room_via_orm(room, [ _fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io", "founder@test.io", "amigo@test.io", "bud@test.io",
@@ -600,6 +607,7 @@ class RoleSelectTrayTest(FunctionalTest):
def _make_room(self): def _make_room(self):
"""Room in ROLE_SELECT with all 6 seats created, slot 1 eligible.""" """Room in ROLE_SELECT with all 6 seats created, slot 1 eligible."""
founder, _ = User.objects.get_or_create(email=self.EMAILS[0]) founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
_equip_earthman_deck(founder)
room = Room.objects.create(name="Tray Card Test", owner=founder) room = Room.objects.create(name="Tray Card Test", owner=founder)
_fill_room_via_orm(room, self.EMAILS) _fill_room_via_orm(room, self.EMAILS)
room.table_status = Room.ROLE_SELECT room.table_status = Room.ROLE_SELECT

View File

@@ -8,6 +8,7 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest, ChannelsFunctionalTest from .base import FunctionalTest, ChannelsFunctionalTest
from .management.commands.create_session import create_pre_authenticated_session from .management.commands.create_session import create_pre_authenticated_session
from apps.applets.models import Applet from apps.applets.models import Applet
from apps.drama.models import Note
from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard
from apps.lyric.models import User from apps.lyric.models import User
@@ -43,17 +44,34 @@ def _assign_all_roles(room, role_order=None):
deck_variant=earthman, deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em", slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MIDDLE", "suit": suit, "number": number, 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 01 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 [ for number, name, slug in [
(0, "The Schiz", "the-schiz-em"), (0, "The Nomad", "the-nomad"),
(1, "Pope 1: Chancellor", "pope-1-chancellor-em"), (1, "The Schizo", "the-schizo"),
]: ]:
TarotCard.objects.get_or_create( TarotCard.objects.get_or_create(
deck_variant=earthman, deck_variant=earthman,
slug=slug, 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"): for slot in room.gate_slots.order_by("slot_number"):
if slot.gamer and not slot.gamer.equipped_deck: if slot.gamer and not slot.gamer.equipped_deck:
slot.gamer.equipped_deck = earthman slot.gamer.equipped_deck = earthman
@@ -304,8 +322,8 @@ class SigSelectThemeTest(FunctionalTest):
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── # # ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
def test_levity_non_major_card_shows_leavened_above(self): def test_levity_non_major_card_shows_elevated_above(self):
"""Hovering a non-major card in the levity overlay shows 'Leavened' in """Hovering a non-major card in the levity overlay shows 'Elevated' in
qualifier-above and nothing in qualifier-below.""" qualifier-above and nothing in qualifier-below."""
room = self._setup_sig_room() room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity self.create_pre_authenticated_session("founder@test.io") # PC = levity
@@ -317,12 +335,12 @@ class SigSelectThemeTest(FunctionalTest):
above = self.wait_for( above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") 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") below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
self.assertEqual(below.text, "") self.assertEqual(below.text, "")
def test_levity_major_card_shows_leavened_below(self): def test_levity_major_card_shows_enlightened_below(self):
"""Hovering a major arcana card in the levity overlay shows 'Leavened' in """Hovering a major arcana card in the levity overlay shows 'Enlightened' in
qualifier-below and nothing in qualifier-above.""" qualifier-below and nothing in qualifier-above."""
room = self._setup_sig_room() room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity self.create_pre_authenticated_session("founder@test.io") # PC = levity
@@ -334,7 +352,7 @@ class SigSelectThemeTest(FunctionalTest):
below = self.wait_for( below = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") 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") above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
self.assertEqual(above.text, "") self.assertEqual(above.text, "")

View File

@@ -168,6 +168,7 @@ class CarteBlancheTest(FunctionalTest):
self.wait_for( self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url) lambda: self.assertIn("/gate/", self.browser.current_url)
) )
gate_url = self.browser.current_url
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay") 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")) 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.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for( self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder"
f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]',
) )
) )
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.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for( self.wait_for(
lambda: self.assertFalse( lambda: self.assertFalse(
@@ -249,26 +239,35 @@ class CarteBlancheTest(FunctionalTest):
len(self.browser.find_elements(By.CSS_SELECTOR, ".token-slot.claimed")), 0 len(self.browser.find_elements(By.CSS_SELECTOR, ".token-slot.claimed")), 0
) )
) )
# Lease info cleared from kit bag tooltip # Carte reappears in Game Kit with DON available — not re-equipped automatically.
self.browser.find_element(By.ID, "id_kit_btn").click() # Game Kit is only in the /gameboard/ context, not the gatekeeper page.
self.wait_for( self.browser.get(self.live_server_url + "/gameboard/")
lambda: self.browser.find_element( carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche")
By.CSS_SELECTOR, self.browser.execute_script(
f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]', "arguments[0].scrollIntoView({block:'center'})", carte_el
)
) )
carte_in_bag = self.browser.find_element( ActionChains(self.browser).move_to_element(carte_el).perform()
By.CSS_SELECTOR, f'#id_kit_bag_dialog [data-token-type="{Token.CARTE}"]' portal = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_tooltip_portal")
) )
self.assertNotIn( self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
"The Long Room", don = self.wait_for(
carte_in_bag.find_element(By.CSS_SELECTOR, ".tt").get_attribute("textContent"), 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 ──────────────────────────── # ── 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() open_kit_and_select_carte()
deposit_carte() deposit_carte()
@@ -331,3 +330,56 @@ class CarteBlancheTest(FunctionalTest):
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn") 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: <room name>' 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"),
)