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>
This commit is contained in:
116
src/functional_tests/test_gatekeeper_bud_btn.py
Normal file
116
src/functional_tests/test_gatekeeper_bud_btn.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""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
|
||||
from apps.epic.models import Room, RoomInvite
|
||||
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())
|
||||
))
|
||||
Reference in New Issue
Block a user