game kit page: four 6×3 applets (trinkets, tokens, card decks, dice sets) with applet grid; tarot fan modal with coverflow, sessionStorage position memory, and 403 guard on locked decks; unlocked_decks M2M on User with backfill migration; game kit icon wrap fix; tarot_deck.html moved to gameboard/ per template dir convention (now documented in CLAUDE.md); FTs 6–13, 2 new ITs; 360 passing [log Co-Authored-By: Claude Sonnet 4.6]

This commit is contained in:
Disco DeDisco
2026-03-24 22:57:12 -04:00
parent b03ba09b65
commit a5d71925fc
10 changed files with 580 additions and 1 deletions

View File

@@ -342,3 +342,163 @@ class GameKitDeckSelectionTest(FunctionalTest):
self.browser.find_element(By.ID, "id_kit_earthman_deck")
fiorentine_cards = self.browser.find_elements(By.ID, "id_kit_fiorentine_deck")
self.assertEqual(len(fiorentine_cards), 0)
class GameKitPageTest(FunctionalTest):
"""
User navigates from gameboard to the dedicated game-kit page.
The page shows four rows: trinkets, tokens, card decks, dice placeholder.
Clicking a deck card opens a tarot fan modal with coverflow navigation.
"""
def setUp(self):
super().setUp()
from apps.epic.models import TarotCard
for slug, name, cols, rows in [
("new-game", "New Game", 6, 3),
("my-games", "My Games", 6, 3),
("game-kit", "Game Kit", 6, 3),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"},
)
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
# Seed 10 cards — enough to demonstrate full 7-card coverflow
for i in range(10):
TarotCard.objects.get_or_create(
deck_variant=self.earthman,
slug=f"gkp-card-{i}",
defaults={"name": f"Card {i}", "arcana": "MAJOR", "number": i},
)
# Create user after decks so signal sets equipped_deck + unlocked_decks
self.gamer = User.objects.create(email="gamer@kit.io")
self.gamer.refresh_from_db()
self.create_pre_authenticated_session("gamer@kit.io")
# ------------------------------------------------------------------ #
# Test 7 — gameboard Game Kit heading links to dedicated page #
# ------------------------------------------------------------------ #
def test_gameboard_game_kit_heading_links_to_game_kit_page(self):
self.browser.get(self.live_server_url + "/gameboard/")
link = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_game_kit h2 a")
)
link.click()
self.wait_for(lambda: self.assertIn("/gameboard/game-kit/", self.browser.current_url))
# ------------------------------------------------------------------ #
# Test 8 — game-kit page shows four rows #
# ------------------------------------------------------------------ #
def test_game_kit_page_shows_four_rows(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_gk_trinkets"))
self.browser.find_element(By.ID, "id_gk_tokens")
self.browser.find_element(By.ID, "id_gk_decks")
self.browser.find_element(By.ID, "id_gk_dice")
# ------------------------------------------------------------------ #
# Test 9 — clicking a deck card opens the tarot fan modal #
# ------------------------------------------------------------------ #
def test_clicking_deck_opens_tarot_fan_modal(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
).click()
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
# ------------------------------------------------------------------ #
# Test 10 — fan shows active center card plus receding cards #
# ------------------------------------------------------------------ #
def test_fan_shows_active_card_and_receding_cards(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
).click()
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
visible = self.browser.find_elements(
By.CSS_SELECTOR, "#id_fan_content .fan-card:not([style*='display: none'])"
)
self.assertGreater(len(visible), 1)
# ------------------------------------------------------------------ #
# Test 11 — next button advances the active card #
# ------------------------------------------------------------------ #
def test_fan_next_button_advances_card(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
).click()
first_index = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active")
).get_attribute("data-index")
self.browser.find_element(By.ID, "id_fan_next").click()
self.wait_for(
lambda: self.assertNotEqual(
self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index"),
first_index,
)
)
# ------------------------------------------------------------------ #
# Test 12 — clicking outside the modal closes it #
# ------------------------------------------------------------------ #
def test_clicking_outside_fan_closes_modal(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
).click()
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
# Dispatch a click directly on the dialog element (simulates clicking the dark backdrop)
self.browser.execute_script(
"document.getElementById('id_tarot_fan_dialog')"
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
)
self.wait_for(lambda: self.assertFalse(dialog.is_displayed()))
# ------------------------------------------------------------------ #
# Test 13 — reopening the modal remembers scroll position #
# ------------------------------------------------------------------ #
def test_fan_remembers_position_on_reopen(self):
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
deck_card = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
)
deck_card.click()
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
# Advance 3 cards
for _ in range(3):
self.browser.find_element(By.ID, "id_fan_next").click()
saved_index = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index")
)
# Close
self.browser.execute_script(
"document.getElementById('id_tarot_fan_dialog')"
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
)
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_tarot_fan_dialog").is_displayed()
)
)
# Reopen and verify position restored
deck_card.click()
self.wait_for(
lambda: self.assertEqual(
self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index"),
saved_index,
)
)