Files
python-tdd/src/functional_tests/test_gameboard.py
Disco DeDisco db9ac9cb24
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
GAME KIT: DON|DOFF equip system — portal tooltips, kit bag sync, btn-disabled fix
- DON/DOFF buttons on left edge of game kit applet portal tooltip (mirroring FLIP/FYI)
- equip-trinket/unequip-trinket/equip-deck/unequip-deck views + URLs
- Portal stays open after DON/DOFF; buttons swap state in-place (_setEquipState)
- _syncTokenButtons: updates all .tt DON/DOFF buttons after equip state change
- _syncKitBagDialog (DOFF): replaces card with grayed placeholder icon in-place
- _refreshKitDialog (DON): re-fetches kit content so newly-equipped card appears immediately
- kit-content-refreshed event: game-kit.js re-attaches card listeners after re-fetch
- Bounding box expanded 24px left so buttons at portal edge don't trigger close
- mini-portal pinned with right (not left) so text width changes grow/shrink leftward
- btn-disabled moved dead last in .btn block — wins by source order, no !important needed
- Kit bag panel: trinket + token sections always render (placeholder when empty)
- Backstage Pass in GameKitEquipTest setUp (is_staff, natural unequipped state)
- Portal padding 0.75rem / 1.5rem; tt-description/shoptalk smaller; tt-expiry --priRd
- Wallet tokens CSS hover rule for .tt removed (portal-only now)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 00:14:47 -04:00

