"""FT for the My Buds page — bud btn + slide-out add flow. 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. """ from selenium.webdriver.common.by import By from apps.lyric.models import User from .base import 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_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, ".bud-entry .bud-name") ) # Bud names render via `at_handle` filter — `@` w. an # `@` prefix on users w. a username; truncated email otherwise. self.assertEqual(entry.text, "@alice") 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, ".applet-list-entry--empty") ) self.assertIn("No buds yet", empty.text) 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_bud_panel .btn.btn-confirm") ok.click() # New entry appears w. alice's @-prefixed handle (not the bare email) self.wait_for(lambda: self.assertEqual( self.browser.find_element( 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.buds.all()) )) # "No buds yet" empty-state row dismisses async as the first bud lands # (shell partial renders w. .applet-list-entry--empty, not .bud-entry--empty) self.wait_for(lambda: self.assertEqual( self.browser.find_elements(By.CSS_SELECTOR, ".applet-list-entry--empty"), [], )) def test_no_autocomplete_suggestions_on_my_buds_page(self): """The bud-autocomplete pool is request.user.buds — surfacing buds you've already added on the page where you ADD new buds is just noise. Post-share + gatekeeper-invite panels keep it (re-sharing with an existing bud is a real flow); the My Buds add-panel drops the autocomplete bindings entirely. Absence of #id_bud_suggestions is the deterministic check (no debounce-window races).""" self.gamer.buds.add(self.alice) self.browser.get(self.live_server_url + "/billboard/my-buds/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_bud_btn")) self.assertEqual( self.browser.find_elements(By.ID, "id_bud_suggestions"), [], ) def test_add_unregistered_email_is_silent_noop(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("ghost@test.io") 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 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) def test_re_add_existing_bud_shows_already_present_brief_and_fyi_flashes_bud_name(self): """Re-adding an existing bud: server returns already_present=true; client renders the error Brief titled `@alice is already present`; clicking FYI dismisses the Brief AND adds .bud-duplicate-flash to the existing .bud-name cell.""" self.gamer.buds.add(self.alice) 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") self.browser.find_element(By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm").click() # Error Brief appears w. the duplicate title 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") # Only one .bud-entry — no second alice row appended self.assertEqual( len(self.browser.find_elements(By.CSS_SELECTOR, ".bud-entry")), 1, ) # Pre-flash: the .bud-name carries no flash class yet bud_name = self.browser.find_element( By.CSS_SELECTOR, f".bud-entry[data-bud-id='{self.alice.id}'] .bud-name" ) self.assertNotIn("bud-duplicate-flash", bud_name.get_attribute("class") or "") # Click FYI → Brief dismisses AND .bud-name gets the flash class 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", bud_name.get_attribute("class") or "" ))