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
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
)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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 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 [
(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, "")

View File

@@ -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: <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"),
)