2026-03-24 21:07:01 -04:00
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-03-24 21:52:57 -04:00
|
|
|
# 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},
|
|
|
|
|
)
|
2026-03-24 21:07:01 -04:00
|
|
|
self.gamer = User.objects.create(email="gamer@deck.io")
|
2026-03-24 22:34:50 -04:00
|
|
|
# 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.
|
2026-03-24 21:52:57 -04:00
|
|
|
self.gamer.refresh_from_db()
|
2026-03-24 22:34:50 -04:00
|
|
|
self.gamer.unlocked_decks.add(self.fiorentine)
|
2026-03-24 21:52:57 -04:00
|
|
|
self.gamer.equipped_deck = self.fiorentine
|
|
|
|
|
self.gamer.save(update_fields=["equipped_deck"])
|
2026-03-24 21:07:01 -04:00
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# 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"), ""
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-03-24 22:34:50 -04:00
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# 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)
|