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:
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user