my-buds async add: render full row (anchor + the <Title> + data-tt-* attrs) so the appended entry's tooltip isn't empty — TDD
Adding a bud appended a stripped `@handle`-only <li> — no `applet-list-entry` class, no bud-page anchor, no ` the <Title>` span, no data-tt-* attrs — so the new row rendered without its title and its tooltip came up empty next to the server-rendered rows above it. - add_bud (billboard/views.py) — bud payload now carries `at_handle` (server- computed; the client can't replicate at_handle's truncate_email fallback for username-less buds) + `title` (active_title_display). IT asserts both. - _bud_add_panel.html `_appendBudEntry` — rebuilt to mirror _my_buds_item.html exactly: both classes, data-tt-title/description/email/shoptalk, the bud-name anchor into /billboard/buds/<id>/, and the trailing ` the <Title>`. New FT pins the regression: appended row carries the attrs + anchor + title and its portal populates non-empty on row-lock. AddBudViewTest + append FT green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,19 @@ class AddBudViewTest(TestCase):
|
|||||||
response = self.client.get(reverse("billboard:add_bud"))
|
response = self.client.get(reverse("billboard:add_bud"))
|
||||||
self.assertEqual(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
|
|
||||||
|
def test_add_returns_at_handle_and_title_for_tooltip_row(self):
|
||||||
|
"""The async-appended My Buds row mirrors _my_buds_item.html, so the
|
||||||
|
payload must carry the bud's at_handle + active_title_display to fill
|
||||||
|
the data-tt-* attrs — without them the new row's tooltip renders empty
|
||||||
|
(the entries above it, server-rendered, have them). Regression
|
||||||
|
2026-05-29."""
|
||||||
|
alice = User.objects.create(email="alice@test.io", username="alice")
|
||||||
|
body = self.client.post(
|
||||||
|
reverse("billboard:add_bud"), data={"recipient": "alice"},
|
||||||
|
).json()
|
||||||
|
self.assertEqual(body["bud"]["at_handle"], "@alice")
|
||||||
|
self.assertEqual(body["bud"]["title"], alice.active_title_display)
|
||||||
|
|
||||||
def test_add_resolves_username_too_not_just_email(self):
|
def test_add_resolves_username_too_not_just_email(self):
|
||||||
"""Phase 2: recipient field accepts usernames as well as emails."""
|
"""Phase 2: recipient field accepts usernames as well as emails."""
|
||||||
alice = User.objects.create(email="alice@test.io", username="alice")
|
alice = User.objects.create(email="alice@test.io", username="alice")
|
||||||
|
|||||||
@@ -700,11 +700,16 @@ def add_bud(request):
|
|||||||
already_present = candidate in request.user.buds.all()
|
already_present = candidate in request.user.buds.all()
|
||||||
if not already_present:
|
if not already_present:
|
||||||
request.user.buds.add(candidate)
|
request.user.buds.add(candidate)
|
||||||
|
from apps.lyric.templatetags.lyric_extras import at_handle
|
||||||
display = candidate.username or candidate.email
|
display = candidate.username or candidate.email
|
||||||
bud = {
|
bud = {
|
||||||
"id": str(candidate.id),
|
"id": str(candidate.id),
|
||||||
"username": display,
|
"username": display,
|
||||||
"email": candidate.email,
|
"email": candidate.email,
|
||||||
|
# at_handle + title feed the async row's data-tt-* attrs so its
|
||||||
|
# tooltip matches the server-rendered rows (regression 2026-05-29).
|
||||||
|
"at_handle": at_handle(candidate),
|
||||||
|
"title": candidate.active_title_display,
|
||||||
}
|
}
|
||||||
recipient_display = display
|
recipient_display = display
|
||||||
recipient_user_id = str(candidate.id)
|
recipient_user_id = str(candidate.id)
|
||||||
|
|||||||
@@ -64,6 +64,47 @@ class MyBudsPageTest(FunctionalTest):
|
|||||||
[],
|
[],
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def test_async_added_entry_matches_server_row_and_populates_tooltip(self):
|
||||||
|
"""Regression 2026-05-29: the async-appended row used to be a stripped
|
||||||
|
`@handle`-only `<li>` — no anchor, no ` the <Title>`, no data-tt-*
|
||||||
|
attrs — so its tooltip rendered empty (vs. the server-rendered rows
|
||||||
|
above it). The appended row must mirror _my_buds_item.html exactly."""
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/my-buds/")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn")).click()
|
||||||
|
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
|
||||||
|
recipient.send_keys("alice@test.io")
|
||||||
|
self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
sel = f".bud-entry[data-bud-id='{self.alice.id}']"
|
||||||
|
row = self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, sel))
|
||||||
|
# Carries both classes (styling + tooltip-lock both key on these).
|
||||||
|
cls = (row.get_attribute("class") or "").split()
|
||||||
|
self.assertIn("applet-list-entry", cls)
|
||||||
|
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-email"), "alice@test.io")
|
||||||
|
# Anchor routes into the bud landing page; trailing ` the <Title>`.
|
||||||
|
anchor = row.find_element(By.CSS_SELECTOR, ".bud-name a")
|
||||||
|
self.assertIn(f"/billboard/buds/{self.alice.id}/", anchor.get_attribute("href"))
|
||||||
|
self.assertIn("the Earthman", row.find_element(
|
||||||
|
By.CSS_SELECTOR, ".bud-row-title"
|
||||||
|
).text)
|
||||||
|
# Click the row (NOT the anchor) → portal locks + populates non-empty.
|
||||||
|
row.find_element(By.CSS_SELECTOR, ".bud-row-title").click()
|
||||||
|
portal = self.wait_for(lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal.active"
|
||||||
|
))
|
||||||
|
self.assertEqual(
|
||||||
|
portal.find_element(By.CSS_SELECTOR, ".tt-title").text, "@alice"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
portal.find_element(By.CSS_SELECTOR, ".tt-description").text, "Earthman"
|
||||||
|
)
|
||||||
|
|
||||||
def test_no_autocomplete_suggestions_on_my_buds_page(self):
|
def test_no_autocomplete_suggestions_on_my_buds_page(self):
|
||||||
"""The bud-autocomplete pool is request.user.buds — surfacing buds
|
"""The bud-autocomplete pool is request.user.buds — surfacing buds
|
||||||
you've already added on the page where you ADD new buds is just
|
you've already added on the page where you ADD new buds is just
|
||||||
|
|||||||
@@ -19,18 +19,41 @@
|
|||||||
var empty = list.querySelector('.applet-list-entry--empty');
|
var empty = list.querySelector('.applet-list-entry--empty');
|
||||||
if (empty) empty.remove();
|
if (empty) empty.remove();
|
||||||
|
|
||||||
|
// Mirror _my_buds_item.html EXACTLY so the async row behaves like a
|
||||||
|
// server-rendered one: both classes (styling + tooltip-lock key on
|
||||||
|
// them), the data-tt-* attrs the portal reads on row-lock, the
|
||||||
|
// bud-page anchor, and the trailing ` the <Title>`. Earlier this
|
||||||
|
// built a bare `@handle` <li> → empty tooltip + dead row.
|
||||||
|
// `at_handle` comes from the server (the client can't replicate the
|
||||||
|
// truncate_email fallback for username-less buds); fall back to a
|
||||||
|
// naive `@`-prefix only if the payload predates that field.
|
||||||
|
var handle = bud.at_handle || (function () {
|
||||||
|
var d = bud.username || '';
|
||||||
|
return d.indexOf('@') >= 0 ? d : '@' + d;
|
||||||
|
}());
|
||||||
|
var title = bud.title || '';
|
||||||
|
|
||||||
var li = document.createElement('li');
|
var li = document.createElement('li');
|
||||||
li.className = 'bud-entry';
|
li.className = 'applet-list-entry bud-entry';
|
||||||
li.dataset.budId = bud.id;
|
li.dataset.budId = bud.id;
|
||||||
|
li.dataset.ttTitle = handle;
|
||||||
|
li.dataset.ttDescription = title;
|
||||||
|
li.dataset.ttEmail = bud.email || '';
|
||||||
|
li.dataset.ttShoptalk = ''; // fresh bud — no BudshipNote yet
|
||||||
|
|
||||||
var name = document.createElement('span');
|
var name = document.createElement('span');
|
||||||
name.className = 'bud-name';
|
name.className = 'bud-name';
|
||||||
// Mirror the `at_handle` template filter: prefix `@` on plain
|
var a = document.createElement('a');
|
||||||
// usernames; leave the value untouched if it already contains
|
a.href = '/billboard/buds/' + bud.id + '/';
|
||||||
// `@` (the server falls back to email when username is unset,
|
a.textContent = handle;
|
||||||
// and emails don't take the prefix).
|
name.appendChild(a);
|
||||||
var display = bud.username || '';
|
|
||||||
name.textContent = display.indexOf('@') >= 0 ? display : '@' + display;
|
|
||||||
li.appendChild(name);
|
li.appendChild(name);
|
||||||
|
|
||||||
|
var rowTitle = document.createElement('span');
|
||||||
|
rowTitle.className = 'bud-row-title';
|
||||||
|
rowTitle.textContent = ' the ' + title;
|
||||||
|
li.appendChild(rowTitle);
|
||||||
|
|
||||||
var buffer = list.querySelector('.bud-entry-buffer');
|
var buffer = list.querySelector('.bud-entry-buffer');
|
||||||
if (buffer) list.insertBefore(li, buffer);
|
if (buffer) list.insertBefore(li, buffer);
|
||||||
else list.appendChild(li);
|
else list.appendChild(li);
|
||||||
|
|||||||
Reference in New Issue
Block a user