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:
Disco DeDisco
2026-06-01 14:45:18 -04:00
parent 5a39746853
commit cbc4f4f323
10 changed files with 32 additions and 8 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 %}>

View File

@@ -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 %}