from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet from apps.epic.models import DeckVariant, Room from apps.lyric.models import User class TarotAdminTest(FunctionalTest): """Admin can browse tarot cards by deck variant via Django admin.""" def setUp(self): super().setUp() from apps.epic.models import TarotCard # DeckVariant + TarotCard rows are flushed by TransactionTestCase — recreate self.earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) # Seed enough cards so admin filter shows a meaningful count # The "108 tarot cards" assertion relies on deck_variant.card_count reported # by the admin, not on actual row count (admin shows real rows, so we seed # representative cards — 3 are enough to reach "The Schiz" in the list) for number, name, slug, group, correspondence in [ (0, "The Schiz", "the-schiz-adm", "", "The Fool / Il Matto"), (1, "Pope I: President","pope-i-president-adm","The Popes", "The Magician / Il Bagatto"), (50, "The Eagle", "the-eagle-adm", "", "Judgement / L'Angelo"), ]: TarotCard.objects.get_or_create( deck_variant=self.earthman, slug=slug, defaults={ "name": name, "arcana": "MAJOR", "number": number, "group": group, "correspondence": correspondence, }, ) self.superuser = User.objects.create_superuser( email="admin@example.com", password="correct-password", ) def _login_to_admin(self): self.browser.get(self.live_server_url + "/admin/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_username")) self.browser.find_element(By.ID, "id_username").send_keys("admin@example.com") self.browser.find_element(By.ID, "id_password").send_keys("correct-password") self.browser.find_element(By.CSS_SELECTOR, "input[type=submit]").click() # ------------------------------------------------------------------ # # Test 1a — admin home lists Tarot cards + Deck variants under Epic # # ------------------------------------------------------------------ # def test_admin_epic_section_shows_tarot_cards_and_deck_variants(self): self._login_to_admin() body = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body")) self.assertIn("Tarot cards", body.text) self.assertIn("Deck variants", body.text) # ------------------------------------------------------------------ # # Test 1b — changelist shows deck variant filter sidebar # # ------------------------------------------------------------------ # def test_admin_tarot_card_list_shows_deck_variant_filter(self): self._login_to_admin() self.browser.get(self.live_server_url + "/admin/epic/tarotcard/") body = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body")) # Filter sidebar has a link for the Earthman deck self.assertIn("Earthman Deck", body.text) # Cards are listed — 3 seeded in setUp self.assertIn("3 tarot cards", body.text) # ------------------------------------------------------------------ # # Test 1c — Earthman card detail shows name, group, and correspondence # # ------------------------------------------------------------------ # def test_admin_earthman_card_detail_shows_group_and_correspondence(self): self._login_to_admin() self.browser.get(self.live_server_url + "/admin/epic/tarotcard/") self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body")) # The Schiz is the Earthman Fool (card 0) self.browser.find_element(By.LINK_TEXT, "The Schiz").click() body = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body")) self.assertIn("Major Arcana", body.text) # arcana dropdown self.assertIn("the-schiz-adm", body.text) # slug (readonly → rendered as text) self.assertIn("The Fool / Il Matto", body.text) # correspondence (readonly → text) class TarotDeckTest(FunctionalTest): """A room founder can view the tarot deck page and deal a Celtic Cross spread.""" def setUp(self): super().setUp() # DeckVariant + TarotCard rows are flushed by TransactionTestCase — recreate from apps.epic.models import TarotCard self.earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) # Seed 8 major cards — enough for a 6-card cross deal (with buffer) major_stubs = [ (0, "The Schiz", "the-schiz-ft"), (1, "Pope I: President", "pope-i-president-ft"), (2, "Pope II: Tsar", "pope-ii-tsar-ft"), (3, "Pope III: Chairman","pope-iii-chairman-ft"), (4, "Pope IV: Emperor", "pope-iv-emperor-ft"), (5, "Pope V: Chancellor","pope-v-chancellor-ft"), (10, "Wheel of Fortune", "wheel-of-fortune-em-ft"), (11, "The Junkboat", "the-junkboat-ft"), ] for number, name, slug in major_stubs: TarotCard.objects.get_or_create( deck_variant=self.earthman, slug=slug, defaults={"name": name, "arcana": "MAJOR", "number": number}, ) self.founder = User.objects.create(email="founder@test.io") # Signal sets equipped_deck to Earthman (now it exists) self.founder.refresh_from_db() self.room = Room.objects.create(name="Whispering Pines", owner=self.founder) # ------------------------------------------------------------------ # # Test 2 — tarot deck page reports 108 cards (Earthman default) # # ------------------------------------------------------------------ # def test_founder_can_reach_room_tarot_page_and_sees_full_deck(self): self.create_pre_authenticated_session("founder@test.io") self.browser.get( self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/" ) # Browser tab title confirms we're on the tarot page self.wait_for( lambda: self.assertIn("Tarot", self.browser.title) ) # Deck status shows all 108 Earthman cards remaining status = self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]") self.assertEqual(status.get_attribute("data-tarot-remaining"), "108") # ------------------------------------------------------------------ # # Test 3 — dealing a Celtic Cross spread shows 10 positioned cards # # ------------------------------------------------------------------ # def test_dealing_celtic_cross_spread_shows_ten_unique_cards(self): self.create_pre_authenticated_session("founder@test.io") self.browser.get( self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/" ) # Click the "Deal Celtic Cross" button self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]") ).click() # Six cross positions appear in the spread (staff positions filled via gameplay) positions = self.wait_for( lambda: self.browser.find_elements(By.CSS_SELECTOR, ".tarot-position") ) self.assertEqual(len(positions), 6) # Each position shows a card name and an orientation label names = set() for pos in positions: name = pos.find_element(By.CSS_SELECTOR, ".tarot-card-name").text orientation = pos.find_element(By.CSS_SELECTOR, ".tarot-card-orientation").text self.assertTrue(len(name) > 0, "Card name should not be empty") self.assertIn(orientation, ["Upright", "Reversed"]) names.add(name) # All 6 cards are unique self.assertEqual(len(names), 6, "All 6 drawn cards must be unique") # ------------------------------------------------------------------ # # Test 4 — deck count decreases after the spread is dealt # # ------------------------------------------------------------------ # def test_remaining_count_decreases_after_dealing_spread(self): self.create_pre_authenticated_session("founder@test.io") self.browser.get( self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/" ) self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]") ).click() # After dealing 6 cross cards from the 108-card Earthman deck, 102 remain remaining = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]") ) self.assertEqual(remaining.get_attribute("data-tarot-remaining"), "102") class GameKitDeckSelectionTest(FunctionalTest): """ Game Kit applet on gameboard shows available deck variants with hover tooltips and an equip/equipped state — following the same mini-tooltip pattern as trinket selection. Test scenario: the gamer's active deck is explicitly set to Fiorentine (non-default) in setUp, so we can exercise switching back to Earthman. Once DeckVariant model exists, replace the TODO stubs with real ORM calls. """ def setUp(self): super().setUp() 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", }, ) # DeckVariant rows are flushed by TransactionTestCase — recreate before # creating the user so the post_save signal can set equipped_deck = earthman. self.earthman, _ = DeckVariant.objects.get_or_create( slug="earthman", defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, ) self.fiorentine, _ = DeckVariant.objects.get_or_create( slug="fiorentine-minchiate", defaults={"name": "Fiorentine Minchiate", "card_count": 78, "is_default": False}, ) self.gamer = User.objects.create(email="gamer@deck.io") # Signal sets equipped_deck = earthman and unlocked_decks = [earthman]. # Explicitly grant fiorentine too, then switch equipped_deck to it so # the test can exercise switching back to Earthman. self.gamer.refresh_from_db() self.gamer.unlocked_decks.add(self.fiorentine) self.gamer.equipped_deck = self.fiorentine self.gamer.save(update_fields=["equipped_deck"]) # ------------------------------------------------------------------ # # Test 5 — Game Kit shows deck cards with correct equip/equipped state # # ------------------------------------------------------------------ # def test_game_kit_deck_cards_show_equip_state_and_switching_works(self): """ Gamer (currently on Fiorentine) visits gameboard, hovers over the Earthman deck — sees it is NOT equipped. Hovers to Fiorentine — sees it IS equipped. Hovers back to Earthman and clicks Equip. """ self.create_pre_authenticated_session("gamer@deck.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) # ── Hover over Earthman deck ────────────────────────────────────── earthman_el = self.wait_for( lambda: self.browser.find_element(By.ID, "id_kit_earthman_deck") ) self.browser.execute_script( "arguments[0].scrollIntoView({block: 'center'})", earthman_el ) ActionChains(self.browser).move_to_element(earthman_el).perform() # Main tooltip shows deck name and card count self.wait_for( lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) portal = self.browser.find_element(By.ID, "id_tooltip_portal") self.assertIn("Earthman", portal.text) self.assertIn("108", portal.text) # Mini tooltip shows Equip button — Earthman is NOT currently equipped mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) equip_btn = mini.find_element(By.CSS_SELECTOR, ".equip-deck-btn") self.assertEqual(equip_btn.text, "Equip Deck?") # ── Hover over Fiorentine Minchiate deck ───────────────────────── fiorentine_el = self.browser.find_element(By.ID, "id_kit_fiorentine_deck") self.browser.execute_script( "arguments[0].scrollIntoView({block: 'center'})", fiorentine_el ) ActionChains(self.browser).move_to_element(fiorentine_el).perform() self.wait_for( lambda: self.assertIn( "Fiorentine", self.browser.find_element(By.ID, "id_tooltip_portal").text, ) ) portal = self.browser.find_element(By.ID, "id_tooltip_portal") self.assertIn("78", portal.text) # Mini tooltip shows "Equipped" — Fiorentine is the active deck mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) self.assertIn("Equipped", mini.text) # ── Hover back to Earthman and click Equip ──────────────────────── ActionChains(self.browser).move_to_element(earthman_el).perform() self.wait_for( lambda: self.assertIn( "Earthman", self.browser.find_element(By.ID, "id_tooltip_portal").text, ) ) mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") self.wait_for(lambda: self.assertTrue(mini.is_displayed())) mini.find_element(By.CSS_SELECTOR, ".equip-deck-btn").click() # Both portals close after equip self.wait_for( lambda: self.assertFalse( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() ) ) # Game Kit data attribute now reflects Earthman's id game_kit = self.browser.find_element(By.ID, "id_game_kit") self.wait_for( lambda: self.assertNotEqual( game_kit.get_attribute("data-equipped-deck-id"), "" ) ) # ------------------------------------------------------------------ # # Test 6 — new user's Game Kit shows only the default Earthman deck # # ------------------------------------------------------------------ # def test_new_user_game_kit_shows_only_earthman_deck(self): """A fresh user's game kit contains only the Earthman deck card; the Fiorentine deck is not visible because it has not been unlocked.""" newcomer = User.objects.create(email="newcomer@deck.io") newcomer.unlocked_decks.add(self.earthman) self.create_pre_authenticated_session("newcomer@deck.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant") self.assertEqual(len(deck_cards), 1) 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, ) )