140 lines
6.3 KiB
Python
140 lines
6.3 KiB
Python
"""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 — `@<username>` 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 ""
|
|
))
|