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:
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 L→R.
|
||||
- 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}/")
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user