Files
python-tdd/src/functional_tests/test_buddy_btn.py

382 lines
19 KiB
Python
Raw Normal View History

buddy btn sprint scaffolding: TDD spec + partial template + SCSS + page_class — 15/16 FTs green, 1 captured-red for post-compaction handoff Pre-compaction handoff for the bottom-left handshake btn that replaces the inline share form on post.html. The full spec lives in functional_tests/test_buddy_btn.py — running it as a red-first TDD checklist for the next agent (or future Disco) to pick up after compaction. Scaffolding landed: - functional_tests/test_buddy_btn.py — 16 tests across 6 classes covering presence-only-on-post.html (B1–B3), bottom-left fixed positioning matching kit-btn dimensions, slide-out panel structure (closed/open width, OK btn = .btn-confirm, recipient input left-padding clears the glyph), kit↔buddy mutual-exclusion via opacity, click-outside + Escape dismiss + clear, OK→async share creates Brief + appends Line + chips the recipient + closes the panel + clears the field, and post.html / my_posts.html body class picks up the aperture marker (page-billboard). - templates/apps/billboard/_partials/_buddy_panel.html — the partial: <button id="id_buddy_btn"><i class="fa-solid fa-handshake"></i></button> + #id_buddy_panel housing #id_recipient and #id_buddy_ok (.btn.btn-confirm). Inline JS mirrors the game-kit.js click/escape/click-outside pattern, toggles html.buddy-open + .active on the btn, intercepts OK to POST share-post w. Accept:application/json (reuses C3.b shape — line_text + brief.to_banner_dict() + recipient_display), appends the chip + line in-DOM, Brief.showBanner shows the slide-down banner, _close clears the input. - templates/apps/billboard/post.html — drops the inline #id_share_form / #id_recipient / SHARE-primary block + its JS; includes the buddy panel partial at the end of {% block content %}. - billboard.views.view_post + my_posts now set page_class="page-billboard" so the body class hooks into the aperture SCSS group (the user noted post.html wasn't in that group; this brings it in). - static_src/scss/_buddy.scss — new partial: #id_buddy_btn fixed bottom-left mirror of #id_kit_btn (3rem circle, secUser border, .active state, transition: opacity 0.15s); #id_buddy_panel slide-out spans calc(100vw - 3rem) (1.5rem each side w. landscape sidebar carve-outs), transform: scaleX(0)→1 from left center on html.buddy-open, opacity 0→1, the recipient input gets padding 0 1rem 0 3.5rem so the glyph doesn't overlap. Mutual exclusion: html.buddy-open #id_kit_btn → opacity:0; html:has(#id_kit_bag_dialog[open]) #id_buddy_btn → opacity:0 (uses :has() per project convention; no JS-side kit-open class needed). - core.scss imports buddy after game-kit. 15/16 FTs green; the lone red is BuddyBtnOkSubmitsAsyncShareTest.test_ok_creates_brief_appends_line_and_chip — server flow works (Brief is created, recipient chip + line append in DOM both visible in the screendump), only the .note-banner injection isn't surfacing on post.html. Likely cause: note.js inserts after the first <h2>, but post.html's only h2 is the rotated navbar header which is position:absolute, so the banner's geometry parents to that and falls outside the visible aperture. Two clean follow-ups for the post-compaction agent: (a) make Brief.showBanner pick a different anchor when h2.parentElement is position:absolute, or (b) define a #id_brief_banner_anchor in base.html under the page content and have showBanner prefer it. Also pending for post-compaction: update functional_tests.post_page.PostPage.share_post_with() to drive the new buddy-btn flow (click btn → type → click OK → wait for chip) so the legacy test_sharing FT keeps working — currently it still operates on the inline form selectors that no longer exist. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:00:28 -04:00
"""FT spec for the Buddy 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
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
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).
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
`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
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
quickly eases to opacity 0. Symmetric: when html.kit-open is set,
#id_buddy_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
(no Brief). Reopening shows the placeholder, not the prior typed value.
Escape key closes the slide-out (same as kit btn pattern).
post.html + my_posts.html should be aperture-styled (body.page-billboard
or new body.page-post if needed; the user noted post.html isn't
currently in the base/applet scss aperture group).
IMPLEMENTATION CHECKLIST (post-compaction)
1. Create templates/apps/billboard/_partials/_buddy_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" %}`.
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
(3rem circle, secUser border, .active state, etc.)
- #id_buddy_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; }
- 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:
- Mirror game-kit.js click/escape/click-outside pattern.
- Toggle html.buddy-open + #id_buddy_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).
- On dismiss-without-OK: clear input.value.
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).
8. Re-run test_sharing.SharingTest should still pass once the page-
object mirrors the new flow.
KNOWN AT TIME OF WRITING
- #id_kit_btn lives in templates/core/base.html (line ~55), styled in
static_src/scss/_game-kit.scss; toggled by apps/dashboard/static/.../game-kit.js.
- 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.
- body.page-billboard class is set by billboard:billboard view; post.html
needs it (or its own class) added in billboard.views.view_post.
"""
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from apps.applets.models import Applet
from apps.billboard.models import Brief, Line, Post
from apps.lyric.models import User
from .base import FunctionalTest
def _seed_a_post(user):
"""Create a Post w. one Line so view_post renders w/o redirect."""
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD - schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner). - lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed). - drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default. - billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title. - post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio). - SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square. - _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self). - FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors. - rootvars.scss: minor plutonium + fuschia tweaks (pre-existing). - 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
p = Post.objects.create(owner=user, title="seed line")
Line.objects.create(post=p, text="seed line", author=user)
buddy btn sprint scaffolding: TDD spec + partial template + SCSS + page_class — 15/16 FTs green, 1 captured-red for post-compaction handoff Pre-compaction handoff for the bottom-left handshake btn that replaces the inline share form on post.html. The full spec lives in functional_tests/test_buddy_btn.py — running it as a red-first TDD checklist for the next agent (or future Disco) to pick up after compaction. Scaffolding landed: - functional_tests/test_buddy_btn.py — 16 tests across 6 classes covering presence-only-on-post.html (B1–B3), bottom-left fixed positioning matching kit-btn dimensions, slide-out panel structure (closed/open width, OK btn = .btn-confirm, recipient input left-padding clears the glyph), kit↔buddy mutual-exclusion via opacity, click-outside + Escape dismiss + clear, OK→async share creates Brief + appends Line + chips the recipient + closes the panel + clears the field, and post.html / my_posts.html body class picks up the aperture marker (page-billboard). - templates/apps/billboard/_partials/_buddy_panel.html — the partial: <button id="id_buddy_btn"><i class="fa-solid fa-handshake"></i></button> + #id_buddy_panel housing #id_recipient and #id_buddy_ok (.btn.btn-confirm). Inline JS mirrors the game-kit.js click/escape/click-outside pattern, toggles html.buddy-open + .active on the btn, intercepts OK to POST share-post w. Accept:application/json (reuses C3.b shape — line_text + brief.to_banner_dict() + recipient_display), appends the chip + line in-DOM, Brief.showBanner shows the slide-down banner, _close clears the input. - templates/apps/billboard/post.html — drops the inline #id_share_form / #id_recipient / SHARE-primary block + its JS; includes the buddy panel partial at the end of {% block content %}. - billboard.views.view_post + my_posts now set page_class="page-billboard" so the body class hooks into the aperture SCSS group (the user noted post.html wasn't in that group; this brings it in). - static_src/scss/_buddy.scss — new partial: #id_buddy_btn fixed bottom-left mirror of #id_kit_btn (3rem circle, secUser border, .active state, transition: opacity 0.15s); #id_buddy_panel slide-out spans calc(100vw - 3rem) (1.5rem each side w. landscape sidebar carve-outs), transform: scaleX(0)→1 from left center on html.buddy-open, opacity 0→1, the recipient input gets padding 0 1rem 0 3.5rem so the glyph doesn't overlap. Mutual exclusion: html.buddy-open #id_kit_btn → opacity:0; html:has(#id_kit_bag_dialog[open]) #id_buddy_btn → opacity:0 (uses :has() per project convention; no JS-side kit-open class needed). - core.scss imports buddy after game-kit. 15/16 FTs green; the lone red is BuddyBtnOkSubmitsAsyncShareTest.test_ok_creates_brief_appends_line_and_chip — server flow works (Brief is created, recipient chip + line append in DOM both visible in the screendump), only the .note-banner injection isn't surfacing on post.html. Likely cause: note.js inserts after the first <h2>, but post.html's only h2 is the rotated navbar header which is position:absolute, so the banner's geometry parents to that and falls outside the visible aperture. Two clean follow-ups for the post-compaction agent: (a) make Brief.showBanner pick a different anchor when h2.parentElement is position:absolute, or (b) define a #id_brief_banner_anchor in base.html under the page content and have showBanner prefer it. Also pending for post-compaction: update functional_tests.post_page.PostPage.share_post_with() to drive the new buddy-btn flow (click btn → type → click OK → wait for chip) so the legacy test_sharing FT keeps working — currently it still operates on the inline form selectors that no longer exist. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:00:28 -04:00
return p
class BuddyBtnPresenceTest(FunctionalTest):
"""The buddy btn is post-only — present on post.html, absent elsewhere."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
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.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@test.io")
# ── B1 ──────────────────────────────────────────────────────────────────
def test_buddy_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"))
icon = btn.find_element(By.CSS_SELECTOR, "i.fa-solid.fa-handshake")
self.assertIsNotNone(icon)
# ── B2 ──────────────────────────────────────────────────────────────────
def test_buddy_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"))
# ── B3 ──────────────────────────────────────────────────────────────────
def test_buddy_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"))
class BuddyBtnPositionTest(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.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@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"))
cs = self.browser.execute_script(
"var s = getComputedStyle(arguments[0]); "
"return {position: s.position, bottom: s.bottom, left: s.left, right: s.right};",
btn,
)
self.assertEqual(cs["position"], "fixed")
# bottom + left are non-auto/0; right is auto (or large) so the btn
# hugs the bottom-left corner.
self.assertNotEqual(cs["bottom"], "auto")
self.assertNotEqual(cs["left"], "auto")
def test_buddy_btn_size_matches_kit_btn(self):
"""Same circular-3rem look — visually a mirror pair."""
btn = self.browser.find_element(By.ID, "id_buddy_btn")
kit = self.browser.find_element(By.ID, "id_kit_btn")
b_box = btn.size
k_box = kit.size
self.assertEqual(b_box["width"], k_box["width"])
self.assertEqual(b_box["height"], k_box["height"])
class BuddyBtnSlideOutTest(FunctionalTest):
"""Click the buddy btn → recipient field + OK btn slide out under it."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@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"))
# 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")
# 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"))
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")
self.assertEqual(ok.text.strip().upper(), "OK")
# Buddy 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.click()
panel = self.browser.find_element(By.ID, "id_buddy_panel")
# Wait for the slide-out transition to settle
self.wait_for(lambda: self.assertGreater(
panel.size["width"],
self.browser.execute_script("return window.innerWidth;") * 0.7,
))
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.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
# 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."""
def setUp(self):
super().setUp()
self.gamer = User.objects.create(email="buddy@test.io")
self.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@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"))
kit = self.browser.find_element(By.ID, "id_kit_btn")
buddy.click()
self.wait_for(lambda: self.assertEqual(
self.browser.execute_script(
"return parseFloat(getComputedStyle(arguments[0]).opacity);",
kit,
),
0.0,
))
def test_kit_active_fades_buddy_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")
kit.click()
self.wait_for(lambda: self.assertEqual(
self.browser.execute_script(
"return parseFloat(getComputedStyle(arguments[0]).opacity);",
buddy,
),
0.0,
))
class BuddyBtnDismissTest(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.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@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.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
recipient.send_keys(text)
return btn, recipient
def test_click_outside_dismisses_and_clears(self):
btn, recipient = self._open_and_type("alice@test.io")
# Click somewhere far from the panel + btn
body = self.browser.find_element(By.TAG_NAME, "body")
self.browser.execute_script(
"var e = new MouseEvent('click', {bubbles:true, clientX: 100, clientY: 100});"
"document.querySelector('h2').dispatchEvent(e);"
)
self.wait_for(lambda: self.assertNotIn("active", btn.get_attribute("class")))
# Reopening: input value should be cleared
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
self.assertEqual(recipient.get_attribute("value"), "")
def test_escape_dismisses_and_clears(self):
btn, recipient = self._open_and_type("alice@test.io")
recipient.send_keys(Keys.ESCAPE)
self.wait_for(lambda: self.assertNotIn("active", btn.get_attribute("class")))
btn.click()
recipient = self.wait_for(lambda: self.browser.find_element(By.ID, "id_recipient"))
self.assertEqual(recipient.get_attribute("value"), "")
class BuddyBtnOkSubmitsAsyncShareTest(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.recipient = User.objects.create(email="alice@test.io")
self.post = _seed_a_post(self.sharer)
self.create_pre_authenticated_session("buddy@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.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.click()
# 1. Brief is created server-side
self.wait_for(lambda: self.assertEqual(
Brief.objects.filter(owner=self.sharer, kind=Brief.KIND_SHARE_INVITE).count(),
1,
))
# 2. Banner appears
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner"))
# 3. .post-recipient chip appended
self.wait_for(lambda: self.assertTrue(
self.browser.find_elements(By.CSS_SELECTOR, ".post-recipient")
))
# 4. Field cleared
recipient_input = self.browser.find_element(By.ID, "id_recipient")
self.assertEqual(recipient_input.get_attribute("value"), "")
# 5. Panel closed (btn no longer .active)
self.wait_for(lambda: self.assertNotIn("active", btn.get_attribute("class")))
class PostHtmlAperturePageClassTest(FunctionalTest):
"""post.html and my_posts.html should pick up the aperture body class
so they share the navbar/footer scroll-lock pattern w. sky.html etc."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
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.post = _seed_a_post(self.gamer)
self.create_pre_authenticated_session("buddy@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}/")
body = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body"))
cls = body.get_attribute("class")
self.assertTrue(
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD - schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner). - lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed). - drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default. - billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title. - post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio). - SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square. - _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self). - FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors. - rootvars.scss: minor plutonium + fuschia tweaks (pre-existing). - 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
"page-billboard" in cls or "page-billpost" in cls,
buddy btn sprint scaffolding: TDD spec + partial template + SCSS + page_class — 15/16 FTs green, 1 captured-red for post-compaction handoff Pre-compaction handoff for the bottom-left handshake btn that replaces the inline share form on post.html. The full spec lives in functional_tests/test_buddy_btn.py — running it as a red-first TDD checklist for the next agent (or future Disco) to pick up after compaction. Scaffolding landed: - functional_tests/test_buddy_btn.py — 16 tests across 6 classes covering presence-only-on-post.html (B1–B3), bottom-left fixed positioning matching kit-btn dimensions, slide-out panel structure (closed/open width, OK btn = .btn-confirm, recipient input left-padding clears the glyph), kit↔buddy mutual-exclusion via opacity, click-outside + Escape dismiss + clear, OK→async share creates Brief + appends Line + chips the recipient + closes the panel + clears the field, and post.html / my_posts.html body class picks up the aperture marker (page-billboard). - templates/apps/billboard/_partials/_buddy_panel.html — the partial: <button id="id_buddy_btn"><i class="fa-solid fa-handshake"></i></button> + #id_buddy_panel housing #id_recipient and #id_buddy_ok (.btn.btn-confirm). Inline JS mirrors the game-kit.js click/escape/click-outside pattern, toggles html.buddy-open + .active on the btn, intercepts OK to POST share-post w. Accept:application/json (reuses C3.b shape — line_text + brief.to_banner_dict() + recipient_display), appends the chip + line in-DOM, Brief.showBanner shows the slide-down banner, _close clears the input. - templates/apps/billboard/post.html — drops the inline #id_share_form / #id_recipient / SHARE-primary block + its JS; includes the buddy panel partial at the end of {% block content %}. - billboard.views.view_post + my_posts now set page_class="page-billboard" so the body class hooks into the aperture SCSS group (the user noted post.html wasn't in that group; this brings it in). - static_src/scss/_buddy.scss — new partial: #id_buddy_btn fixed bottom-left mirror of #id_kit_btn (3rem circle, secUser border, .active state, transition: opacity 0.15s); #id_buddy_panel slide-out spans calc(100vw - 3rem) (1.5rem each side w. landscape sidebar carve-outs), transform: scaleX(0)→1 from left center on html.buddy-open, opacity 0→1, the recipient input gets padding 0 1rem 0 3.5rem so the glyph doesn't overlap. Mutual exclusion: html.buddy-open #id_kit_btn → opacity:0; html:has(#id_kit_bag_dialog[open]) #id_buddy_btn → opacity:0 (uses :has() per project convention; no JS-side kit-open class needed). - core.scss imports buddy after game-kit. 15/16 FTs green; the lone red is BuddyBtnOkSubmitsAsyncShareTest.test_ok_creates_brief_appends_line_and_chip — server flow works (Brief is created, recipient chip + line append in DOM both visible in the screendump), only the .note-banner injection isn't surfacing on post.html. Likely cause: note.js inserts after the first <h2>, but post.html's only h2 is the rotated navbar header which is position:absolute, so the banner's geometry parents to that and falls outside the visible aperture. Two clean follow-ups for the post-compaction agent: (a) make Brief.showBanner pick a different anchor when h2.parentElement is position:absolute, or (b) define a #id_brief_banner_anchor in base.html under the page content and have showBanner prefer it. Also pending for post-compaction: update functional_tests.post_page.PostPage.share_post_with() to drive the new buddy-btn flow (click btn → type → click OK → wait for chip) so the legacy test_sharing FT keeps working — currently it still operates on the inline form selectors that no longer exist. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:00:28 -04:00
f"post.html body class missing aperture marker: {cls!r}",
)
def test_my_posts_html_body_carries_billboard_or_post_page_class(self):
self.browser.get(self.live_server_url + f"/billboard/users/{self.gamer.id}/")
body = self.wait_for(lambda: self.browser.find_element(By.TAG_NAME, "body"))
cls = body.get_attribute("class")
self.assertTrue(
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD - schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner). - lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed). - drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default. - billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title. - post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio). - SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square. - _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self). - FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors. - rootvars.scss: minor plutonium + fuschia tweaks (pre-existing). - 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
"page-billboard" in cls or "page-billpost" in cls,
buddy btn sprint scaffolding: TDD spec + partial template + SCSS + page_class — 15/16 FTs green, 1 captured-red for post-compaction handoff Pre-compaction handoff for the bottom-left handshake btn that replaces the inline share form on post.html. The full spec lives in functional_tests/test_buddy_btn.py — running it as a red-first TDD checklist for the next agent (or future Disco) to pick up after compaction. Scaffolding landed: - functional_tests/test_buddy_btn.py — 16 tests across 6 classes covering presence-only-on-post.html (B1–B3), bottom-left fixed positioning matching kit-btn dimensions, slide-out panel structure (closed/open width, OK btn = .btn-confirm, recipient input left-padding clears the glyph), kit↔buddy mutual-exclusion via opacity, click-outside + Escape dismiss + clear, OK→async share creates Brief + appends Line + chips the recipient + closes the panel + clears the field, and post.html / my_posts.html body class picks up the aperture marker (page-billboard). - templates/apps/billboard/_partials/_buddy_panel.html — the partial: <button id="id_buddy_btn"><i class="fa-solid fa-handshake"></i></button> + #id_buddy_panel housing #id_recipient and #id_buddy_ok (.btn.btn-confirm). Inline JS mirrors the game-kit.js click/escape/click-outside pattern, toggles html.buddy-open + .active on the btn, intercepts OK to POST share-post w. Accept:application/json (reuses C3.b shape — line_text + brief.to_banner_dict() + recipient_display), appends the chip + line in-DOM, Brief.showBanner shows the slide-down banner, _close clears the input. - templates/apps/billboard/post.html — drops the inline #id_share_form / #id_recipient / SHARE-primary block + its JS; includes the buddy panel partial at the end of {% block content %}. - billboard.views.view_post + my_posts now set page_class="page-billboard" so the body class hooks into the aperture SCSS group (the user noted post.html wasn't in that group; this brings it in). - static_src/scss/_buddy.scss — new partial: #id_buddy_btn fixed bottom-left mirror of #id_kit_btn (3rem circle, secUser border, .active state, transition: opacity 0.15s); #id_buddy_panel slide-out spans calc(100vw - 3rem) (1.5rem each side w. landscape sidebar carve-outs), transform: scaleX(0)→1 from left center on html.buddy-open, opacity 0→1, the recipient input gets padding 0 1rem 0 3.5rem so the glyph doesn't overlap. Mutual exclusion: html.buddy-open #id_kit_btn → opacity:0; html:has(#id_kit_bag_dialog[open]) #id_buddy_btn → opacity:0 (uses :has() per project convention; no JS-side kit-open class needed). - core.scss imports buddy after game-kit. 15/16 FTs green; the lone red is BuddyBtnOkSubmitsAsyncShareTest.test_ok_creates_brief_appends_line_and_chip — server flow works (Brief is created, recipient chip + line append in DOM both visible in the screendump), only the .note-banner injection isn't surfacing on post.html. Likely cause: note.js inserts after the first <h2>, but post.html's only h2 is the rotated navbar header which is position:absolute, so the banner's geometry parents to that and falls outside the visible aperture. Two clean follow-ups for the post-compaction agent: (a) make Brief.showBanner pick a different anchor when h2.parentElement is position:absolute, or (b) define a #id_brief_banner_anchor in base.html under the page content and have showBanner prefer it. Also pending for post-compaction: update functional_tests.post_page.PostPage.share_post_with() to drive the new buddy-btn flow (click btn → type → click OK → wait for chip) so the legacy test_sharing FT keeps working — currently it still operates on the inline form selectors that no longer exist. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:00:28 -04:00
f"my_posts.html body class missing aperture marker: {cls!r}",
)