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
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

.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:
Disco DeDisco
2026-05-12 20:06:25 -04:00
parent af1a90e76b
commit f9c05a3eba
27 changed files with 713 additions and 625 deletions

View 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 ""
))