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:
339
src/functional_tests/test_dash_palette.py
Normal file
339
src/functional_tests/test_dash_palette.py
Normal file
@@ -0,0 +1,339 @@
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from apps.applets.models import Applet
|
||||
from apps.lyric.models import User
|
||||
|
||||
from .base import FunctionalTest
|
||||
|
||||
|
||||
def _setup_palette_applet():
|
||||
Applet.objects.get_or_create(
|
||||
slug="palette",
|
||||
defaults={"name": "Palette", "context": "dashboard"},
|
||||
)
|
||||
|
||||
|
||||
class PalettePreviewTest(FunctionalTest):
|
||||
"""Clicking a swatch previews the palette on the whole body."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
_setup_palette_applet()
|
||||
User.objects.get_or_create(email="preview@test.io")
|
||||
self.create_pre_authenticated_session("preview@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
def _click_non_active_swatch(self):
|
||||
swatch = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".palette-item:not(:has(.swatch.active)) .swatch",
|
||||
)
|
||||
)
|
||||
swatch.click()
|
||||
return swatch
|
||||
|
||||
def test_clicking_swatch_adds_preview_class_to_body(self):
|
||||
swatch = self._click_non_active_swatch()
|
||||
palette_name = swatch.get_attribute("data-palette")
|
||||
self.wait_for(
|
||||
lambda: self.assertIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
|
||||
def test_body_has_only_preview_palette_during_preview(self):
|
||||
# Original palette class is swapped out — only the preview class is on body
|
||||
swatch = self._click_non_active_swatch()
|
||||
palette_name = swatch.get_attribute("data-palette")
|
||||
self.wait_for(
|
||||
lambda: self.assertIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertNotIn(
|
||||
"palette-default",
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
|
||||
def test_clicking_elsewhere_reverts_body_to_original_palette(self):
|
||||
swatch = self._click_non_active_swatch()
|
||||
palette_name = swatch.get_attribute("data-palette")
|
||||
self.wait_for(
|
||||
lambda: self.assertIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
# Click outside the applet
|
||||
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertNotIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
# Original palette restored
|
||||
self.assertIn(
|
||||
"palette-default",
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
|
||||
def test_swatch_gets_previewing_class_on_click(self):
|
||||
self._click_non_active_swatch()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.previewing")
|
||||
)
|
||||
|
||||
def test_previewing_class_removed_on_dismiss(self):
|
||||
self._click_non_active_swatch()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.previewing")
|
||||
)
|
||||
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
len(self.browser.find_elements(By.CSS_SELECTOR, ".swatch.previewing")),
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
def test_auto_dismiss_after_ten_seconds(self):
|
||||
swatch = self._click_non_active_swatch()
|
||||
palette_name = swatch.get_attribute("data-palette")
|
||||
self.wait_for(
|
||||
lambda: self.assertIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
# After 10s the preview should clear automatically
|
||||
self.wait_for_slow(
|
||||
lambda: self.assertNotIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
),
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
|
||||
class PaletteOkButtonTest(FunctionalTest):
|
||||
"""OK btn (unlocked) / × btn (locked) appear centered on clicked swatch."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
_setup_palette_applet()
|
||||
User.objects.get_or_create(email="okbtn@test.io")
|
||||
self.create_pre_authenticated_session("okbtn@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
def test_ok_btn_absent_before_click(self):
|
||||
btns = self.browser.find_elements(By.CSS_SELECTOR, ".swatch .btn-confirm")
|
||||
self.assertEqual(len([b for b in btns if b.is_displayed()]), 0)
|
||||
|
||||
def test_clicking_unlocked_swatch_shows_ok_btn_inside_swatch(self):
|
||||
swatch = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||
)
|
||||
)
|
||||
swatch.click()
|
||||
ok = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||
)
|
||||
)
|
||||
self.assertTrue(ok.is_displayed())
|
||||
|
||||
def test_clicking_locked_swatch_shows_disabled_times_btn(self):
|
||||
locked = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.locked")
|
||||
)
|
||||
locked.click()
|
||||
disabled = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".swatch.previewing .btn-disabled"
|
||||
)
|
||||
)
|
||||
self.assertIn("×", disabled.text)
|
||||
|
||||
def test_clicking_ok_commits_palette_and_no_reload(self):
|
||||
self.browser.execute_script("window._no_reload = true")
|
||||
swatch = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||
)
|
||||
)
|
||||
palette_name = swatch.get_attribute("data-palette")
|
||||
swatch.click()
|
||||
|
||||
ok = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||
)
|
||||
)
|
||||
ok.click()
|
||||
|
||||
# Body ends up with only the new palette (preview cleared, committed)
|
||||
self.wait_for(
|
||||
lambda: self.assertNotIn(
|
||||
"palette-default",
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertIn(
|
||||
palette_name,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
)
|
||||
self.assertTrue(self.browser.execute_script("return window._no_reload === true"))
|
||||
|
||||
def test_btn_disappears_on_dismiss(self):
|
||||
swatch = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||
)
|
||||
)
|
||||
swatch.click()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||
)
|
||||
)
|
||||
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
len([b for b in self.browser.find_elements(
|
||||
By.CSS_SELECTOR, ".swatch .btn-confirm"
|
||||
) if b.is_displayed()]),
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
class PaletteTooltipTest(FunctionalTest):
|
||||
"""Clicking a swatch shows a tooltip in #id_tooltip_portal."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
_setup_palette_applet()
|
||||
self.user, _ = User.objects.get_or_create(email="palettett@test.io")
|
||||
self.create_pre_authenticated_session("palettett@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
def _click_swatch(self, selector=".swatch"):
|
||||
swatch = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, selector)
|
||||
)
|
||||
swatch.click()
|
||||
return swatch
|
||||
|
||||
def test_clicking_swatch_shows_tooltip_portal(self):
|
||||
self._click_swatch()
|
||||
portal = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||
)
|
||||
self.assertTrue(portal.is_displayed())
|
||||
|
||||
def test_tooltip_title_is_colored_ter_user(self):
|
||||
self._click_swatch()
|
||||
title = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tooltip_portal .tt-title"
|
||||
)
|
||||
)
|
||||
ter_user = self.browser.execute_script(
|
||||
"return getComputedStyle(document.body).getPropertyValue('--terUser').trim()"
|
||||
)
|
||||
r, g, b = [int(x.strip()) for x in ter_user.split(",")]
|
||||
color = self.browser.execute_script(
|
||||
"return getComputedStyle(arguments[0]).color", title
|
||||
)
|
||||
self.assertIn(f"rgb({r}, {g}, {b})", color)
|
||||
|
||||
def test_tooltip_shows_description(self):
|
||||
self._click_swatch()
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tooltip_portal .tt-description"
|
||||
)
|
||||
)
|
||||
|
||||
def test_unlocked_swatch_shows_lock_open_and_unlocked(self):
|
||||
self._click_swatch(
|
||||
".palette-item:not(:has(.swatch.locked)) .swatch"
|
||||
)
|
||||
lock_line = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||
)
|
||||
)
|
||||
self.assertIn("Unlocked", lock_line.text)
|
||||
self.assertTrue(lock_line.find_elements(By.CSS_SELECTOR, ".fa-lock-open"))
|
||||
|
||||
def test_locked_swatch_shows_lock_and_locked(self):
|
||||
self._click_swatch(".swatch.locked")
|
||||
lock_line = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||
)
|
||||
)
|
||||
self.assertIn("Locked", lock_line.text)
|
||||
self.assertTrue(lock_line.find_elements(By.CSS_SELECTOR, ".fa-lock"))
|
||||
|
||||
def test_unlocked_tooltip_shows_default_label(self):
|
||||
self._click_swatch(".palette-item:not(:has(.swatch.locked)) .swatch")
|
||||
lock_line = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||
)
|
||||
)
|
||||
self.assertIn("Unlocked", lock_line.text)
|
||||
|
||||
def test_tooltip_dismisses_on_click_outside(self):
|
||||
self._click_swatch()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||
)
|
||||
)
|
||||
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SiteThemeTest(FunctionalTest):
|
||||
def test_page_renders_with_earthman_palette(self):
|
||||
self.browser.get(self.live_server_url)
|
||||
body = self.browser.find_element(By.TAG_NAME, "body")
|
||||
self.assertIn("palette-default", body.get_attribute("class"))
|
||||
|
||||
|
||||
class LightPaletteTest(FunctionalTest):
|
||||
def test_light_palette_tooltip_uses_white_background(self):
|
||||
user, _ = User.objects.get_or_create(email="light@example.com")
|
||||
user.palette = "palette-oblivion-light"
|
||||
user.save()
|
||||
self.create_pre_authenticated_session("light@example.com")
|
||||
|
||||
self.browser.get(self.live_server_url + "/dashboard/wallet/")
|
||||
|
||||
body = self.browser.find_element(By.TAG_NAME, "body")
|
||||
tooltip_bg = self.browser.execute_script(
|
||||
"return getComputedStyle(arguments[0]).getPropertyValue('--tooltip-bg').trim()",
|
||||
body,
|
||||
)
|
||||
self.assertEqual(tooltip_bg, "255, 255, 255")
|
||||
Reference in New Issue
Block a user