From cbc4f4f323a9c6068b9e21c370673cd655c00054 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 1 Jun 2026 14:45:18 -0400 Subject: [PATCH] =?UTF-8?q?position=20tooltips:=20titles=20read=20'the=20E?= =?UTF-8?q?arthman'=20(article)=20+=20occupied=20gatekeeper=20circles=20no?= =?UTF-8?q?w=20produce=20tooltips;=20FT=20flow=20dismisses=20the=20gameboa?= =?UTF-8?q?rd=20Brief=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ' 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> --- src/apps/billboard/tests/integrated/test_views.py | 3 ++- src/apps/epic/tests/integrated/test_views.py | 3 ++- src/functional_tests/test_bill_my_buds.py | 4 ++-- src/functional_tests/test_bill_my_buds_tooltip.py | 2 +- src/functional_tests/test_game_room_gatekeeper.py | 6 ++++++ src/functional_tests/test_game_room_select_role.py | 1 + src/static_src/scss/_room.scss | 11 +++++++++++ .../apps/billboard/_partials/_bud_add_panel.html | 6 +++++- .../apps/billboard/_partials/_my_buds_item.html | 2 +- .../apps/gameboard/_partials/_table_positions.html | 2 +- 10 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index e4c58c5..adc205d 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -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): diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index b3262a3..11b285e 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -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 diff --git a/src/functional_tests/test_bill_my_buds.py b/src/functional_tests/test_bill_my_buds.py index c2baab4..8c4fd32 100644 --- a/src/functional_tests/test_bill_my_buds.py +++ b/src/functional_tests/test_bill_my_buds.py @@ -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): diff --git a/src/functional_tests/test_bill_my_buds_tooltip.py b/src/functional_tests/test_bill_my_buds_tooltip.py index 04c321d..6beb1aa 100644 --- a/src/functional_tests/test_bill_my_buds_tooltip.py +++ b/src/functional_tests/test_bill_my_buds_tooltip.py @@ -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): diff --git a/src/functional_tests/test_game_room_gatekeeper.py b/src/functional_tests/test_game_room_gatekeeper.py index e4336d9..98bde9b 100644 --- a/src/functional_tests/test_game_room_gatekeeper.py +++ b/src/functional_tests/test_game_room_gatekeeper.py @@ -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)) diff --git a/src/functional_tests/test_game_room_select_role.py b/src/functional_tests/test_game_room_select_role.py index 3d4c329..3543a35 100644 --- a/src/functional_tests/test_game_room_select_role.py +++ b/src/functional_tests/test_game_room_select_role.py @@ -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) diff --git a/src/static_src/scss/_room.scss b/src/static_src/scss/_room.scss index bd570cf..689dfad 100644 --- a/src/static_src/scss/_room.scss +++ b/src/static_src/scss/_room.scss @@ -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 diff --git a/src/templates/apps/billboard/_partials/_bud_add_panel.html b/src/templates/apps/billboard/_partials/_bud_add_panel.html index 668768e..cb8e2f8 100644 --- a/src/templates/apps/billboard/_partials/_bud_add_panel.html +++ b/src/templates/apps/billboard/_partials/_bud_add_panel.html @@ -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 diff --git a/src/templates/apps/billboard/_partials/_my_buds_item.html b/src/templates/apps/billboard/_partials/_my_buds_item.html index e3a5f65..e1d8bea 100644 --- a/src/templates/apps/billboard/_partials/_my_buds_item.html +++ b/src/templates/apps/billboard/_partials/_my_buds_item.html @@ -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 %}> diff --git a/src/templates/apps/gameboard/_partials/_table_positions.html b/src/templates/apps/gameboard/_partials/_table_positions.html index a5e723a..0b0ef5f 100644 --- a/src/templates/apps/gameboard/_partials/_table_positions.html +++ b/src/templates/apps/gameboard/_partials/_table_positions.html @@ -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 %}