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

178 lines
7.7 KiB
Python
Raw Normal View History

gatekeeper invite ports to #id_bud_btn slide-out: drop the inline #id_invite_email form, add bud-invite panel for room owner during gate phase, async POST to invite_gamer w. autocomplete + symmetric buds auto-add + slide-down Brief banner — TDD - Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set. - epic.invite_gamer view refactor: • Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat). • Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username). • RoomInvite stores the resolved User's email (or raw input if unregistered). • Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User. • Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null. • Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before. • Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch. - _gatekeeper.html: gate-invite-panel block (lines 62-71) removed. - new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE. - room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner. - Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add). - test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable). - test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references). - 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper 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>
2026-05-09 00:59:54 -04:00
"""FT for the gatekeeper invite via #id_bud_btn slide-out.
Replaces the legacy inline `<form action="invite_gamer">` panel inside
the gatekeeper modal. The bud-btn lives at the upper-right corner of
the right sidebar (footer in landscape); slide-out hosts the email/
username field + OK btn. Submit fires async POST to
epic:invite_gamer w. Accept: application/json server returns
{brief, recipient_display}, JS shows the slide-down Brief banner.
"""
from selenium.webdriver.common.by import By
from apps.billboard.models import Brief
bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries `{already_present, recipient_display, recipient_user_id}`; bud-btn.js branches on `already_present` → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads `@<username> is already present`, NVM dismisses, FYI dismisses AND eases in the `.bud-duplicate-flash` class (`color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms`) onto the existing element (`.bud-entry .bud-name` / `.post-recipient[data-user-id=…]` / `.gate-slot.filled[data-user-id=…]`); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — `"0 failures" in result.text` matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. `re.search(r"(?<!\d)0 failures\b", …)` (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD 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-12 16:40:15 -04:00
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat
gatekeeper invite ports to #id_bud_btn slide-out: drop the inline #id_invite_email form, add bud-invite panel for room owner during gate phase, async POST to invite_gamer w. autocomplete + symmetric buds auto-add + slide-down Brief banner — TDD - Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set. - epic.invite_gamer view refactor: • Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat). • Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username). • RoomInvite stores the resolved User's email (or raw input if unregistered). • Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User. • Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null. • Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before. • Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch. - _gatekeeper.html: gate-invite-panel block (lines 62-71) removed. - new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE. - room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner. - Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add). - test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable). - test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references). - 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper 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>
2026-05-09 00:59:54 -04:00
from apps.lyric.models import User
from .base import FunctionalTest
class GatekeeperBudBtnPresenceTest(FunctionalTest):
"""The bud-btn renders for the room owner during gate phase, and is
absent for non-owners (friend invites are owner-only)."""
def setUp(self):
super().setUp()
self.owner = User.objects.create(email="owner@test.io", username="owner")
self.gamer = User.objects.create(email="gamer@test.io", username="gamer")
self.room = Room.objects.create(name="Bingobango", owner=self.owner)
self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
def test_bud_btn_renders_for_owner(self):
self.create_pre_authenticated_session("owner@test.io")
self.browser.get(self.room_url)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
def test_bud_btn_absent_for_non_owner(self):
# A registered non-owner viewer doesn't see the invite affordance.
self.create_pre_authenticated_session("gamer@test.io")
self.browser.get(self.room_url)
# Gatekeeper-specific element confirms page rendered
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal"))
self.assertFalse(self.browser.find_elements(By.ID, "id_bud_btn"))
def test_legacy_invite_email_input_is_gone(self):
"""Sanity: the old inline form has been removed."""
self.create_pre_authenticated_session("owner@test.io")
self.browser.get(self.room_url)
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal"))
self.assertFalse(self.browser.find_elements(By.ID, "id_invite_email"))
class GatekeeperBudBtnAsyncInviteTest(FunctionalTest):
"""OK on the bud-btn slide-out fires the async invite — RoomInvite
persisted, Brief w/ kind=GAME_INVITE created, slide-down banner shown."""
def setUp(self):
super().setUp()
self.owner = User.objects.create(email="owner@test.io", username="owner")
self.alice = User.objects.create(email="alice@test.io", username="alice")
self.room = Room.objects.create(name="Bingobango", owner=self.owner)
self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
self.create_pre_authenticated_session("owner@test.io")
self.browser.get(self.room_url)
def _open_panel_and_invite(self, recipient):
bud_btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn"))
bud_btn.click()
recipient_input = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_recipient")
)
recipient_input.send_keys(recipient)
self.browser.find_element(
By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
).click()
return bud_btn
def test_invite_creates_room_invite(self):
self._open_panel_and_invite("alice@test.io")
self.wait_for(lambda: self.assertEqual(
RoomInvite.objects.filter(
room=self.room, invitee_email="alice@test.io"
).count(),
1,
))
def test_invite_spawns_game_invite_brief(self):
self._open_panel_and_invite("alice@test.io")
self.wait_for(lambda: self.assertEqual(
Brief.objects.filter(
owner=self.owner, kind=Brief.KIND_GAME_INVITE,
).count(),
1,
))
def test_invite_renders_slide_down_banner(self):
self._open_panel_and_invite("alice@test.io")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner"))
def test_invite_closes_panel_after_success(self):
bud_btn = self._open_panel_and_invite("alice@test.io")
self.wait_for(lambda: self.assertNotIn("active", bud_btn.get_attribute("class")))
def test_invite_username_resolves_to_user_email(self):
"""Username-typed invite stores the resolved User's email."""
self._open_panel_and_invite("alice")
self.wait_for(lambda: self.assertEqual(
RoomInvite.objects.filter(
room=self.room, invitee_email="alice@test.io"
).count(),
1,
))
def test_invite_auto_adds_recipient_to_owner_buds(self):
self._open_panel_and_invite("alice@test.io")
self.wait_for(lambda: self.assertIn(
self.alice, list(self.owner.buds.all())
))
bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries `{already_present, recipient_display, recipient_user_id}`; bud-btn.js branches on `already_present` → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads `@<username> is already present`, NVM dismisses, FYI dismisses AND eases in the `.bud-duplicate-flash` class (`color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms`) onto the existing element (`.bud-entry .bud-name` / `.post-recipient[data-user-id=…]` / `.gate-slot.filled[data-user-id=…]`); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — `"0 failures" in result.text` matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. `re.search(r"(?<!\d)0 failures\b", …)` (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD 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-12 16:40:15 -04:00
class GatekeeperBudBtnDuplicateInviteErrorTest(FunctionalTest):
"""Re-inviting a recipient already seated in the room triggers the
error Brief titled `@<username> is already present`. FYI on the Brief
dismisses + adds .bud-duplicate-flash to the existing
.gate-slot.filled[data-user-id=] element. Pending-but-unseated
duplicates also surface the Brief but FYI has no slot to highlight."""
def setUp(self):
super().setUp()
self.owner = User.objects.create(email="owner@test.io", username="owner")
self.alice = User.objects.create(email="alice@test.io", username="alice")
self.room = Room.objects.create(name="Dup Room", owner=self.owner)
# Seat alice via a GateSlot — _gate_positions renders .gate-slot.filled
# cells from GateSlot records (TableSeat spins up later at SIG SELECT),
# so the duplicate-highlight target lives there during gatekeeper phase.
GateSlot.objects.create(
room=self.room, gamer=self.alice, slot_number=1,
status=GateSlot.FILLED,
)
self.room_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
self.create_pre_authenticated_session("owner@test.io")
self.browser.get(self.room_url)
def test_duplicate_invite_shows_error_brief_and_fyi_flashes_slot(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"))
recipient.send_keys("alice@test.io")
self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm").click()
title = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".note-banner--duplicate .note-banner__title"
))
self.assertEqual(title.text, "@alice is already present")
# No new RoomInvite or Brief persisted server-side on duplicate
self.assertFalse(RoomInvite.objects.filter(
room=self.room, invitee_email="alice@test.io",
).exists())
self.assertEqual(
Brief.objects.filter(owner=self.owner, kind=Brief.KIND_GAME_INVITE).count(),
0,
)
slot = self.browser.find_element(
By.CSS_SELECTOR, f".gate-slot.filled[data-user-id='{self.alice.id}']"
)
self.assertNotIn("bud-duplicate-flash", slot.get_attribute("class") or "")
self.browser.find_element(
By.CSS_SELECTOR, ".note-banner--duplicate .note-banner__fyi"
).click()
self.wait_for(lambda: self.assertEqual(
self.browser.find_elements(By.CSS_SELECTOR, ".note-banner--duplicate"),
[],
))
self.wait_for(lambda: self.assertIn(
"bud-duplicate-flash", slot.get_attribute("class") or ""
))