- 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>
297 lines
14 KiB
Python
297 lines
14 KiB
Python
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")
|