fix: /gameboard/my-sea/ sig polarity now matches /billboard/my-sign/ — two-bug stack. **Bug 1 (primary):** my_sea.html:10 had {% if significator_reversed %}gravity{% else %}levity{% endif %} — INVERTED from my_sign.html:22's {% if current_significator_reversed %}levity{% else %}gravity{% endif %} + its JS _polarity() (revInput.value === '1' ? 'levity' : 'gravity'). Same User.significator_reversed value produced opposite polarity styling across the two surfaces → a levity sig picked on my-sign rendered gravity-styled on my-sea (--priUser bg + --secUser text); a gravity sig rendered levity-styled. User-reported 2026-05-21. **Bug 2 (latent, masked by Bug 1):** .sea-sig-card hardcoded .fan-corner-rank + i to color: rgba(var(--secUser), …) — fine against gravity's --priUser bg, but against levity's --secUser bg (set by .sig-stage-card in the polarity rule at _card-deck.scss:935-943) the rank + suit-icon collided w. the bg and disappeared. Bug 1 was hiding this: levity sigs were getting rendered gravity-styled (visible), so the invisibility only surfaced for gravity sigs (which got levity-styled). Fixing Bug 1 alone would've exposed Bug 2 for the previously-fine levity case → fix both in one shot. **Bug 2 fix:** switch .fan-corner-rank + i to color: currentColor w. opacity preserved (0.85 / 0.75); add explicit color: rgba(var(--secUser), 1) on the default .sig-stage-card.sea-sig-card rule so gravity inherits secUser; levity polarity rule already sets .sig-stage-card { color: rgba(var(--priUser), 1) } so it cascades down through currentColor. TDD — new MySeaPolarityMatchesMySignTest (2 ITs) pins both pages to the same User.significator_reversed → data-polarity mapping: unreversed → gravity on BOTH surfaces; reversed → levity on BOTH. 1147 IT/UT green. Visual verify deferred to user — the SCSS edge case wasn't reachable via Selenium (computed-style-on---secUser would require palette resolution at runtime)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -640,6 +640,49 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
|||||||
self.assertNotContains(response, "my-sea-picker")
|
self.assertNotContains(response, "my-sea-picker")
|
||||||
|
|
||||||
|
|
||||||
|
class MySeaPolarityMatchesMySignTest(TestCase):
|
||||||
|
"""Bug 2026-05-21 (user-reported): a levity sig chosen on /billboard/
|
||||||
|
my-sign/ rendered as gravity-styled on /gameboard/my-sea/ (priUser bg
|
||||||
|
+ secUser text) — and vice versa for gravity sigs (which then collided
|
||||||
|
w. the hardcoded --secUser corner-rank color in `.sea-sig-card`,
|
||||||
|
making the rank + suit-icon invisible against a --secUser bg).
|
||||||
|
|
||||||
|
Root cause was a polarity inversion in `my_sea.html:10` —
|
||||||
|
`{% if significator_reversed %}gravity{% else %}levity{% endif %}` —
|
||||||
|
opposite of `my_sign.html:22` + its JS `_polarity()`. This test pins
|
||||||
|
the two surfaces to agree on the same `User.significator_reversed`
|
||||||
|
→ `data-polarity` mapping, so any future drift gets caught."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from apps.epic.models import personal_sig_cards
|
||||||
|
self.user = User.objects.create(email="polarity@test.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
target = personal_sig_cards(self.user)[0]
|
||||||
|
self.user.significator = target
|
||||||
|
self.user.save(update_fields=["significator"])
|
||||||
|
|
||||||
|
def test_unreversed_sig_renders_gravity_on_both_surfaces(self):
|
||||||
|
"""`significator_reversed=False` (the on-creation default) renders
|
||||||
|
data-polarity="gravity" on BOTH my_sign + my_sea — the convention
|
||||||
|
established by my_sign's Sprint 4a picker + its `_polarity()` JS."""
|
||||||
|
self.user.significator_reversed = False
|
||||||
|
self.user.save(update_fields=["significator_reversed"])
|
||||||
|
sea = self.client.get(reverse("my_sea")).content.decode()
|
||||||
|
sign = self.client.get(reverse("billboard:my_sign")).content.decode()
|
||||||
|
self.assertIn('data-polarity="gravity"', sea)
|
||||||
|
self.assertIn('data-polarity="gravity"', sign)
|
||||||
|
|
||||||
|
def test_reversed_sig_renders_levity_on_both_surfaces(self):
|
||||||
|
"""`significator_reversed=True` (FLIP-toggled on my_sign) renders
|
||||||
|
data-polarity="levity" on BOTH surfaces."""
|
||||||
|
self.user.significator_reversed = True
|
||||||
|
self.user.save(update_fields=["significator_reversed"])
|
||||||
|
sea = self.client.get(reverse("my_sea")).content.decode()
|
||||||
|
sign = self.client.get(reverse("billboard:my_sign")).content.decode()
|
||||||
|
self.assertIn('data-polarity="levity"', sea)
|
||||||
|
self.assertIn('data-polarity="levity"', sign)
|
||||||
|
|
||||||
|
|
||||||
class MySeaSpreadFormTemplateTest(TestCase):
|
class MySeaSpreadFormTemplateTest(TestCase):
|
||||||
"""Sprint 5 iter 3 — form col + SPREAD dropdown structure + default-
|
"""Sprint 5 iter 3 — form col + SPREAD dropdown structure + default-
|
||||||
spread context + cross's `data-spread-shape` attribute. Iter 3 spec
|
spread context + cross's `data-spread-shape` attribute. Iter 3 spec
|
||||||
|
|||||||
@@ -1364,13 +1364,20 @@ $sea-card-h: 6.5rem;
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
|
// Corner-rank + suit-icon track `color` so the polarity rules below
|
||||||
|
// (which set `.sig-stage-card { color: ... }` to --priUser for levity)
|
||||||
|
// flip the contrast w. the card's bg. The default (gravity, --priUser
|
||||||
|
// bg) inherits --secUser from the `.sig-stage-card.sea-sig-card` rule
|
||||||
|
// below; the levity polarity rule overrides `.sig-stage-card { color }`
|
||||||
|
// to --priUser, which propagates down through currentColor.
|
||||||
.fan-corner-rank {
|
.fan-corner-rank {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: rgba(var(--secUser), 0.85);
|
color: currentColor;
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
i { font-size: 1rem; color: rgba(var(--secUser), 0.75); }
|
i { font-size: 1rem; color: currentColor; opacity: 0.75; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
|
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
|
||||||
@@ -1384,6 +1391,7 @@ $sea-card-h: 6.5rem;
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: rgba(var(--priUser), 1);
|
background: rgba(var(--priUser), 1);
|
||||||
border: 0.15rem solid rgba(var(--secUser), 0.6);
|
border: 0.15rem solid rgba(var(--secUser), 0.6);
|
||||||
|
color: rgba(var(--secUser), 1); // default (gravity) text color; `[data-polarity="levity"] .sig-stage-card { color: --priUser }` overrides for levity. Corner-rank + suit-icon track currentColor.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="my-sea-page"
|
<div class="my-sea-page"
|
||||||
data-phase="{% if show_picker %}picker{% else %}landing{% endif %}"
|
data-phase="{% if show_picker %}picker{% else %}landing{% endif %}"
|
||||||
data-polarity="{% if significator_reversed %}gravity{% else %}levity{% endif %}">
|
data-polarity="{% if significator_reversed %}levity{% else %}gravity{% endif %}">
|
||||||
{% if not user_has_sig %}
|
{% if not user_has_sig %}
|
||||||
{# Sprint 4b sign-gate. The draw UX is gated behind a saved #}
|
{# Sprint 4b sign-gate. The draw UX is gated behind a saved #}
|
||||||
{# significator — render a Look!-formatted Brief-style line w. #}
|
{# significator — render a Look!-formatted Brief-style line w. #}
|
||||||
|
|||||||
Reference in New Issue
Block a user