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>
This commit is contained in:
@@ -10,7 +10,7 @@ epic:invite_gamer w. Accept: application/json — server returns
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from apps.billboard.models import Brief
|
||||
from apps.epic.models import Room, RoomInvite
|
||||
from apps.epic.models import GateSlot, Room, RoomInvite, TableSeat
|
||||
from apps.lyric.models import User
|
||||
|
||||
from .base import FunctionalTest
|
||||
@@ -114,3 +114,64 @@ class GatekeeperBudBtnAsyncInviteTest(FunctionalTest):
|
||||
self.wait_for(lambda: self.assertIn(
|
||||
self.alice, list(self.owner.buds.all())
|
||||
))
|
||||
|
||||
|
||||
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 ""
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user