297 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from selenium.common.exceptions import NoSuchElementException
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 Token, User
class GameboardNavigationTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
def test_footer_links_to_gameboard(self):
# 1. Log in, nav to dashboard
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url)
# 2. Assert footer nav present w. dash- & gameboard tabs
self.browser.find_element(By.ID, "id_footer_nav")
self.browser.find_element(By.CSS_SELECTOR, '#id_footer_nav a[href="/"]')
self.browser.find_element(By.CSS_SELECTOR, '#id_footer_nav a[href="/gameboard/"]')
# 3. Click the gameboard tab
self.browser.find_element(
By.CSS_SELECTOR, '#id_footer_nav a[href="/gameboard/"]'
).click()
# 4. Assert user landed on gameboard
self.wait_for(
lambda: self.assertRegex(self.browser.current_url, r"/gameboard/$")
)
def test_gameboard_shows_game_applets(self):
# 1. Log in, nav directly to gameboard
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
# 2. Assert My Games applet present
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
)
# 3. Assert no games listed yet for new user
my_games = self.browser.find_element(By.ID, "id_applet_my_games")
game_items = my_games.find_elements(By.CSS_SELECTOR, ".game-item")
self.assertEqual(len(game_items), 0)
# 4. Assert New Game applet present
self.browser.find_element(By.ID, "id_applet_new_game")
def test_game_kit_panel_shows_token_inventory(self):
# 1. Log in, nav to gameboard
self.create_pre_authenticated_session("capman@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
# 2. Assert game kit applet & gear btn present
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_game_kit")
)
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn")
# 3. Assert Coin-on-a-String present in kit
coin = self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
self.browser.execute_script("arguments[0].scrollIntoView({block: 'center'});", coin)
# 6. Hover over it; assert tooltip shows name, entry text & reuse description
ActionChains(self.browser).move_to_element(coin).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
self.assertIn("Coin-on-a-String", coin_tooltip)
self.assertIn("Admit 1 Entry", coin_tooltip)
self.assertIn("and another after that", coin_tooltip)
# 7. Assert 1× Free Token (complimentary) present in kit
free_token = self.browser.find_element(By.ID, "id_kit_free_token")
# 8. Hover over it; assert tooltip shows name, entry text & expiry date
ActionChains(self.browser).move_to_element(free_token).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
self.assertIn("Free Token", free_tooltip)
self.assertIn("Admit 1 Entry", free_tooltip)
self.assertIn("Expires", free_tooltip)
# 9. Assert card deck & dice set placeholder present
self.browser.find_element(By.ID, "id_kit_card_deck")
self.browser.find_element(By.ID, "id_kit_dice_set")
class GameboardAppletMenuTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
self.create_pre_authenticated_session("gamer@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
def test_user_can_toggle_applet_visibility_via_gear_menu(self):
# 1. Assert both applets present
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
)
self.browser.find_element(By.ID, "id_applet_new_game")
# 2. Click gear; wait for menu
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
)
)
# 3. Find checkboxes; assert both checked
menu = self.browser.find_element(By.ID, "id_game_applet_menu")
my_games_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="my-games"]')
new_game_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="new-game"]')
self.assertTrue(my_games_cb.is_selected())
self.assertTrue(new_game_cb.is_selected())
# 4. Uncheck my-games; plant no-reload marker; submit
my_games_cb.click()
self.assertFalse(my_games_cb.is_selected())
self.browser.execute_script("window.__no_reload_marker = true")
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
# 5. Wait for menu to close; assert my-games gone, new game remains
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
)
)
self.wait_for(
lambda: self.assertRaises(
NoSuchElementException,
self.browser.find_element,
By.ID, "id_applet_my_games",
)
)
self.browser.find_element(By.ID, "id_applet_new_game")
# 6. Re-check my-games; assert it reappears
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
)
)
menu = self.browser.find_element(By.ID, "id_game_applet_menu")
my_games_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="my-games"]')
self.assertFalse(my_games_cb.is_selected())
my_games_cb.click()
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
)
)
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_applet_my_games")
)
)
# 7. Assert no full page reload occurred
self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true"))
class GameKitEquipTest(FunctionalTest):
"""DON|DOFF equip buttons in the game kit applet portal tooltip."""
def setUp(self):
super().setUp()
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
self.create_pre_authenticated_session("gamer@equip.io")
self.gamer = User.objects.get(email="gamer@equip.io")
# Promote to staff so the pass_token appears in the game kit applet.
self.gamer.is_staff = True
self.gamer.save(update_fields=["is_staff"])
self.gamer.unlocked_decks.add(self.earthman)
self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first()
# Create a PASS token manually — starts unequipped (coin is auto-equipped).
self.pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
self.browser.set_window_size(1200, 900)
self.browser.get(self.live_server_url + "/gameboard/")
def _hover_game_kit_token(self, token_el):
"""Hover token, wait for portal, return portal element."""
ActionChains(self.browser).move_to_element(token_el).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
return self.browser.find_element(By.ID, "id_tooltip_portal")
def test_unequipped_token_shows_don_active_doff_disabled(self):
"""Backstage Pass — naturally unequipped (coin is auto-equipped): DON active, DOFF is ×."""
pass_el = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_pass")
)
portal = self._hover_game_kit_token(pass_el)
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip")
self.assertNotIn("btn-disabled", don.get_attribute("class"))
self.assertIn("btn-disabled", doff.get_attribute("class"))
self.assertEqual(doff.text, "×")
def test_equipped_token_shows_doff_active_don_disabled(self):
"""Auto-equipped coin: DOFF active, DON is ×; mini-portal says Equipped."""
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip")
self.assertIn("btn-disabled", don.get_attribute("class"))
self.assertEqual(don.text, "×")
self.assertNotIn("btn-disabled", doff.get_attribute("class"))
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
self.assertEqual(mini.text, "Equipped")
self.assertEqual(len(mini.find_elements(By.CSS_SELECTOR, "button")), 0)
def test_doff_then_don_roundtrip(self):
"""Full roundtrip: DOFF unequips (portal stays open, buttons swap, mini updates);
DON re-equips (buttons swap back, mini updates back, DB confirms)."""
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
# — DOFF —
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click()
self.wait_for(
lambda: self.assertIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"),
)
)
self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-unequip").text, "×")
self.assertNotIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
)
self.assertEqual(
self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Not Equipped"
)
self.gamer.refresh_from_db()
self.assertIsNone(self.gamer.equipped_trinket)
# — DON —
portal.find_element(By.CSS_SELECTOR, ".btn-equip").click()
self.wait_for(
lambda: self.assertIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
)
)
self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-equip").text, "×")
self.assertNotIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"),
)
self.assertEqual(
self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Equipped"
)
self.gamer.refresh_from_db()
self.assertEqual(self.gamer.equipped_trinket, self.coin)
def test_doff_updates_open_kit_bag_dialog(self):
"""DOFF from game kit applet replaces trinket card in currently-open kit bag dialog."""
self.gamer.equipped_trinket = self.coin
self.gamer.save(update_fields=["equipped_trinket"])
self.browser.refresh()
# Open kit bag dialog
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-id="{self.coin.id}"]',
)
)
# While dialog is open, hover coin and DOFF from game kit
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click()
# Dialog updates: trinket card replaced with placeholder
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR,
f'#id_kit_bag_dialog [data-token-id="{self.coin.id}"]',
)), 0
)
)
self.browser.find_element(By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder")