tray cards: shadow, hover-tilt w. focus persistence, role-card tooltip — TDD
- _tray.scss: drop-shadow on cell child elements (img → filter:drop-shadow so the silhouette is the shadow caster, div → box-shadow); 7° hover-tilt on .tray-role-card > img (-7°) and .tray-sig-card > .sig-stage-card (+7° via the standalone `rotate` property so the existing -5° baseline transform composes); :focus persists the tilt after click; cursor: pointer
- tray.js: set tabIndex=0 on placeCard's role cell + on template-rendered .tray-role-card / .tray-sig-card cells at init() so :focus latches the hover state; clear tabindex in reset() for Jasmine afterEach
- TraySpec: 4 new specs covering placeCard tabindex, reset cleanup, init-time tabindex on template-rendered sig & role cards, no-tabindex on bare cells
- New tray-tooltip.js (#id_tooltip_portal) — Phase 1 of the apps.tooltips integration: hovering .tray-role-card > img copies its sibling .tt's innerHTML into the page-root portal, anchors above/below the trigger, & clamps to the viewport horizontally; mousemove outside the union of [trigger, portal] rects clears the portal (Game-Kit pattern, no btns)
- room.html: #id_tooltip_portal mounted at room-page root (outside tray's overflow:hidden); .tt block rendered inline inside .tray-role-card via {% tooltip %} templatetag w. title=role display name & description="[Placeholder description]"
- epic/views.py: my_tray_role_tooltip context dict ({title, description}) keyed off the seated role
- TrayTooltipSpec: 8 specs covering portal population, .active class, sibling-.tt fallback, viewport-edge clamp left/right, and union-rect mouseleave
- 2 FTs in test_component_tray_tooltip.py: hover role img → portal title=Player + description=Placeholder; mouseleave → portal clears
Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel via a DRY refactor) deferred per plan.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
src/functional_tests/test_component_tray_tooltip.py
Normal file
163
src/functional_tests/test_component_tray_tooltip.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Component FT — tray tooltip (Phase 1: role-card img).
|
||||
|
||||
Hovering the tray's role-card <img> populates #id_tooltip_portal with the
|
||||
role's display name as the title and a description, matching the existing
|
||||
Game-Kit tooltip portal pattern (apps.tooltips). Mousing well off the card
|
||||
clears the portal.
|
||||
|
||||
Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel) lives in a separate
|
||||
test class and is not yet covered here.
|
||||
"""
|
||||
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, GateSlot, Room, TableSeat
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
def _equip_earthman_deck(user):
|
||||
deck, _ = DeckVariant.objects.get_or_create(
|
||||
slug="earthman",
|
||||
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
|
||||
)
|
||||
user.equipped_deck = deck
|
||||
user.save(update_fields=["equipped_deck"])
|
||||
|
||||
|
||||
def _fill_room_via_orm(room, emails):
|
||||
for i, email in enumerate(emails, start=1):
|
||||
gamer, _ = User.objects.get_or_create(email=email)
|
||||
slot = room.gate_slots.get(slot_number=i)
|
||||
slot.gamer = gamer
|
||||
slot.status = GateSlot.FILLED
|
||||
slot.save()
|
||||
room.gate_status = Room.OPEN
|
||||
room.save()
|
||||
|
||||
|
||||
class TrayRoleCardTooltipTest(FunctionalTest):
|
||||
EMAILS = [
|
||||
"tt1@test.io", "tt2@test.io", "tt3@test.io",
|
||||
"tt4@test.io", "tt5@test.io", "tt6@test.io",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.browser.set_window_size(800, 1200)
|
||||
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"}
|
||||
)
|
||||
|
||||
def _make_room_with_role(self, role="PC"):
|
||||
"""Room in ROLE_SELECT with founder seated and assigned `role`."""
|
||||
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
|
||||
_equip_earthman_deck(founder)
|
||||
room = Room.objects.create(name="Tooltip Room", owner=founder)
|
||||
_fill_room_via_orm(room, self.EMAILS)
|
||||
room.table_status = Room.ROLE_SELECT
|
||||
room.save()
|
||||
for slot in room.gate_slots.order_by("slot_number"):
|
||||
TableSeat.objects.create(
|
||||
room=room, gamer=slot.gamer, slot_number=slot.slot_number,
|
||||
role=(role if slot.slot_number == 1 else None),
|
||||
)
|
||||
return room
|
||||
|
||||
def _open_tray(self):
|
||||
# Tray is closed off-screen by default. Open it AND wait for the slide
|
||||
# transition (left: 736px → 0px, ~0.35s) to land before hovering;
|
||||
# otherwise getBoundingClientRect returns the closed-state rect that
|
||||
# ActionChains sees as off-viewport.
|
||||
self.browser.execute_script("Tray.open();")
|
||||
self.wait_for(
|
||||
lambda: self.assertLess(
|
||||
self.browser.execute_script(
|
||||
"return document.getElementById('id_tray_wrap').getBoundingClientRect().left"
|
||||
),
|
||||
40,
|
||||
"tray wrap should slide to ~0",
|
||||
)
|
||||
)
|
||||
|
||||
def _hover(self, element):
|
||||
ActionChains(self.browser).move_to_element(element).perform()
|
||||
|
||||
def _move_far_away(self):
|
||||
# Synthesize a mousemove event well outside the trigger+portal union.
|
||||
# ActionChains origins require a visible element; firing the event
|
||||
# directly via JS sidesteps that and exercises the same listener path.
|
||||
self.browser.execute_script(
|
||||
"document.dispatchEvent(new MouseEvent('mousemove',"
|
||||
"{bubbles:true, clientX:9999, clientY:9999}));"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T1 — Hover role img → portal shows title (role display name) #
|
||||
# + description ("[Placeholder description]"). #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_hover_role_img_populates_tooltip_portal(self):
|
||||
room = self._make_room_with_role(role="PC")
|
||||
self.create_pre_authenticated_session(self.EMAILS[0])
|
||||
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")
|
||||
|
||||
self._open_tray()
|
||||
# The role card is rendered server-side as the first tray cell.
|
||||
role_img = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card img"
|
||||
)
|
||||
)
|
||||
|
||||
# Portal exists at page root, hidden initially.
|
||||
portal = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||
)
|
||||
self.assertFalse(
|
||||
portal.is_displayed(),
|
||||
"tooltip portal should start hidden before any hover",
|
||||
)
|
||||
|
||||
# Hover the role art → portal becomes visible with title + description.
|
||||
self._hover(role_img)
|
||||
self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
|
||||
|
||||
title = portal.find_element(By.CSS_SELECTOR, ".tt-title").text
|
||||
self.assertEqual(title.strip(), "Player") # PC → "Player"
|
||||
|
||||
description = portal.find_element(By.CSS_SELECTOR, ".tt-description").text
|
||||
self.assertEqual(description.strip(), "[Placeholder description]")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T2 — Mousing well off the card clears the portal. #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_mouseleave_clears_tooltip_portal(self):
|
||||
room = self._make_room_with_role(role="EC")
|
||||
self.create_pre_authenticated_session(self.EMAILS[0])
|
||||
self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")
|
||||
|
||||
self._open_tray()
|
||||
role_img = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tray_grid .tray-role-card img"
|
||||
)
|
||||
)
|
||||
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||
|
||||
self._hover(role_img)
|
||||
self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
|
||||
# Title reflects the EC mapping.
|
||||
self.assertEqual(
|
||||
portal.find_element(By.CSS_SELECTOR, ".tt-title").text.strip(),
|
||||
"Economist",
|
||||
)
|
||||
|
||||
self._move_far_away()
|
||||
self.wait_for(lambda: self.assertFalse(portal.is_displayed()))
|
||||
Reference in New Issue
Block a user