functional_tests + CI: rename pass + structural consolidations + parallel test-FTs split — every FT file now starts with one of 6 prefixes (test_admin_* / test_bill_* / test_core_* / test_dash_* / test_game_room_* / test_trinket_*) plus the 4 page-roots test_billboard / test_dashboard / test_gameboard / test_jasmine, so the partition is unambiguous and stable for tooling (the previous mix of test_applet_*, test_room_*, test_component_*, ad-hoc names had no consistent grouping); session-side: merged test_gatekeeper_bud_btn.py into test_bud_btn.py (then user renamed to test_core_bud_btn.py) — both files drove the same #id_bud_btn UI in two contexts (post-share + gatekeeper invite) and shared the bud-btn.js skeleton, so consolidation was overdue; split test_component_cards_tarot.py into test_admin_tarot.py (just TarotAdminTest, sitting next to test_admin / test_admin_post_readonly) + 3 classes (TarotDeckTest / GameKitDeckSelectionTest / GameKitPageTest) appended to test_game_room_tray.py; updated stale test_bud_btn.py references in the test_core_bud_btn.py docstring + test_admin_post_readonly.py comment to point at the new filename; user-driven renames (22 files): test_applet_my_notes/posts → test_bill_my_*, test_applet_new_post[_line_validation] → test_bill_new_post[_line_validation], test_applet_my_sky → test_dash_my_sky, test_applet_palette → test_dash_palette, test_wallet → test_dash_wallet, test_login → test_core_login, test_navbar → test_core_navbar, test_sharing → test_core_sharing, test_layout_and_styling → test_core_styling, test_my_buds → test_bill_my_buds, test_bud_btn → test_core_bud_btn, test_deck_contribution → test_game_room_deck_contrib, test_game_invite → test_game_room_invite, test_room_gatekeeper → test_game_room_gatekeeper, test_room_role_select → test_game_room_select_role, test_room_sea_select → test_game_room_select_sea, test_room_sig_select → test_game_room_select_sig, test_room_sky_select → test_game_room_select_sky, test_room_tray → test_game_room_tray, test_component_tray_tooltip → test_game_room_tray_tooltip; the post_page.py / room_page.py helper modules from the May-12 sprint absorbed the cross-file FT imports that would otherwise have cascade-broken on these renames
.woodpecker/main.yaml — CI test-FTs step splits into parallel siblings test-FTs-non-room (22 files via `ls functional_tests/test_*.py | grep -v 'test_game_room_'`) + test-FTs-room (9 files via `ls functional_tests/test_game_room_*.py`); room cluster is the heaviest (~70% of the pre-split ~40-min wall-clock) and now runs concurrently w. the rest instead of in series; DAG explicit via depends_on on every step (Woodpecker mixes default-sequential w. depends_on awkwardly, so each step pins its prerequisite); collectstatic stays in test-two-browser-FTs only — the shared workspace propagates assets to both parallel FT steps, no race + no duplication; screendumps + build-and-push fan back in (depends_on both parallel steps); deploy-staging + deploy-prod depend on build-and-push smoke-import: 31/31 FT modules green after the rename pass 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:
137
src/functional_tests/test_bill_my_buds.py
Normal file
137
src/functional_tests/test_bill_my_buds.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""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")
|
||||
)
|
||||
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 username (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 ""
|
||||
))
|
||||
Reference in New Issue
Block a user