buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD

- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
  - applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
  - billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
  - global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
  - new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
  - my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
  - my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
  - SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
  - 841 ITs + 5 my_buds/my_posts FTs green.

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:
Disco DeDisco
2026-05-08 23:08:33 -04:00
parent 5f6002aa70
commit 246e45e55d
29 changed files with 552 additions and 443 deletions

View File

@@ -55,17 +55,17 @@ class PostPage:
)
def share_post_with(self, email):
# Buddy-btn flow (post-Brief sprint): click bottom-left handshake,
# Bud-btn flow (post-Brief sprint): click bottom-left handshake,
# type the email in the slide-out, click the .btn-confirm OK, wait
# for the recipient chip.
buddy_btn = self.test.browser.find_element(By.ID, "id_buddy_btn")
buddy_btn.click()
bud_btn = self.test.browser.find_element(By.ID, "id_bud_btn")
bud_btn.click()
recipient = self.test.wait_for(
lambda: self.test.browser.find_element(By.ID, "id_recipient")
)
recipient.send_keys(email)
ok = self.test.browser.find_element(
By.CSS_SELECTOR, "#id_buddy_panel .btn.btn-confirm"
By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
)
ok.click()
self.test.wait_for(

View File

@@ -59,21 +59,21 @@ class AdminPostInputReadonlyTest(FunctionalTest):
)
class AdminPostHasNoBuddyBtnTest(FunctionalTest):
"""Admin-Post (note-unlock thread) suppresses #id_buddy_btn — friend
class AdminPostHasNoBudBtnTest(FunctionalTest):
"""Admin-Post (note-unlock thread) suppresses #id_bud_btn — friend
invites don't apply to system-authored threads. User-Post still
renders the btn (regression coverage in test_buddy_btn.py)."""
renders the btn (regression coverage in test_bud_btn.py)."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="nobuddy@test.io")
self.gamer = User.objects.create(email="nobud@test.io")
Note.grant_if_new(self.gamer, "stargazer")
self.admin_post = Post.objects.get(
owner=self.gamer, kind=Post.KIND_NOTE_UNLOCK,
)
self.create_pre_authenticated_session("nobuddy@test.io")
self.create_pre_authenticated_session("nobud@test.io")
def test_buddy_btn_absent_on_admin_post(self):
def test_bud_btn_absent_on_admin_post(self):
self.browser.get(
self.live_server_url + f"/billboard/post/{self.admin_post.id}/"
)
@@ -82,8 +82,8 @@ class AdminPostHasNoBuddyBtnTest(FunctionalTest):
lambda: self.browser.find_element(By.ID, "id_post_line_text")
)
self.assertFalse(
self.browser.find_elements(By.ID, "id_buddy_btn"),
"Admin-Post must NOT render #id_buddy_btn",
self.browser.find_elements(By.ID, "id_bud_btn"),
"Admin-Post must NOT render #id_bud_btn",
)

View File

@@ -26,7 +26,7 @@ class BillboardScrollTest(FunctionalTest):
super().setUp()
for slug, name, cols, rows in [
("my-scrolls", "My Scrolls", 4, 3),
("my-buddies", "My Buddies", 4, 3),
("my-buds", "My Buds", 4, 3),
("most-recent-scroll", "Most Recent Scroll", 8, 6),
]:
Applet.objects.get_or_create(
@@ -193,7 +193,7 @@ class BillscrollPositionTest(FunctionalTest):
class BillboardAppletsTest(FunctionalTest):
"""
FT: billboard page renders three applets in the grid — My Scrolls,
My Buddies, and Most Recent Scroll — with a functioning gear menu.
My Buds, and Most Recent Scroll — with a functioning gear menu.
"""
def setUp(self):
@@ -203,7 +203,7 @@ class BillboardAppletsTest(FunctionalTest):
self.room = Room.objects.create(name="Arcane Assembly", owner=self.founder)
for slug, name, cols, rows in [
("my-scrolls", "My Scrolls", 4, 3),
("my-buddies", "My Buddies", 4, 3),
("my-buds", "My Buds", 4, 3),
("most-recent-scroll", "Most Recent Scroll", 8, 6),
]:
Applet.objects.get_or_create(
@@ -219,7 +219,7 @@ class BillboardAppletsTest(FunctionalTest):
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_my_scrolls")
)
self.browser.find_element(By.ID, "id_applet_my_buddies")
self.browser.find_element(By.ID, "id_applet_my_buds")
self.browser.find_element(By.ID, "id_applet_most_recent_scroll")
def test_billboard_my_scrolls_lists_rooms(self):
@@ -278,7 +278,7 @@ class BillboardAppletsTest(FunctionalTest):
lambda: self.browser.find_element(By.ID, "id_billboard_applet_menu")
)
contacts_cb = menu.find_element(
By.CSS_SELECTOR, "input[value='my-buddies']"
By.CSS_SELECTOR, "input[value='my-buds']"
)
self.browser.execute_script("arguments[0].click()", contacts_cb)
menu.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
@@ -286,7 +286,7 @@ class BillboardAppletsTest(FunctionalTest):
# Contacts is hidden; Most Recent Scroll + My Scrolls keep their content (bug #2)
self.wait_for(
lambda: self.assertEqual(
self.browser.find_elements(By.ID, "id_applet_my_buddies"),
self.browser.find_elements(By.ID, "id_applet_my_buds"),
[],
)
)
@@ -305,7 +305,7 @@ class BillboardAppletsTest(FunctionalTest):
)
# The freshly-rendered menu must reflect DB state (Contacts unchecked)
contacts_cb = menu.find_element(
By.CSS_SELECTOR, "input[value='my-buddies']"
By.CSS_SELECTOR, "input[value='my-buds']"
)
self.assertFalse(contacts_cb.is_selected())
most_recent_scroll_cb = menu.find_element(
@@ -322,7 +322,7 @@ class BillboardAppletsTest(FunctionalTest):
)
)
self.assertEqual(
self.browser.find_elements(By.ID, "id_applet_my_buddies"),
self.browser.find_elements(By.ID, "id_applet_my_buds"),
[],
)
@@ -332,7 +332,7 @@ class BillboardAppletsTest(FunctionalTest):
lambda: self.browser.find_element(By.ID, "id_applet_my_scrolls")
)
self.assertEqual(
self.browser.find_elements(By.ID, "id_applet_my_buddies"),
self.browser.find_elements(By.ID, "id_applet_my_buds"),
[],
)
self.assertEqual(

View File

@@ -1,9 +1,9 @@
"""FT spec for the Buddy btn sprint — post.html bottom-left handshake button.
"""FT spec for the Bud btn sprint — post.html bottom-left handshake button.
Written red BEFORE implementation as a TDD handoff so the post-compaction
agent (or future Disco) can land the feature without losing intent. Run:
python src/manage.py test functional_tests.test_buddy_btn
python src/manage.py test functional_tests.test_bud_btn
All tests should be RED initially. Implementation lands when they go green.
@@ -11,22 +11,22 @@ All tests should be RED initially. Implementation lands when they go green.
SPEC SUMMARY
A new #id_buddy_btn (<i class="fa-solid fa-handshake">) sits bottom-left
A new #id_bud_btn (<i class="fa-solid fa-handshake">) sits bottom-left
of the viewport mirror of #id_kit_btn (bottom-right). Shares the same
fixed/circular/secUser-bordered look + .active-state styling.
Lives in a partial template, e.g. apps/billboard/_partials/_buddy_panel.html,
included only by post.html (NOT the global base.html buddy is post-only).
Lives in a partial template, e.g. apps/billboard/_partials/_bud_panel.html,
included only by post.html (NOT the global base.html bud is post-only).
Replaces the inline share form on post.html: typing the recipient now
happens in a slide-out under the buddy btn.
Click #id_buddy_btn → recipient field grows L→R under it, spanning
happens in a slide-out under the bud btn.
Click #id_bud_btn → recipient field grows L→R under it, spanning
`100vw - 3rem` (1.5rem padding each side). The field is vertically
centred on the buddy btn's centre, w. healthy left padding so the typed
centred on the bud btn's centre, w. healthy left padding so the typed
text + placeholder don't overlap the btn glyph.
An OK btn (.btn.btn-confirm) is tacked on the trailing edge of the
field (replaces the legacy big SHARE .btn-primary).
While the recipient field is open: html.buddy-open is set; #id_kit_btn
While the recipient field is open: html.bud-open is set; #id_kit_btn
quickly eases to opacity 0. Symmetric: when html.kit-open is set,
#id_buddy_btn eases to opacity 0.
#id_bud_btn eases to opacity 0.
Click OK POST share-post async (existing C3.b endpoint), clears the
field, closes the slide-out, slide-down Brief banner appears.
Click outside the field/btn closes the slide-out, clears the field
@@ -40,29 +40,29 @@ SPEC SUMMARY
IMPLEMENTATION CHECKLIST (post-compaction)
1. Create templates/apps/billboard/_partials/_buddy_panel.html w. the btn
1. Create templates/apps/billboard/_partials/_bud_panel.html w. the btn
+ slide-out form + inline JS.
2. Edit templates/apps/billboard/post.html:
- Drop the inline `<form id=id_share_form>` + #id_recipient + SHARE btn
block (the share JS moves into _buddy_panel.html).
- Add `{% include "apps/billboard/_partials/_buddy_panel.html" %}`.
block (the share JS moves into _bud_panel.html).
- Add `{% include "apps/billboard/_partials/_bud_panel.html" %}`.
3. Edit billboard.views.view_post (or my_posts) context:
- "page_class": "page-billboard" (or new page-post) so the body class
picks up the aperture SCSS.
4. SCSS add to _game-kit.scss neighbour or new _buddy.scss:
- #id_buddy_btn: position fixed bottom-left, mirror of #id_kit_btn
4. SCSS add to _game-kit.scss neighbour or new _bud.scss:
- #id_bud_btn: position fixed bottom-left, mirror of #id_kit_btn
(3rem circle, secUser border, .active state, etc.)
- #id_buddy_panel (the slide-out wrapper): position fixed,
- #id_bud_panel (the slide-out wrapper): position fixed,
left: 1.5rem, right: 1.5rem (or 1.5rem + #id_kit_btn width when
kit btn visible but mutual-exclusion makes that moot), bottom-
aligned w. the btn centre, transition transform/width LR.
- html.buddy-open #id_kit_btn { opacity: 0; transition: opacity 0.15s; }
- html.kit-open #id_buddy_btn { opacity: 0; transition: opacity 0.15s; }
- html.bud-open #id_kit_btn { opacity: 0; transition: opacity 0.15s; }
- html.kit-open #id_bud_btn { opacity: 0; transition: opacity 0.15s; }
- The OK .btn-confirm gets normal btn-pad styling; flex-shrink:0 on
the trailing edge of the slide-out.
5. JS in _buddy_panel.html:
5. JS in _bud_panel.html:
- Mirror game-kit.js click/escape/click-outside pattern.
- Toggle html.buddy-open + #id_buddy_btn.active.
- Toggle html.bud-open + #id_bud_btn.active.
- On submit/OK: fetch POST share-post w. Accept:application/json,
reuse the C3.b response handling (line append, banner via
Brief.showBanner, recipient_display chip append).
@@ -70,7 +70,7 @@ IMPLEMENTATION CHECKLIST (post-compaction)
6. post.html + my_posts.html: add the body class hook so the aperture
SCSS engages (probably page-billboard already, just need to confirm).
7. Update functional_tests.post_page.PostPage.share_post_with() to
drive the buddy-btn flow (click btn type click OK wait for chip).
drive the bud-btn flow (click btn type click OK wait for chip).
8. Re-run test_sharing.SharingTest should still pass once the page-
object mirrors the new flow.
@@ -82,7 +82,7 @@ KNOWN AT TIME OF WRITING
- The C3.b share-post async endpoint accepts Accept: application/json,
returns {brief, line_text, recipient_display}; intercepted by the
inline JS in post.html's existing #id_share_form. That JS moves into
_buddy_panel.html.
_bud_panel.html.
- body.page-billboard class is set by billboard:billboard view; post.html
needs it (or its own class) added in billboard.views.view_post.
"""
@@ -104,8 +104,8 @@ def _seed_a_post(user):
return p
class BuddyBtnPresenceTest(FunctionalTest):
"""The buddy btn is post-only — present on post.html, absent elsewhere."""
class BudBtnPresenceTest(FunctionalTest):
"""The bud btn is post-only — present on post.html, absent elsewhere."""
def setUp(self):
super().setUp()
@@ -113,46 +113,46 @@ class BuddyBtnPresenceTest(FunctionalTest):
slug="my-posts",
defaults={"name": "My Posts", "grid_cols": 4, "grid_rows": 3, "context": "billboard"},
)
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
# ── B1 ──────────────────────────────────────────────────────────────────
def test_buddy_btn_renders_on_post_html(self):
def test_bud_btn_renders_on_post_html(self):
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
icon = btn.find_element(By.CSS_SELECTOR, "i.fa-solid.fa-handshake")
self.assertIsNotNone(icon)
# ── B2 ──────────────────────────────────────────────────────────────────
def test_buddy_btn_absent_on_dashboard(self):
def test_bud_btn_absent_on_dashboard(self):
self.browser.get(self.live_server_url + "/")
# Allow page to settle
self.wait_for(lambda: self.browser.find_element(By.ID, "id_kit_btn"))
self.assertFalse(self.browser.find_elements(By.ID, "id_buddy_btn"))
self.assertFalse(self.browser.find_elements(By.ID, "id_bud_btn"))
# ── B3 ──────────────────────────────────────────────────────────────────
def test_buddy_btn_absent_on_billboard_index(self):
def test_bud_btn_absent_on_billboard_index(self):
self.browser.get(self.live_server_url + "/billboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_kit_btn"))
self.assertFalse(self.browser.find_elements(By.ID, "id_buddy_btn"))
self.assertFalse(self.browser.find_elements(By.ID, "id_bud_btn"))
class BuddyBtnPositionTest(FunctionalTest):
class BudBtnPositionTest(FunctionalTest):
"""The btn sits bottom-left, mirror of #id_kit_btn's bottom-right."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
def test_buddy_btn_is_fixed_bottom_left(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
def test_bud_btn_is_fixed_bottom_left(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
cs = self.browser.execute_script(
"var s = getComputedStyle(arguments[0]); "
"return {position: s.position, bottom: s.bottom, left: s.left, right: s.right};",
@@ -164,9 +164,9 @@ class BuddyBtnPositionTest(FunctionalTest):
self.assertNotEqual(cs["bottom"], "auto")
self.assertNotEqual(cs["left"], "auto")
def test_buddy_btn_size_matches_kit_btn(self):
def test_bud_btn_size_matches_kit_btn(self):
"""Same circular-3rem look — visually a mirror pair."""
btn = self.browser.find_element(By.ID, "id_buddy_btn")
btn = self.browser.find_element(By.ID, "id_bud_btn")
kit = self.browser.find_element(By.ID, "id_kit_btn")
b_box = btn.size
k_box = kit.size
@@ -174,47 +174,47 @@ class BuddyBtnPositionTest(FunctionalTest):
self.assertEqual(b_box["height"], k_box["height"])
class BuddyBtnSlideOutTest(FunctionalTest):
"""Click the buddy btn → recipient field + OK btn slide out under it."""
class BudBtnSlideOutTest(FunctionalTest):
"""Click the bud btn → recipient field + OK btn slide out under it."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
def test_recipient_field_hidden_until_click(self):
"""Pre-click: the field is in DOM but visually closed (e.g. width 0
or transform scaleX(0)) assertion checks it doesn't take its full
viewport-spanning width yet."""
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
# The field+OK panel is rendered (so JS can transition it) but should
# be in a closed state — assert the panel container exists and the
# input is not displayed at full width (a CSS-driven slide-out).
panel = self.browser.find_element(By.ID, "id_buddy_panel")
panel = self.browser.find_element(By.ID, "id_bud_panel")
# Before click, panel visible-width should be < viewport / 2 (closed)
viewport_w = self.browser.execute_script("return window.innerWidth;")
self.assertLess(panel.size["width"], viewport_w / 2)
def test_click_buddy_btn_reveals_recipient_field_and_ok_btn(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
def test_click_bud_btn_reveals_recipient_field_and_ok_btn(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
self.assertTrue(recipient.is_displayed())
# OK btn is .btn.btn-confirm tacked onto the panel — not a big SHARE
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_buddy_panel .btn.btn-confirm")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm")
self.assertEqual(ok.text.strip().upper(), "OK")
# Buddy btn picks up .active when open (mirror kit-btn pattern)
# Bud btn picks up .active when open (mirror kit-btn pattern)
self.assertIn("active", btn.get_attribute("class"))
def test_panel_spans_almost_full_viewport_when_open(self):
"""When open, the panel spans 100vw - 3rem (1.5rem each side)."""
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
panel = self.browser.find_element(By.ID, "id_buddy_panel")
panel = self.browser.find_element(By.ID, "id_bud_panel")
# Wait for the slide-out transition to settle
self.wait_for(lambda: self.assertGreater(
panel.size["width"],
@@ -222,32 +222,32 @@ class BuddyBtnSlideOutTest(FunctionalTest):
))
def test_recipient_input_has_left_padding_so_glyph_doesnt_overlap(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
pad = self.browser.execute_script(
"return parseFloat(getComputedStyle(arguments[0]).paddingLeft);",
recipient,
)
# At least 2.5rem (40px-ish) so the buddy glyph (3rem circle) doesn't
# At least 2.5rem (40px-ish) so the bud glyph (3rem circle) doesn't
# overlap the placeholder/typed text.
self.assertGreaterEqual(pad, 32)
class BuddyKitMutualExclusionTest(FunctionalTest):
"""When kit btn is active, buddy btn fades to 0 — and vice-versa."""
class BudKitMutualExclusionTest(FunctionalTest):
"""When kit btn is active, bud btn fades to 0 — and vice-versa."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
def test_buddy_active_fades_kit_btn(self):
buddy = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
def test_bud_active_fades_kit_btn(self):
bud = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
kit = self.browser.find_element(By.ID, "id_kit_btn")
buddy.click()
bud.click()
self.wait_for(lambda: self.assertEqual(
self.browser.execute_script(
"return parseFloat(getComputedStyle(arguments[0]).opacity);",
@@ -256,32 +256,32 @@ class BuddyKitMutualExclusionTest(FunctionalTest):
0.0,
))
def test_kit_active_fades_buddy_btn(self):
def test_kit_active_fades_bud_btn(self):
kit = self.wait_for(lambda: self.browser.find_element(By.ID, "id_kit_btn"))
buddy = self.browser.find_element(By.ID, "id_buddy_btn")
bud = self.browser.find_element(By.ID, "id_bud_btn")
kit.click()
self.wait_for(lambda: self.assertEqual(
self.browser.execute_script(
"return parseFloat(getComputedStyle(arguments[0]).opacity);",
buddy,
bud,
),
0.0,
))
class BuddyBtnDismissTest(FunctionalTest):
class BudBtnDismissTest(FunctionalTest):
"""Click outside / Escape closes the panel; field is cleared; reopening
shows the placeholder, not the previously-typed value."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
def _open_and_type(self, text):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
recipient.send_keys(text)
@@ -310,24 +310,24 @@ class BuddyBtnDismissTest(FunctionalTest):
self.assertEqual(recipient.get_attribute("value"), "")
class BuddyBtnOkSubmitsAsyncShareTest(FunctionalTest):
class BudBtnOkSubmitsAsyncShareTest(FunctionalTest):
"""OK → POST share-post (Accept:application/json) → Brief banner +
recipient chip appended; field clears; panel closes."""
def setUp(self):
super().setUp()
self.sharer = User.objects.create(email="buddy@test.io")
self.sharer = User.objects.create(email="bud@test.io")
self.recipient = User.objects.create(email="alice@test.io")
self.post = _seed_a_post(self.sharer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")
def test_ok_creates_brief_appends_line_and_chip(self):
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient_input = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
recipient_input.send_keys("alice@test.io")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_buddy_panel .btn.btn-confirm")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm")
ok.click()
# 1. Brief is created server-side
@@ -358,9 +358,9 @@ class PostHtmlAperturePageClassTest(FunctionalTest):
slug="my-posts",
defaults={"name": "My Posts", "grid_cols": 4, "grid_rows": 3, "context": "billboard"},
)
self.gamer = User.objects.create(email="buddy@test.io")
self.gamer = User.objects.create(email="bud@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
self.create_pre_authenticated_session("bud@test.io")
def test_post_html_body_carries_billboard_or_post_page_class(self):
self.browser.get(self.live_server_url + f"/billboard/post/{self.post.id}/")

View File

@@ -134,7 +134,7 @@ class PronounsAppletFlowTest(FunctionalTest):
# Billboard applets — page renders blank without these
for slug, name, cols, rows in [
("my-scrolls", "My Scrolls", 4, 3),
("my-buddies", "My Buddies", 4, 3),
("my-buds", "My Buds", 4, 3),
("most-recent-scroll", "Most Recent Scroll", 8, 6),
]:
Applet.objects.get_or_create(

View File

@@ -1,6 +1,6 @@
"""FT for the My Buddies page — buddy btn + slide-out add flow.
"""FT for the My Buds page — bud btn + slide-out add flow.
Phase 1 of the buddies sprint: explicit add via my_buddies.html. Phase 2
Phase 1 of the buds sprint: explicit add via my_buds.html. Phase 2
will layer autocomplete (sky-place-style top-3 username suggestions) and
implicit auto-add on post-share / gate-invite.
"""
@@ -12,64 +12,64 @@ from apps.lyric.models import User
from .base import FunctionalTest
class MyBuddiesPageTest(FunctionalTest):
class MyBudsPageTest(FunctionalTest):
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="me@test.io", username="me")
self.alice = User.objects.create(email="alice@test.io", username="alice")
self.create_pre_authenticated_session("me@test.io")
def test_renders_existing_buddies(self):
"""Pre-existing buddies show up as entries on first render."""
self.gamer.buddies.add(self.alice)
self.browser.get(self.live_server_url + "/billboard/my-buddies/")
def test_renders_existing_buds(self):
"""Pre-existing buds show up as entries on first render."""
self.gamer.buds.add(self.alice)
self.browser.get(self.live_server_url + "/billboard/my-buds/")
entry = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".buddy-entry .buddy-name")
lambda: self.browser.find_element(By.CSS_SELECTOR, ".bud-entry .bud-name")
)
self.assertEqual(entry.text, "alice")
def test_empty_state_when_no_buddies(self):
self.browser.get(self.live_server_url + "/billboard/my-buddies/")
def test_empty_state_when_no_buds(self):
self.browser.get(self.live_server_url + "/billboard/my-buds/")
empty = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".buddy-entry--empty")
lambda: self.browser.find_element(By.CSS_SELECTOR, ".applet-list-entry--empty")
)
self.assertIn("No buddies yet", empty.text)
self.assertIn("No buds yet", empty.text)
def test_add_buddy_via_buddy_btn_appends_entry(self):
self.browser.get(self.live_server_url + "/billboard/my-buddies/")
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
def test_add_bud_via_bud_btn_appends_entry(self):
self.browser.get(self.live_server_url + "/billboard/my-buds/")
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
recipient.send_keys("alice@test.io")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_buddy_panel .btn.btn-confirm")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm")
ok.click()
# New entry appears w. alice's username (not the bare email)
self.wait_for(lambda: self.assertEqual(
self.browser.find_element(
By.CSS_SELECTOR, f".buddy-entry[data-buddy-id='{self.alice.id}'] .buddy-name"
By.CSS_SELECTOR, f".bud-entry[data-bud-id='{self.alice.id}'] .bud-name"
).text,
"alice",
))
# Server-side persisted
self.wait_for(lambda: self.assertIn(
self.alice, list(self.gamer.buddies.all())
self.alice, list(self.gamer.buds.all())
))
def test_add_unregistered_email_is_silent_noop(self):
self.browser.get(self.live_server_url + "/billboard/my-buddies/")
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_buddy_btn"))
self.browser.get(self.live_server_url + "/billboard/my-buds/")
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
recipient.send_keys("ghost@test.io")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_buddy_panel .btn.btn-confirm")
ok = self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm")
ok.click()
# Wait for the panel close (a positive signal the request landed)
self.wait_for(lambda: self.assertNotIn(
"active", btn.get_attribute("class")
))
# No entries beyond the empty-state row
entries = self.browser.find_elements(By.CSS_SELECTOR, ".buddy-entry:not(.buddy-entry--empty)")
# No bud entries (the empty-state row has its own --empty class)
entries = self.browser.find_elements(By.CSS_SELECTOR, ".bud-entry")
self.assertEqual(len(entries), 0)