position tooltips: titles read 'the Earthman' (article) + occupied gatekeeper circles now produce tooltips; FT flow dismisses the gameboard Brief — TDD
Three follow-ups from manual review: 1. Title article: the tooltip .tt-description (position circles + My Buds) prepends 'the ' so it reads 'the Earthman', matching the established '@handle the <Title>' convention + the visible bud-row. Touches _table_positions.html, _my_buds_item.html, and the async add-bud row builder in _bud_add_panel.html (was building data-tt-description without the article, diverging from the server-rendered rows). active_title_display always returns a value so the article is always well-formed. Tests updated: epic + billboard IT (data-tt-description='the Earthman'), the two My Buds FTs. 2. Initial-gatekeeper tooltips: under the gatekeeper .gate-backdrop, .position-strip circles are pointer-events:none, so mouseenter only reached a .gate-slot that contained a pointer-events:auto descendant (an OK/NVM/drop button). An occupied circle WITHOUT such a button never fired its tooltip. Re-enable .gate-slot.filled/.reserved (occupied, hoverable, no click action of their own) at (0,4,1) > the (0,3,1) suppressor; empty circles stay suppressed. room_gate already handled via .room-gate-page; Role Select covered by the same .role-select-backdrop variant. 3. FT flow: the @taxman 'Debits & credits' ledger Brief renders over the gameboard top and intercepts id_create_game_btn. Added dismiss_brief_if_present() before the create-game click in the gatekeeper + select_role FTs (the trinket FTs already carry their own dismisses). Verified: 11 position-tooltip FTs, gatekeeper drop, both My Buds FTs, select_role create-game FT all green; full IT/UT suite 1604 green. The gatekeeper-circle pointer-events couldn't be FT-asserted reliably (synthetic mouseenter bypasses pointer-events; real-hover is flaky) — verified via the compiled-CSS cascade + the drop FT confirming the OK button still clicks. [[project-position-circle-tooltips]] [[feedback-dismiss-brief-ft-helper]] [[feedback-scss-import-order-specificity]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1464,7 +1464,8 @@ class MyBudsRowEnrichmentTest(TestCase):
|
||||
def test_row_carries_tt_title_description_email_attrs(self):
|
||||
response = self.client.get(reverse("billboard:my_buds"))
|
||||
self.assertContains(response, 'data-tt-title="@alice"')
|
||||
self.assertContains(response, 'data-tt-description="Earthman"')
|
||||
# Title carries the article — "the Earthman", matching the visible row.
|
||||
self.assertContains(response, 'data-tt-description="the Earthman"')
|
||||
self.assertContains(response, 'data-tt-email="alice@row.io"')
|
||||
|
||||
def test_row_renders_at_handle_the_title(self):
|
||||
|
||||
@@ -675,7 +675,8 @@ class PositionTooltipRenderTest(TestCase):
|
||||
self.assertIn('data-tt-title="@g2"', slot2)
|
||||
# No email field in the tooltip payload (user-spec).
|
||||
self.assertNotIn("data-tt-email", slot2)
|
||||
self.assertIn("data-tt-description", slot2)
|
||||
# Title carries the article — "the Earthman", not bare "Earthman".
|
||||
self.assertIn('data-tt-description="the Earthman"', slot2)
|
||||
|
||||
def test_bud_occupant_carries_bud_class_and_shoptalk(self):
|
||||
from apps.billboard.models import BudshipNote
|
||||
|
||||
@@ -92,7 +92,7 @@ class MyBudsPageTest(FunctionalTest):
|
||||
self.assertIn("bud-entry", cls)
|
||||
# data-tt-* attrs the portal reads on row-lock.
|
||||
self.assertEqual(row.get_attribute("data-tt-title"), "@alice")
|
||||
self.assertEqual(row.get_attribute("data-tt-description"), "Earthman")
|
||||
self.assertEqual(row.get_attribute("data-tt-description"), "the Earthman")
|
||||
self.assertEqual(row.get_attribute("data-tt-email"), "alice@test.io")
|
||||
# Anchor routes into the bud landing page; trailing ` the <Title>`.
|
||||
anchor = row.find_element(By.CSS_SELECTOR, ".bud-name a")
|
||||
@@ -109,7 +109,7 @@ class MyBudsPageTest(FunctionalTest):
|
||||
portal.find_element(By.CSS_SELECTOR, ".tt-title").text, "@alice"
|
||||
)
|
||||
self.assertEqual(
|
||||
portal.find_element(By.CSS_SELECTOR, ".tt-description").text, "Earthman"
|
||||
portal.find_element(By.CSS_SELECTOR, ".tt-description").text, "the Earthman"
|
||||
)
|
||||
|
||||
def test_no_autocomplete_suggestions_on_my_buds_page(self):
|
||||
|
||||
@@ -80,7 +80,7 @@ class MyBudsClickOpensTooltipPortalTest(FunctionalTest):
|
||||
desc = portal.find_element(By.CSS_SELECTOR, ".tt-description").text
|
||||
email = portal.find_element(By.CSS_SELECTOR, ".tt-email").text
|
||||
self.assertEqual(title, "@alice")
|
||||
self.assertEqual(desc, "Earthman")
|
||||
self.assertEqual(desc, "the Earthman")
|
||||
self.assertEqual(email, "alice@test.io")
|
||||
|
||||
def test_tooltip_milestone_absent_when_shoptalk_never_edited(self):
|
||||
|
||||
@@ -30,6 +30,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
lambda: self.browser.find_element(By.ID, "id_applet_new_game")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
# 3. User is redirected to Gatekeeper page for new room
|
||||
self.wait_for(
|
||||
@@ -61,6 +62,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertIn("/gate/", self.browser.current_url)
|
||||
@@ -94,6 +96,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertIn("/gate/", self.browser.current_url)
|
||||
@@ -113,6 +116,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertIn("/gate/", self.browser.current_url)
|
||||
@@ -187,6 +191,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertIn("/gate/", self.browser.current_url)
|
||||
@@ -226,6 +231,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_new_game_name"))
|
||||
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_new_game_name")
|
||||
).send_keys("Dragon's Den")
|
||||
self.dismiss_brief_if_present() # @taxman ledger Brief renders over the gameboard top
|
||||
self.browser.find_element(By.ID, "id_create_game_btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertIn("/gate/", self.browser.current_url)
|
||||
|
||||
@@ -359,6 +359,17 @@ html:has(.role-select-backdrop) .position-strip .gate-slot { pointer-events: non
|
||||
// Re-enable clicks on confirm/reject/drop-token forms inside slots
|
||||
html:has(.gate-backdrop) .position-strip .gate-slot form,
|
||||
html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: auto; }
|
||||
// Occupied circles must stay HOVERABLE under the gatekeeper / role-select
|
||||
// backdrop so their rich position tooltips fire. A filled/reserved circle has
|
||||
// no click action of its own (only the OK/NVM/drop forms do, re-enabled
|
||||
// above) — but mouseenter only reaches a pointer-events:none `.gate-slot` when
|
||||
// something inside it IS hit-testable, so a filled circle lacking a button
|
||||
// silently produced no tooltip on the initial gatekeeper. Empty circles stay
|
||||
// suppressed (they carry no tooltip). (0,4,1) beats the (0,3,1) suppressor.
|
||||
html:has(.gate-backdrop) .position-strip .gate-slot.filled,
|
||||
html:has(.gate-backdrop) .position-strip .gate-slot.reserved,
|
||||
html:has(.role-select-backdrop) .position-strip .gate-slot.filled,
|
||||
html:has(.role-select-backdrop) .position-strip .gate-slot.reserved { pointer-events: auto; }
|
||||
// The room-gate renewal modal renders its OWN .gate-backdrop, but its
|
||||
// position circles are hover-only (tooltips) and must stay live — re-enable
|
||||
// them. The doubled `.room-gate-page` makes this (0,4,1) so it UNAMBIGUOUSLY
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
li.className = 'applet-list-entry bud-entry';
|
||||
li.dataset.budId = bud.id;
|
||||
li.dataset.ttTitle = handle;
|
||||
li.dataset.ttDescription = title;
|
||||
// Carry the article — "the Earthman" — to mirror _my_buds_item.html's
|
||||
// `data-tt-description="the {{ active_title_display }}"` (and the
|
||||
// visible `.bud-row-title` below), so the async row's tooltip matches
|
||||
// the server-rendered rows.
|
||||
li.dataset.ttDescription = 'the ' + title;
|
||||
li.dataset.ttEmail = bud.email || '';
|
||||
li.dataset.ttShoptalk = ''; // fresh bud — no BudshipNote yet
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<li class="applet-list-entry bud-entry"
|
||||
data-bud-id="{{ item.id }}"
|
||||
data-tt-title="{{ item|at_handle }}"
|
||||
data-tt-description="{{ item.active_title_display }}"
|
||||
data-tt-description="the {{ item.active_title_display }}"
|
||||
data-tt-email="{{ item.email }}"
|
||||
data-tt-shoptalk="{{ item.shoptalk_text|default:'' }}"
|
||||
{% if item.milestone_dt %}data-tt-milestone="edited {{ item.milestone_dt|relative_ts }}"{% endif %}>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="position-strip">
|
||||
{% for pos in gate_positions %}
|
||||
<div class="gate-slot{% if pos.slot.status == 'EMPTY' %} empty{% elif pos.slot.status == 'FILLED' %} filled{% elif pos.slot.status == 'RESERVED' %} reserved{% endif %}{% if pos.role_assigned %} role-assigned{% endif %}{% if pos.state_class %} {{ pos.state_class }}{% endif %}"
|
||||
data-slot="{{ pos.slot.slot_number }}"{% if pos.slot.gamer %} data-user-id="{{ pos.slot.gamer.id }}" data-tt-title="{{ pos.slot.gamer|at_handle }}" data-tt-description="{{ pos.slot.gamer.active_title_display }}" data-tt-shoptalk="{{ pos.shoptalk }}" data-tt-tokens="{{ pos.tokens }}"{% if pos.expiry %} data-tt-expiry="{{ pos.expiry|date:'c' }}"{% endif %}{% if pos.sign_rank %} data-tt-sign-rank="{{ pos.sign_rank }}" data-tt-sign-suit="{{ pos.sign_suit_icon }}"{% endif %}{% endif %}>
|
||||
data-slot="{{ pos.slot.slot_number }}"{% if pos.slot.gamer %} data-user-id="{{ pos.slot.gamer.id }}" data-tt-title="{{ pos.slot.gamer|at_handle }}" data-tt-description="the {{ pos.slot.gamer.active_title_display }}" data-tt-shoptalk="{{ pos.shoptalk }}" data-tt-tokens="{{ pos.tokens }}"{% if pos.expiry %} data-tt-expiry="{{ pos.expiry|date:'c' }}"{% endif %}{% if pos.sign_rank %} data-tt-sign-rank="{{ pos.sign_rank }}" data-tt-sign-suit="{{ pos.sign_suit_icon }}"{% endif %}{% endif %}>
|
||||
<span class="slot-number">{{ pos.slot.slot_number }}</span>
|
||||
<span class="slot-gamer">{% if pos.slot.gamer %}{{ pos.slot.gamer|at_handle }}{% else %}empty{% endif %}</span>
|
||||
{% if pos.is_me_also %}
|
||||
|
||||
Reference in New Issue
Block a user