PALETTE: swatch preview + tooltip + OK commit — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

Clicking a swatch instantly swaps the body palette class for a live
preview; OK commits silently (POST, no reload); click-elsewhere or
10 s auto-dismiss reverts. Tooltip portal shows label, shoptalk,
lock state. Locked swatches show × (disabled). 20 FTs green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-18 02:05:27 -04:00
parent 6e995647e4
commit 122de3bc80
7 changed files with 488 additions and 46 deletions

View File

@@ -1,3 +1,4 @@
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from apps.applets.models import Applet
@@ -6,31 +7,53 @@ from apps.lyric.models import User
from .base import FunctionalTest
class PaletteSwapTest(FunctionalTest):
def test_selecting_palette_updates_body_class_without_page_reload(self):
Applet.objects.get_or_create(slug="palette", defaults={
"name": "Palette", "context": "dashboard",
})
user, _ = User.objects.get_or_create(email="swap@test.io")
self.create_pre_authenticated_session("swap@test.io")
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)
body = self.browser.find_element(By.TAG_NAME, "body")
self.assertIn("palette-default", body.get_attribute("class"))
# Mark the window — this survives JS execution but is wiped on a real reload
self.browser.execute_script("window._no_reload_marker = true;")
# Click OK on a non-active palette
btn = self.wait_for(
def _click_non_active_swatch(self):
swatch = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
".palette-item:has(.swatch:not(.active)) .btn-confirm",
".palette-item:not(:has(.swatch.active)) .swatch",
)
)
btn.click()
swatch.click()
return swatch
# Body palette class swaps without reload
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",
@@ -38,9 +61,257 @@ class PaletteSwapTest(FunctionalTest):
)
)
# Marker still present — no full page reload occurred
self.assertTrue(
self.browser.execute_script("return window._no_reload_marker === true;")
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_shoptalk(self):
self._click_swatch()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_tooltip_portal .tt-shoptalk"
)
)
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("Default", 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()
)
)