recognition: page, palette modal, & dashboard palette unlock — TDD
- billboard/recognition/ view + template; recognition/<slug>/set-palette endpoint (no trailing slash) - recognition.html: <template>-based modal (clone on open, remove on close — Selenium find_elements compatible) - recognition-page.js: image-box → modal → swatch preview → body-click restore → OK → confirm → POST set-palette - _palettes_for_user() replaces static PALETTES; Recognition.palette unlocks swatch + populates data-shoptalk - _unlocked_palettes_for_user() wires dynamic unlock check into set_palette view - _applet-palette.html: data-shoptalk from context instead of hard-coded "Placeholder" - _recognition.scss: banner, recog-list/item, image-box, modal, palette-confirm; :not([hidden]) pattern avoids display override - FT T2 split into T2a (banner → FYI → recog page), T2b (palette modal flow), T2c (dashboard palette applet) - 684 ITs green; 7 FTs green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,11 @@ outside of any game room.
|
||||
Two test classes — one per surface that can trigger the unlock — each asserting both
|
||||
the negative (disabled/incomplete save does nothing) and positive (first valid save
|
||||
fires the banner) conditions.
|
||||
|
||||
T2 (Dashboard full flow) is split across three focused tests:
|
||||
T2a — save → banner → FYI → recognition page item
|
||||
T2b — palette modal flow on recognition page
|
||||
T2c — dashboard palette applet reflects Recognition palette unlock
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
@@ -15,6 +20,7 @@ from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from apps.applets.models import Applet
|
||||
from apps.drama.models import Recognition
|
||||
from apps.lyric.models import User
|
||||
|
||||
from .base import FunctionalTest
|
||||
@@ -84,11 +90,7 @@ def _fill_valid_sky_form(browser):
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
"""Stargazer Recognition triggered from the My Sky applet.
|
||||
|
||||
T1 — incomplete save does not fire Recognition.
|
||||
T2 — first valid save fires banner, full flow to palette unlock.
|
||||
"""
|
||||
"""Stargazer Recognition triggered from the My Sky applet."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -102,8 +104,8 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
defaults={"name": "Palettes", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
|
||||
)
|
||||
Applet.objects.get_or_create(
|
||||
slug="recognition",
|
||||
defaults={"name": "Recognition", "grid_cols": 12, "grid_rows": 3, "context": "billboard"},
|
||||
slug="billboard-recognition",
|
||||
defaults={"name": "Recognition", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
|
||||
)
|
||||
self.gamer = User.objects.create(email="stargazer@test.io")
|
||||
|
||||
@@ -121,16 +123,11 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
self.assertIsNotNone(confirm_btn.get_attribute("disabled"))
|
||||
self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".recog-banner"))
|
||||
|
||||
# ── T2 ───────────────────────────────────────────────────────────────────
|
||||
# ── T2a ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_first_valid_save_from_applet_unlocks_stargazer_full_flow(self):
|
||||
"""First valid SAVE SKY from the My Sky applet:
|
||||
|
||||
banner slides in below the Dash h2 → FYI leads to /billboard/recognition/ →
|
||||
Recognition page shows Stargazer item → ? box opens palette modal →
|
||||
swatch preview → OK → confirm → chosen palette permanently unlocked in
|
||||
Palette applet on Dashboard.
|
||||
"""
|
||||
def test_first_valid_save_from_applet_fires_banner_and_leads_to_recognition_page(self):
|
||||
"""First valid SAVE SKY from the My Sky applet fires the Stargazer banner.
|
||||
FYI button navigates to /billboard/recognition/ showing the Stargazer item."""
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
@@ -142,7 +139,7 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
self.wait_for(lambda: self.assertIsNone(confirm_btn.get_attribute("disabled")))
|
||||
confirm_btn.click()
|
||||
|
||||
# --- Banner slides in below the Dash h2 ---
|
||||
# Banner slides in below the Dash h2
|
||||
banner = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-banner")
|
||||
)
|
||||
@@ -153,16 +150,16 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__description")
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__timestamp")
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__image")
|
||||
banner.find_element(By.CSS_SELECTOR, ".btn.btn-danger") # NVM
|
||||
banner.find_element(By.CSS_SELECTOR, ".btn.btn-danger") # NVM
|
||||
fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-caution") # FYI
|
||||
|
||||
# --- FYI navigates to Recognition page ---
|
||||
# FYI navigates to Recognition page
|
||||
fyi.click()
|
||||
self.wait_for(
|
||||
lambda: self.assertRegex(self.browser.current_url, r"/billboard/recognition")
|
||||
)
|
||||
|
||||
# --- Recognition page: one Stargazer item ---
|
||||
# Recognition page: one Stargazer item
|
||||
item = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-list .recog-item")
|
||||
)
|
||||
@@ -171,9 +168,24 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__title").text,
|
||||
)
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__description")
|
||||
image_box = item.find_element(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
|
||||
# --- Clicking ? opens palette modal ---
|
||||
# ── T2b ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_recognition_page_palette_modal_flow(self):
|
||||
"""Recognition page palette modal: image-box opens modal, swatch preview,
|
||||
body-click restores modal, OK raises confirm submenu, confirm sets palette."""
|
||||
Recognition.objects.create(
|
||||
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
self.browser.get(self.live_server_url + "/billboard/recognition/")
|
||||
|
||||
image_box = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
)
|
||||
|
||||
# Clicking ? opens palette modal
|
||||
image_box.click()
|
||||
modal = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-palette-modal")
|
||||
@@ -181,7 +193,7 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
modal.find_element(By.CSS_SELECTOR, ".palette-bardo")
|
||||
modal.find_element(By.CSS_SELECTOR, ".palette-sheol")
|
||||
|
||||
# --- Clicking a swatch body previews the palette and dismisses the modal ---
|
||||
# Clicking a swatch body previews the palette and dismisses the modal
|
||||
bardo_body = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .recog-swatch-body")
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
@@ -191,20 +203,20 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".recog-palette-modal")
|
||||
))
|
||||
|
||||
# --- Clicking anything else ends preview and restores the modal ---
|
||||
# Clicking elsewhere ends preview and restores the modal
|
||||
self.browser.find_element(By.TAG_NAME, "body").click()
|
||||
modal = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-palette-modal")
|
||||
)
|
||||
|
||||
# --- Clicking OK on the swatch raises a confirmation submenu ---
|
||||
# Clicking OK on the swatch raises a confirmation submenu
|
||||
ok_btn = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .btn.btn-confirm")
|
||||
ok_btn.click()
|
||||
confirm_menu = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-palette-confirm")
|
||||
)
|
||||
|
||||
# --- Confirming sets palette, closes modal, replaces ? box ---
|
||||
# Confirming sets palette, closes modal, replaces image-box with palette swatch
|
||||
confirm_menu.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
|
||||
self.wait_for(lambda: self.assertFalse(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".recog-palette-modal")
|
||||
@@ -217,8 +229,18 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
item.find_elements(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
)
|
||||
|
||||
# --- Dashboard: Palette applet shows bardo permanently unlocked ---
|
||||
# ── T2c ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_dashboard_palette_applet_reflects_recognition_palette_unlock(self):
|
||||
"""After palette unlock via Recognition, the Dashboard Palette applet shows
|
||||
the palette swatch as unlocked with Stargazer shoptalk."""
|
||||
Recognition.objects.create(
|
||||
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
palette_applet = self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_applet_palette")
|
||||
)
|
||||
@@ -227,10 +249,7 @@ class StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
bardo_ok = bardo.find_element(By.CSS_SELECTOR, ".palette-ok")
|
||||
self.assertIn("btn-confirm", bardo_ok.get_attribute("class"))
|
||||
self.assertNotIn("btn-disabled", bardo_ok.get_attribute("class"))
|
||||
self.assertIn(
|
||||
"Stargazer",
|
||||
bardo.get_attribute("data-shoptalk"),
|
||||
)
|
||||
self.assertIn("Stargazer", bardo.get_attribute("data-shoptalk"))
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -253,8 +272,8 @@ class StargazerRecognitionFromSkyPageTest(FunctionalTest):
|
||||
defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
|
||||
)
|
||||
Applet.objects.get_or_create(
|
||||
slug="recognition",
|
||||
defaults={"name": "Recognition", "grid_cols": 12, "grid_rows": 3, "context": "billboard"},
|
||||
slug="billboard-recognition",
|
||||
defaults={"name": "Recognition", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
|
||||
)
|
||||
self.gamer = User.objects.create(email="stargazer@test.io")
|
||||
self.sky_url = self.live_server_url + "/dashboard/sky/"
|
||||
@@ -307,7 +326,6 @@ class StargazerRecognitionFromSkyPageTest(FunctionalTest):
|
||||
def test_already_earned_recognition_does_not_show_banner_on_subsequent_save(self):
|
||||
"""When Stargazer is already in the database for this user, a valid sky
|
||||
save does not fire another Recognition banner."""
|
||||
from apps.drama.models import Recognition
|
||||
Recognition.objects.create(
|
||||
user=self.gamer,
|
||||
slug="stargazer",
|
||||
|
||||
Reference in New Issue
Block a user