Baltimorean Note unlock loop — full UX from bawlmorese pronoun pick → Brief banner → DON → palette modal → dashboard swatch ; rootvars.scss adds the Baltimorean (Blt) hue family (red 200,16,46 / yellow 255,212,0 / white 255,255,255 / black 0,0,0 / purple 26,25,95 / orange 221,73,38 — Maryland-flag-derived plus a --sixBlt: 162,170,173 neutral) + two .palette-baltimore / .palette-maryland palette classes wiring those hues into the standard --priUser…--decUser slots; companion section-header rename "/* X Palette */" → "/* X Hues */" across rootvars to disambiguate raw hue families (Precious Metal / Cosmic Metal / Chroma / Earthman / Technoman / Inferno) from actual palette classes — section-comment-only, no rule-level change ; baltimorean entry added in 3 registries that drive the loop: _NOTE_DISPLAY (drama/models.py) — {"greeting": "Ayo,", "title": "Ard!"} so DON flips navbar Welcome, Earthman → Ayo, Ard!; _NOTE_TITLES (dashboard/views.py, user-pre-staged) — drives the "recognized via Baltimorean" copy on dashboard palette swatches; _NOTE_META (billboard/views.py) — Baltimorean title + the literal description "Aaron earned an iron urn." + palette_options [palette-baltimore, palette-maryland] feeding the my-notes swatch modal ; set_pronouns view rewired (dashboard/views.py) — first-time pronouns = bawlmorese selection calls Note.grant_if_new(user, "baltimorean") + returns {"brief": brief.to_banner_dict()} JSON @ 200; idempotent on repeat (the grant_if_new returns brief=None on second call so the 204 path resumes naturally); non-bawlmorese choices stay on the original 204 contract ; client wiring: game-kit.js pronouns commit() handles the 200 JSON path — resp.json().then(data => Brief.showBanner(data.brief)) instead of reload (reload would lose the just-fired banner); 204 still reloads to update active pronoun card; game_kit.html pulls in apps/dashboard/note.js so Brief is in scope on the Game Kit page (it wasn't before) ; Brief banner placement fix — note.js showBanner() now measures the .row .col-lg-6 h2 at render-time + sets inline top so the banner portals SQUARELY OVER the page h2 letter-spread wordmark instead of parking at the SCSS-default top: 0.5rem (which had it lurking above the wordmark area on every page); portrait-only (gated if window.innerWidth > window.innerHeight return) — landscape h2 lives in a writing-mode: vertical-rl fixed sidebar column + would need a full banner reorientation (writing-mode + flex-direction restyle of banner contents) to "overlay" sensibly, deferred to a follow-up sprint ; tests: drama/tests/unit/test_models.py (new file) — 5 UTs for _NOTE_DISPLAY[baltimorean] greeting/title/name + stargazer smoke tests; dashboard/tests/integrated/test_views.py — SetPronounsBawlmoreseUnlockTest (9 ITs covering first-bawlmorese-returns-200-w-brief / Note granted / title Ard! / square_url to /billboard/my-notes/ / idempotent on repeat / non-bawlmorese unaffected / bawlmorese-after-other still grants); existing SetPronounsViewTest.test_post_each_valid_choice docstring updated to flag the bawlmorese 200 branch ; functional_tests/test_bill_baltimorean.py (new file) — 6 FTs walking the full UX: T1 Game-Kit pronouns click → Brief banner w. Ard! title + Look! prose + ?-square + FYI nav; T2 idempotent repeat-click (no re-fire); T3 my-notes Baltimorean item carries the Aaron quote verbatim; T4 DON flips navbar greeting Welcome, Earthman → Ayo, Ard!; T5 palette modal offers Baltimore + Maryland swatches (and not Bardo/Sheol); T6 Baltimore swatch click previews → OK commits → dashboard Palette applet shows the swatch unlocked w. data-description carrying Baltimorean + non-empty data-unlocked-date + Note.palette = palette-baltimore in DB — all 6 green in 51s; full IT/UT sweep 997 → green in 45s — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,12 @@ _NOTE_META = {
|
||||
"palette_options": [],
|
||||
"swatch_label": "0",
|
||||
},
|
||||
"baltimorean": {
|
||||
"title": "Baltimorean",
|
||||
"description": '"Aaron earned an iron urn."',
|
||||
"palette_options": _palette_opts(["palette-baltimore", "palette-maryland"]),
|
||||
"swatch_label": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,31 @@ const Brief = (() => {
|
||||
} else {
|
||||
document.body.insertBefore(banner, document.body.firstChild);
|
||||
}
|
||||
_alignToH2(banner);
|
||||
}
|
||||
|
||||
// Slot the (position:fixed) banner over the page's h2 letter-spread
|
||||
// header so it visually portals across the wordmark rather than parking
|
||||
// at the top of the viewport.
|
||||
//
|
||||
// Portrait only: h2 is a horizontal wordmark in `.container .row`; its
|
||||
// top edge is navbar-height + row-padding which differs per viewport,
|
||||
// so we measure at render-time + set inline `top`.
|
||||
//
|
||||
// Landscape: h2 is rotated into the fixed --h2-col-w sidebar slot via
|
||||
// `writing-mode: vertical-rl`. A horizontally-flowing banner can't
|
||||
// "overlay" a vertical h2 without a full reorientation of its own
|
||||
// contents (writing-mode + flex-direction restyle). Defer that to a
|
||||
// landscape-vertical-banner sprint; for now landscape keeps the SCSS
|
||||
// default `top: 0.5rem` (banner reads as a horizontal strip near the
|
||||
// top of the page content area).
|
||||
function _alignToH2(banner) {
|
||||
if (window.innerWidth > window.innerHeight) return; // landscape: skip
|
||||
var h2 = document.querySelector('.row .col-lg-6 h2');
|
||||
if (!h2) return;
|
||||
var rect = h2.getBoundingClientRect();
|
||||
if (rect.height <= 0) return;
|
||||
banner.style.top = rect.top + 'px';
|
||||
}
|
||||
|
||||
function handleSaveResponse(data) {
|
||||
|
||||
@@ -714,6 +714,10 @@ class SetPronounsViewTest(TestCase):
|
||||
self.assertEqual(self.user.pronouns, "misogyny")
|
||||
|
||||
def test_post_each_valid_choice(self):
|
||||
"""Every PRONOUN_TABLE key persists. Note: bawlmorese also triggers the
|
||||
Baltimorean Note-unlock JSON response (status 200) on first call —
|
||||
covered separately by SetPronounsBawlmoreseUnlockTest. Here we just
|
||||
confirm the user.pronouns write lands for each choice."""
|
||||
for key in ("pluralism", "bawlmorese", "misogyny", "misandry", "misanthropy"):
|
||||
with self.subTest(key=key):
|
||||
self.client.post(self.url, data={"pronouns": key})
|
||||
@@ -735,3 +739,74 @@ class SetPronounsViewTest(TestCase):
|
||||
self.client.logout()
|
||||
response = self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class SetPronounsBawlmoreseUnlockTest(TestCase):
|
||||
"""First-time selection of `pronouns = bawlmorese` grants the Baltimorean
|
||||
Note + returns a Brief banner payload (similar to sky_save's stargazer
|
||||
unlock). Subsequent selections are idempotent — no new Note, no new
|
||||
Brief, status 204 like other pronoun choices."""
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="aaron@bawlmore.io")
|
||||
self.client.force_login(self.user)
|
||||
self.url = reverse("set_pronouns")
|
||||
|
||||
def test_first_bawlmorese_returns_200_with_brief_json(self):
|
||||
response = self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn("brief", data)
|
||||
self.assertIsNotNone(data["brief"])
|
||||
|
||||
def test_first_bawlmorese_grants_baltimorean_note(self):
|
||||
self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.assertTrue(
|
||||
Note.objects.filter(user=self.user, slug="baltimorean").exists()
|
||||
)
|
||||
|
||||
def test_brief_payload_carries_baltimorean_title(self):
|
||||
response = self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
brief = response.json()["brief"]
|
||||
self.assertEqual(brief["title"], "Ard!")
|
||||
|
||||
def test_brief_payload_square_url_jumps_to_my_notes(self):
|
||||
"""NOTE_UNLOCK Briefs carry a `square_url` pointing at /billboard/my-notes/
|
||||
so the banner's `?` square jumps straight to the user's Note list."""
|
||||
response = self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
brief = response.json()["brief"]
|
||||
self.assertEqual(brief["square_url"], "/billboard/my-notes/")
|
||||
|
||||
def test_second_bawlmorese_is_idempotent_no_new_note(self):
|
||||
self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.assertEqual(
|
||||
Note.objects.filter(user=self.user, slug="baltimorean").count(), 1
|
||||
)
|
||||
|
||||
def test_second_bawlmorese_returns_204(self):
|
||||
"""No fresh Brief on the repeat call — status returns to the no-payload
|
||||
204 that other pronoun choices use."""
|
||||
self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
response = self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_non_bawlmorese_choice_does_not_grant_baltimorean(self):
|
||||
self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
self.assertFalse(
|
||||
Note.objects.filter(user=self.user, slug="baltimorean").exists()
|
||||
)
|
||||
|
||||
def test_non_bawlmorese_choice_still_returns_204(self):
|
||||
response = self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_bawlmorese_after_other_choice_still_grants_note(self):
|
||||
"""User picks misogyny first, then bawlmorese — the first bawlmorese
|
||||
click is still the unlock moment regardless of prior pronoun history."""
|
||||
self.client.post(self.url, data={"pronouns": "misogyny"})
|
||||
response = self.client.post(self.url, data={"pronouns": "bawlmorese"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(
|
||||
Note.objects.filter(user=self.user, slug="baltimorean").exists()
|
||||
)
|
||||
|
||||
@@ -38,11 +38,14 @@ _PALETTE_DEFS = [
|
||||
{"name": "palette-inferno", "label": "Inferno", "locked": True},
|
||||
{"name": "palette-terrestre", "label": "Terrestre", "locked": True},
|
||||
{"name": "palette-celestia", "label": "Celestia", "locked": True},
|
||||
{"name": "palette-baltimore", "label": "Baltimore", "locked": True},
|
||||
{"name": "palette-maryland", "label": "Maryland", "locked": True},
|
||||
]
|
||||
_NOTE_TITLES = {
|
||||
"stargazer": "Stargazer",
|
||||
"schizo": "Schizo",
|
||||
"nomad": "Nomad",
|
||||
"baltimorean": "Baltimorean",
|
||||
}
|
||||
# Keep PALETTES as an alias used by views that don't have a request user.
|
||||
PALETTES = _PALETTE_DEFS
|
||||
@@ -131,6 +134,14 @@ def set_pronouns(request):
|
||||
return HttpResponse(status=400)
|
||||
request.user.pronouns = choice
|
||||
request.user.save(update_fields=["pronouns"])
|
||||
# bawlmorese is the pronoun-side trigger for the Baltimorean Note unlock —
|
||||
# mirrors sky_save's stargazer grant. Grant is idempotent (grant_if_new
|
||||
# returns no Brief on the second + later calls) so the 204 path resumes
|
||||
# naturally after the first unlock.
|
||||
if choice == "bawlmorese":
|
||||
_note, _created, brief = Note.grant_if_new(request.user, "baltimorean")
|
||||
if brief is not None:
|
||||
return JsonResponse({"brief": brief.to_banner_dict()})
|
||||
return HttpResponse(status=204)
|
||||
|
||||
@login_required(login_url="/")
|
||||
|
||||
@@ -212,6 +212,7 @@ _NOTE_DISPLAY = {
|
||||
"nomad": {"greeting": "Welcome,", "title": "Nomad"},
|
||||
"super-schizo": {"greeting": "21<span class='ord'>st</span> Century", "title": "Schizoid Man"},
|
||||
"super-nomad": {"greeting": "Howdy,", "title": "Stranger"},
|
||||
"baltimorean": {"greeting": "Ayo,", "title": "Ard!"},
|
||||
}
|
||||
|
||||
# Note slugs whose grant prose uses the long admin format ("The administration
|
||||
|
||||
0
src/apps/drama/tests/unit/__init__.py
Normal file
0
src/apps/drama/tests/unit/__init__.py
Normal file
39
src/apps/drama/tests/unit/test_models.py
Normal file
39
src/apps/drama/tests/unit/test_models.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.drama.models import Note
|
||||
|
||||
|
||||
def _note(slug):
|
||||
n = Note()
|
||||
n.slug = slug
|
||||
return n
|
||||
|
||||
|
||||
class NoteDisplayBaltimoreanTest(SimpleTestCase):
|
||||
"""`_NOTE_DISPLAY['baltimorean']` — greeting/title combo "Ayo, Ard!" replaces
|
||||
"Welcome, Earthman" once the user dons the Baltimorean Note (unlocked by
|
||||
first-time `pronouns = bawlmorese` selection)."""
|
||||
|
||||
def test_baltimorean_display_title_is_ard(self):
|
||||
self.assertEqual(_note("baltimorean").display_title, "Ard!")
|
||||
|
||||
def test_baltimorean_display_greeting_is_ayo(self):
|
||||
self.assertEqual(_note("baltimorean").display_greeting, "Ayo,")
|
||||
|
||||
def test_baltimorean_display_name_is_baltimorean(self):
|
||||
"""The my-notes heading label — `.title()` of the slug recovers the
|
||||
proper rendering for any slug without a `name` override in
|
||||
`_NOTE_DISPLAY`. Baltimorean has no override, so this should fall
|
||||
through to `slug.title()` → 'Baltimorean'."""
|
||||
self.assertEqual(_note("baltimorean").display_name, "Baltimorean")
|
||||
|
||||
|
||||
class NoteDisplayStargazerTest(SimpleTestCase):
|
||||
"""Smoke tests for the existing stargazer entry — pins the contract that
|
||||
new entries follow."""
|
||||
|
||||
def test_stargazer_display_title_is_stargazer(self):
|
||||
self.assertEqual(_note("stargazer").display_title, "Stargazer")
|
||||
|
||||
def test_stargazer_display_greeting_is_welcome(self):
|
||||
self.assertEqual(_note("stargazer").display_greeting, "Welcome,")
|
||||
@@ -450,6 +450,16 @@ var GameKit = (function () {
|
||||
credentials: 'same-origin',
|
||||
body: body.toString(),
|
||||
}).then(function (resp) {
|
||||
if (resp.status === 200) {
|
||||
// First-time `bawlmorese` selection — server returns the
|
||||
// Baltimorean Brief payload to render inline (no reload, or the
|
||||
// banner would be lost the moment the page navigates).
|
||||
return resp.json().then(function (data) {
|
||||
if (data && data.brief && typeof Brief !== 'undefined') {
|
||||
Brief.showBanner(data.brief);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (resp.status === 204) {
|
||||
// Reload so any provenance prose currently on the page renders
|
||||
// with the new pronouns and the .active class moves to the new card.
|
||||
|
||||
321
src/functional_tests/test_bill_baltimorean.py
Normal file
321
src/functional_tests/test_bill_baltimorean.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""Functional tests for the Baltimorean Note unlock loop.
|
||||
|
||||
Mirrors the Stargazer unlock pattern (see test_bill_my_notes.py) but the
|
||||
trigger lives on Game Kit's Pronouns applet instead of the My Sky save —
|
||||
selecting `bawlmorese` as the global pronoun for the first time grants the
|
||||
Baltimorean Note and fires the slide-down Brief banner.
|
||||
|
||||
End-to-end loop captured here:
|
||||
1. Game Kit → pronouns applet → click `bawlmorese` card → guard portal "OK"
|
||||
2. Brief banner slides in with title "Ard!" + Look! prose + FYI/NVM/?-square
|
||||
3. Navigate to /billboard/my-notes/ → Baltimorean item w. the Aaron quote
|
||||
4. DON the Baltimorean Note → navbar greeting flips
|
||||
"Welcome, Earthman" → "Ayo, Ard!"
|
||||
5. Click the ? swatch box → modal opens w. Baltimore + Maryland swatches
|
||||
6. Click a Baltimore swatch body → body class previews palette-baltimore
|
||||
7. Click OK in the confirm bar → palette commits (Note.palette saves,
|
||||
user.palette saves, ? box replaced by palette swatch)
|
||||
8. Navigate back to /dashboard/ → Palette applet shows Baltimore swatch
|
||||
unlocked + data-description carrying "Baltimorean" attribution
|
||||
|
||||
Each scenario is its own test method so a regression in any segment fails
|
||||
in isolation (and the slowest segments — Game Kit pronouns flip vs palette
|
||||
modal commit — run independently in --parallel).
|
||||
"""
|
||||
import time
|
||||
|
||||
from django.test import tag
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from apps.applets.models import Applet
|
||||
from apps.drama.models import Note
|
||||
from apps.lyric.models import User
|
||||
|
||||
from .base import FunctionalTest
|
||||
|
||||
|
||||
def _setup_applets():
|
||||
"""Both surfaces this loop visits need their applets registered."""
|
||||
Applet.objects.get_or_create(
|
||||
slug="pronouns",
|
||||
defaults={"name": "Pronouns", "grid_cols": 6, "grid_rows": 6, "context": "game-kit"},
|
||||
)
|
||||
Applet.objects.get_or_create(
|
||||
slug="palette",
|
||||
defaults={"name": "Palette", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
|
||||
)
|
||||
Applet.objects.get_or_create(
|
||||
slug="notes",
|
||||
defaults={"name": "Note", "grid_cols": 4, "grid_rows": 4, "context": "billboard"},
|
||||
)
|
||||
|
||||
|
||||
class BaltimoreanNoteFromGameKitTest(FunctionalTest):
|
||||
"""The full Baltimorean unlock loop. Setup creates a fresh gamer; each
|
||||
test seeds either nothing (the unlock-moment tests) or a pre-granted
|
||||
Note (the post-unlock tests) so the surfaces under test are isolated."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Portrait viewport — kit dialog + my-notes layouts both target the
|
||||
# narrow layout in their respective FT suites (mirrors test_dash_palette
|
||||
# + test_bill_my_notes conventions). 800x1200 keeps the navbar h1 +
|
||||
# guard portal + brief banner all in-frame.
|
||||
self.browser.set_window_size(800, 1200)
|
||||
_setup_applets()
|
||||
self.gamer = User.objects.create(email="ard@bawlmore.io")
|
||||
self.game_kit_url = self.live_server_url + "/gameboard/game-kit/"
|
||||
self.my_notes_url = self.live_server_url + "/billboard/my-notes/"
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def _click_bawlmorese_with_guard_ok(self):
|
||||
"""Game Kit pronouns flow: click bawlmorese card → guard "OK" → commit.
|
||||
Uses execute_script for the OK click to bypass any in-flight scroll-
|
||||
into-view contention (same pattern as test_core_bud_btn helpers)."""
|
||||
card = self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, '.gk-pronoun-card[data-pronoun="bawlmorese"]'
|
||||
))
|
||||
card.click()
|
||||
guard_ok = self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes"
|
||||
))
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
guard_ok,
|
||||
)
|
||||
|
||||
# ── T1 — Game-Kit pronouns click fires the Brief banner ─────────────
|
||||
|
||||
def test_first_bawlmorese_click_fires_baltimorean_brief_banner(self):
|
||||
"""First `bawlmorese` selection from Game Kit's Pronouns applet must
|
||||
slide in a Brief banner carrying the Baltimorean title + a square that
|
||||
jumps to my-notes. The FYI button navigates to the underlying Post."""
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.game_kit_url)
|
||||
|
||||
# No banner on the cold page
|
||||
self.assertFalse(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"),
|
||||
"Brief banner must not be present before the unlock click",
|
||||
)
|
||||
|
||||
self._click_bawlmorese_with_guard_ok()
|
||||
|
||||
banner = self.wait_for_slow(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".note-banner"
|
||||
))
|
||||
# The title slot carries the recognition title — for baltimorean
|
||||
# that's "Ard!" (vs stargazer's "Stargazer").
|
||||
self.assertIn(
|
||||
"Ard!",
|
||||
banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text,
|
||||
)
|
||||
# Look! prose lands in the description.
|
||||
desc_text = banner.find_element(
|
||||
By.CSS_SELECTOR, ".note-banner__description"
|
||||
).text
|
||||
self.assertIn("Look!", desc_text)
|
||||
# Banner gets a timestamp + the ? square anchored to /billboard/my-notes/.
|
||||
banner.find_element(By.CSS_SELECTOR, ".note-banner__timestamp")
|
||||
square = banner.find_element(By.CSS_SELECTOR, ".note-banner__image")
|
||||
self.assertEqual(square.tag_name, "a")
|
||||
self.assertEqual(
|
||||
square.get_attribute("href").rstrip("/"),
|
||||
self.live_server_url + "/billboard/my-notes",
|
||||
)
|
||||
# FYI navigates to the Brief's underlying Post detail.
|
||||
fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-info")
|
||||
fyi.click()
|
||||
self.wait_for(lambda: self.assertRegex(
|
||||
self.browser.current_url, r"/billboard/post/[0-9a-f-]+/"
|
||||
))
|
||||
|
||||
# ── T2 — Idempotence: re-selecting bawlmorese does NOT re-fire banner
|
||||
|
||||
def test_second_bawlmorese_click_does_not_re_fire_banner(self):
|
||||
"""The Baltimorean Note grant is idempotent — clicking bawlmorese
|
||||
again after it's already the user's pronouns + Note is granted must
|
||||
not slide in another banner."""
|
||||
# Pre-grant — short-circuits the Brief on the subsequent click.
|
||||
Note.objects.create(
|
||||
user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
|
||||
)
|
||||
self.gamer.pronouns = "bawlmorese"
|
||||
self.gamer.save()
|
||||
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.game_kit_url)
|
||||
self._click_bawlmorese_with_guard_ok()
|
||||
# The commit reloads — wait for the page to settle, then assert
|
||||
# no banner. Give the reload + render a beat.
|
||||
time.sleep(1.2)
|
||||
self.assertFalse(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"),
|
||||
"Repeat bawlmorese click must not re-fire the Brief banner",
|
||||
)
|
||||
|
||||
# ── T3 — my-notes page renders the Baltimorean item w. the Aaron quote
|
||||
|
||||
def test_my_notes_baltimorean_item_renders_aaron_quote(self):
|
||||
"""Once the Note exists, /billboard/my-notes/ shows a Baltimorean
|
||||
item whose description is literally the Aaron quote (the central
|
||||
joke of the Note)."""
|
||||
Note.objects.create(
|
||||
user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.my_notes_url)
|
||||
|
||||
item = self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, '.note-list .note-item[data-slug="baltimorean"]'
|
||||
))
|
||||
self.assertIn(
|
||||
"Baltimorean",
|
||||
item.find_element(By.CSS_SELECTOR, ".note-item__title").text,
|
||||
)
|
||||
# The Aaron quote is the description verbatim, including the
|
||||
# surrounding double-quotes that the user spec'd.
|
||||
self.assertIn(
|
||||
'"Aaron earned an iron urn."',
|
||||
item.find_element(By.CSS_SELECTOR, ".note-item__description").text,
|
||||
)
|
||||
|
||||
# ── T4 — DON flips the navbar greeting to "Ayo, Ard!"
|
||||
|
||||
def test_don_baltimorean_changes_navbar_greeting(self):
|
||||
"""Pressing DON on the Baltimorean note item rewrites the navbar h1
|
||||
greeting from "Welcome, Earthman" to "Ayo, Ard!"."""
|
||||
Note.objects.create(
|
||||
user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.my_notes_url)
|
||||
|
||||
# Pre-condition: cold navbar greeting
|
||||
prefix = self.browser.find_element(By.ID, "id_greeting_prefix")
|
||||
name = self.browser.find_element(By.ID, "id_greeting_name")
|
||||
self.assertEqual(prefix.text.strip().rstrip(","), "Welcome")
|
||||
self.assertEqual(name.text.strip(), "Earthman")
|
||||
|
||||
# Click-lock the item first (the note-page locks on a click before
|
||||
# the DON button becomes interactive — mirrors the test_bill_my_notes
|
||||
# opacity:0 pattern).
|
||||
item = self.browser.find_element(
|
||||
By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
|
||||
)
|
||||
item.click()
|
||||
don = item.find_element(By.CSS_SELECTOR, ".note-don-btn")
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
don,
|
||||
)
|
||||
|
||||
# The greeting flips client-side via the don_title JSON response.
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.find_element(By.ID, "id_greeting_prefix").text.strip().rstrip(","),
|
||||
"Ayo",
|
||||
))
|
||||
self.assertEqual(
|
||||
self.browser.find_element(By.ID, "id_greeting_name").text.strip(),
|
||||
"Ard!",
|
||||
)
|
||||
|
||||
# ── T5 — palette modal opens to Baltimore + Maryland swatches
|
||||
|
||||
def test_palette_modal_shows_baltimore_and_maryland_swatches(self):
|
||||
"""The ? swatch box opens a modal carrying exactly the two
|
||||
palettes Baltimorean unlocks: Baltimore + Maryland."""
|
||||
Note.objects.create(
|
||||
user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.my_notes_url)
|
||||
|
||||
item = self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
|
||||
))
|
||||
# Click-lock the item, then click the ? box.
|
||||
item.click()
|
||||
image_box = item.find_element(By.CSS_SELECTOR, ".note-item__image-box")
|
||||
image_box.click()
|
||||
modal = self.wait_for(lambda: item.find_element(
|
||||
By.CSS_SELECTOR, ".note-palette-modal"
|
||||
))
|
||||
# Both swatches present, each w. its human-readable label.
|
||||
labels = [
|
||||
el.text.strip()
|
||||
for el in modal.find_elements(By.CSS_SELECTOR, ".note-swatch-label")
|
||||
]
|
||||
self.assertIn("Baltimore", labels)
|
||||
self.assertIn("Maryland", labels)
|
||||
# Bardo + Sheol (stargazer's palettes) must NOT bleed through.
|
||||
self.assertNotIn("Bardo", labels)
|
||||
self.assertNotIn("Sheol", labels)
|
||||
|
||||
# ── T6 — Baltimore swatch commit + dashboard reflects unlock + ts
|
||||
|
||||
def test_palette_baltimore_commit_persists_and_unlocks_dashboard_swatch(self):
|
||||
"""Clicking the Baltimore swatch body previews palette-baltimore on
|
||||
the body; OK commits — Note.palette + user.palette both saved — so
|
||||
navigating to /dashboard/ shows palette-baltimore active + the
|
||||
Palette applet swatch unlocked with Baltimorean shoptalk +
|
||||
the unlocked-date timestamp wired through."""
|
||||
note = Note.objects.create(
|
||||
user=self.gamer, slug="baltimorean", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("ard@bawlmore.io")
|
||||
self.browser.get(self.my_notes_url)
|
||||
|
||||
item = self.wait_for(lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR, '.note-item[data-slug="baltimorean"]'
|
||||
))
|
||||
item.click()
|
||||
item.find_element(By.CSS_SELECTOR, ".note-item__image-box").click()
|
||||
modal = self.wait_for(lambda: item.find_element(
|
||||
By.CSS_SELECTOR, ".note-palette-modal"
|
||||
))
|
||||
baltimore_body = modal.find_element(
|
||||
By.CSS_SELECTOR, ".palette-baltimore .note-swatch-body"
|
||||
)
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
baltimore_body,
|
||||
)
|
||||
# Body previews the palette
|
||||
self.wait_for(lambda: self.assertIn(
|
||||
"palette-baltimore",
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
))
|
||||
# OK commits
|
||||
confirm = self.wait_for(lambda: item.find_element(
|
||||
By.CSS_SELECTOR, ".note-palette-confirm"
|
||||
))
|
||||
confirm.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
|
||||
self.wait_for(lambda: self.assertFalse(
|
||||
item.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
||||
))
|
||||
|
||||
# Navigate to dashboard — palette-baltimore persists on body +
|
||||
# the Palette applet swatch is unlocked w. the "recognized via
|
||||
# Baltimorean" data-description + a non-empty unlocked-date.
|
||||
self.browser.get(self.live_server_url)
|
||||
self.wait_for(lambda: self.assertIn(
|
||||
"palette-baltimore",
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
))
|
||||
palette_applet = self.wait_for(lambda: self.browser.find_element(
|
||||
By.ID, "id_applet_palette"
|
||||
))
|
||||
baltimore = palette_applet.find_element(
|
||||
By.CSS_SELECTOR, ".swatch.palette-baltimore"
|
||||
)
|
||||
self.assertNotIn("locked", baltimore.get_attribute("class"))
|
||||
self.assertIn("Baltimorean", baltimore.get_attribute("data-description"))
|
||||
# Unlocked-date carries the Note's earned_at as ISO — non-empty proves
|
||||
# the wire from Note.earned_at through _palettes_for_user.
|
||||
self.assertTrue(baltimore.get_attribute("data-unlocked-date"))
|
||||
# And the same Note row in DB now carries the palette commit.
|
||||
note.refresh_from_db()
|
||||
self.assertEqual(note.palette, "palette-baltimore")
|
||||
@@ -14,7 +14,7 @@
|
||||
:root {
|
||||
/* rgb Variable Index */
|
||||
|
||||
/* Precious Metal Palette */
|
||||
/* Precious Metal Hues */
|
||||
// nickel
|
||||
--priNi: 141, 142, 140;
|
||||
--secNi: 118, 120, 118;
|
||||
@@ -59,7 +59,7 @@
|
||||
--quiAg: 175, 175, 175;
|
||||
--sixAg: 240, 240, 240;
|
||||
|
||||
/* Cosmic Metal Palette */
|
||||
/* Cosmic Metal Hues */
|
||||
// mercury (Mercury)
|
||||
--priHg: 23, 31, 51;
|
||||
--secHg: 51, 62, 87;
|
||||
@@ -117,7 +117,7 @@
|
||||
--quiPu: 189, 175, 214;
|
||||
--sixPu: 235, 211, 217;
|
||||
|
||||
/* Chroma Palette */
|
||||
/* Chroma Hues */
|
||||
// red (A-Fire)
|
||||
--priRd: 233, 53, 37;
|
||||
--secRd: 193, 43, 28;
|
||||
@@ -203,7 +203,7 @@
|
||||
--quiMe: 89, 0, 48;
|
||||
--sixMe: 59, 0, 32;
|
||||
|
||||
/* Earthman Palette */
|
||||
/* Earthman Hues */
|
||||
// bark
|
||||
--priBrk: 162, 103, 98;
|
||||
--secBrk: 117, 78, 68;
|
||||
@@ -233,7 +233,7 @@
|
||||
--secFor: 94, 124, 61;
|
||||
--terFor: 74, 102, 43;
|
||||
|
||||
/* Technoman Palette */
|
||||
/* Technoman Hue */
|
||||
// carbon steel
|
||||
// stainless steel
|
||||
// maraging steel
|
||||
@@ -253,7 +253,7 @@
|
||||
--tooltip-bg: 0, 0, 0;
|
||||
--title-shadow-offset: -0.125rem;
|
||||
|
||||
/* Inferno Palette (4 per) */
|
||||
/* Inferno Hues (4 per) */
|
||||
// mist (Elpis's Lethe)
|
||||
--priMst: 168, 202, 172;
|
||||
--secMst: 103, 145, 105;
|
||||
@@ -282,7 +282,7 @@
|
||||
--terIce: 74, 119, 125;
|
||||
--quaIce: 35, 65, 75;
|
||||
|
||||
/* Terrestre Palette (6 per) */
|
||||
/* Terrestre Hues (6 per) */
|
||||
// crumbling perse (Contrition)
|
||||
--priPer: 34, 30, 77;
|
||||
--secPer: 52, 45, 99;
|
||||
@@ -304,7 +304,7 @@
|
||||
--quiAdm: 197, 213, 228;
|
||||
--sixAdm: 226, 244, 253;
|
||||
|
||||
/* Emanation Palettes */
|
||||
/* Emanation Hues */
|
||||
// Plant Bundle
|
||||
// • beige-pink (streetlamps)
|
||||
--priBpk: 223, 159, 140;
|
||||
@@ -336,6 +336,23 @@
|
||||
--ninClh: 192, 77, 1;
|
||||
--decClh: 255, 174, 0;
|
||||
|
||||
/* Lord Baltimore Hues */
|
||||
// yellow
|
||||
--priBlt: 235, 171, 0;
|
||||
--secBlt: 187, 147, 52;
|
||||
// white
|
||||
--terBlt: 255, 255, 255;
|
||||
// --quaBlt: ;
|
||||
// black
|
||||
--quiBlt: 0, 0, 0;
|
||||
--sixBlt: 162, 170, 173;
|
||||
// purple
|
||||
--sepBlt: 26, 25, 95;
|
||||
--octBlt: 157, 34, 53;
|
||||
// orange
|
||||
--ninBlt: 221, 73, 38;
|
||||
// --decBlt: ;
|
||||
|
||||
// Felt values
|
||||
--undUser: var(--priFor);
|
||||
--duoUser: var(--terFor);
|
||||
@@ -423,6 +440,34 @@
|
||||
|
||||
/* Nebula Palette */
|
||||
|
||||
/* Baltimore Palette */
|
||||
.palette-baltimore {
|
||||
--priUser: var(--sepBlt);
|
||||
--secUser: var(--terBlt);
|
||||
--terUser: var(--ninBlt);
|
||||
--quaUser: var(--priYl);
|
||||
--quiUser: var(--priBlt);
|
||||
--sixUser: var(--quiBlt);
|
||||
--sepUser: var(--quiBlt);
|
||||
--octUser: var(--quiBlt);
|
||||
--ninUser: var(--sixBlt);
|
||||
--decUser: var(--quiBlt);
|
||||
}
|
||||
|
||||
/* Maryland Palette */
|
||||
.palette-maryland {
|
||||
--priUser: var(--quiBlt);
|
||||
--secUser: var(--sixBlt);
|
||||
--terUser: var(--octBlt);
|
||||
--quaUser: var(--priBlt);
|
||||
--quiUser: var(--secBlt);
|
||||
--sixUser: var(--quiBlt);
|
||||
--sepUser: var(--quiBlt);
|
||||
--octUser: var(--quiBlt);
|
||||
--ninUser: var(--priRd);
|
||||
--decUser: var(--quiBlt);
|
||||
}
|
||||
|
||||
/* Monochrome Dark Palette */
|
||||
.palette-monochrome-dark {
|
||||
--priUser: var(--priAg); /* 30,30,30 — near-black bg */
|
||||
|
||||
@@ -38,5 +38,8 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||
{# Brief.showBanner — needed for the Baltimorean Note-unlock banner the #}
|
||||
{# pronouns applet fires on first `bawlmorese` selection. #}
|
||||
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
||||
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
|
||||
{% endblock scripts %}
|
||||
|
||||
Reference in New Issue
Block a user