sky wheel: ubiquitous DEL btn — applet & PICK SKY parity w. Dashsky; PICK SKY clears client-only state (no User-model touch) — TDD

My Sky applet (.../dashboard/_partials/_applet-my-sky.html): adds <button id="id_applet_sky_delete_btn" class="btn btn-danger"> at the wheel center, gated on user.sky_chart_data. Click → window.showGuard("Forget sky?") → on OK, fetch POSTs sky_delete (clears every sky_* field on User), removes the 'sky-form:dashboard:sky' localStorage entry that would otherwise rehydrate the post-reload form via _restoreForm(), then reloads — applet's form-render branch is server-template-gated on chart_data so the page comes back form-only.

PICK SKY in-room overlay (.../gameboard/_partials/_sky_overlay.html): adds <button id="id_sky_delete_btn"> at the wheel center. The wheel here is purely a live preview — sky_save fires only on SAVE SKY click w. action='confirm', so there's no draft Character to delete & we do NOT touch the Character/User model. The DEL handler clears the SVG, resets form fields (including lat/lon/tz/tzHint), nulls _lastChartData, disables the SAVE SKY btn, & purges the LS_KEY entry that would otherwise rehydrate on next overlay open / page refresh. Mirrors the user's spec ("shouldn't be targeting the user model anyway, only the character/seat model" — and there's currently no character/seat draft in the PICK SKY flow).

Both handlers defer the window.showGuard readiness check to click-time rather than gating the listener bind itself: window.showGuard is assigned by a base.html script that lives BELOW the content block, so an `if (window.showGuard)` gate at script-execute time would skip the bind entirely (we hit this writing the applet handler — manifested as portal class never receiving 'active' on click).

SCSS: extends the existing #id_sky_delete_form absolute-center rule onto the two new btn IDs (#id_sky_delete_btn, #id_applet_sky_delete_btn). #id_applet_my_sky picks up position:relative as the absolute anchor for the applet btn.

FTs: MySkyAppletDelTest (applet → DEL → guard → OK → reload, asserts User cleared + LS purged + form re-renders) & PickSkyDelTest (overlay → fill form → wheel paints → DEL → guard → OK, asserts SVG empty + form blank + LS purged). Both red before the wiring, green after; full sky suite (46 tests) green.

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:
Disco DeDisco
2026-05-08 14:34:41 -04:00
parent 283b417341
commit e9bceaab62
5 changed files with 256 additions and 4 deletions

View File

@@ -467,6 +467,69 @@ class MySkyApertureSnapScrollTest(FunctionalTest):
))
class MySkyAppletDelTest(FunctionalTest):
"""My Sky applet (on /dashboard/) gets a parity DEL btn at the wheel
center: clicking opens the global guard portal; OK clears the saved sky
on the User model & the relevant localStorage entry, then the applet
swaps back to its form rendering on reload."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="my-sky",
defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
)
self.gamer = User.objects.create(email="stargazer@test.io")
self.gamer.sky_chart_data = _CHART_FIXTURE
self.gamer.sky_birth_place = "Baltimore, MD, US"
self.gamer.sky_birth_tz = "America/New_York"
self.gamer.sky_birth_lat = 39.2904
self.gamer.sky_birth_lon = -76.6122
self.gamer.sky_birth_dt = datetime(
1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
)
self.gamer.save()
self.dash_url = self.live_server_url + "/"
def test_applet_del_clears_user_sky_and_swaps_to_form(self):
self.create_pre_authenticated_session("stargazer@test.io")
self.browser.get(self.dash_url)
# 1. Applet shows wheel + DEL btn
del_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_my_sky .btn-danger"
))
self.assertTrue(del_btn.is_displayed())
# Seed localStorage as if user had previously typed in the form. After
# DEL, this entry should be cleared so the form doesn't repopulate
# from stale state on the post-reload form render.
self.browser.execute_script("""
localStorage.setItem('sky-form:dashboard:sky',
JSON.stringify({date:'1980-01-01', lat:'1', lon:'1', place:'Stale', tz:'UTC'}));
""")
del_btn.click()
portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))
portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()
# 2. Page reloads → applet renders form (no chart_data)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_applet_sky_form_wrap"))
# 3. localStorage cleared (so form isn't seeded with stale data)
ls_value = self.browser.execute_script(
"return localStorage.getItem('sky-form:dashboard:sky');"
)
self.assertIsNone(ls_value)
# 4. DB-side: every sky field is cleared
self.gamer.refresh_from_db()
self.assertIsNone(self.gamer.sky_chart_data)
self.assertIsNone(self.gamer.sky_birth_dt)
self.assertEqual(self.gamer.sky_birth_place, "")
class MySkyAsyncSaveTest(FunctionalTest):
"""A fresh user (no saved sky) clicks SAVE SKY (mocked) — without a page
reload, the page transitions into the saved state: body picks up

View File

@@ -1,5 +1,7 @@
"""Functional tests for the PICK SKY overlay — natal chart entry."""
import json as _json
from selenium.webdriver.common.by import By
from apps.applets.models import Applet
@@ -9,6 +11,21 @@ from apps.lyric.models import User
from .base import FunctionalTest
_PICK_SKY_CHART_FIXTURE = {
"planets": {
"Sun": {"sign": "Gemini", "degree": 66.7, "retrograde": False},
"Moon": {"sign": "Taurus", "degree": 43.0, "retrograde": False},
},
"houses": {"cusps": [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
"asc": 180.0, "mc": 90.0},
"elements": {"Fire": 1, "Water": 0, "Stone": 2, "Air": 4, "Time": 1, "Space": 1},
"aspects": [],
"distinctions": {str(i): 0 for i in range(1, 13)},
"house_system": "O",
"timezone": "America/New_York",
}
def _make_sky_select_room():
"""Minimal SKY_SELECT room — just enough for the overlay to render."""
email = "founder@test.io"
@@ -125,3 +142,104 @@ class PickSkyLocalStorageTest(FunctionalTest):
self.assertEqual(values["tz"], "America/New_York")
class PickSkyDelTest(FunctionalTest):
"""PICK SKY overlay gets a DEL btn at the wheel center: clicking opens the
global guard portal; OK clears the wheel SVG, resets the form fields, &
purges the localStorage entry that would otherwise rehydrate the form on
the next overlay open / page refresh. No server hit (the wheel here is
purely a preview — un-saved data lives only in localStorage)."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
self.room, self.founder, self.founder_email = _make_sky_select_room()
self.room_url = self.live_server_url + f"/gameboard/room/{self.room.id}/"
def _ls_key(self):
# Mirrors the JS: 'sky-form:' + SAVE_URL (overlay.dataset.saveUrl is
# an absolute URL, so the LS key carries the full http://host:port… too)
return self.browser.execute_script(
"return 'sky-form:' + document.getElementById('id_sky_overlay').dataset.saveUrl;"
)
def test_del_clears_wheel_form_and_localstorage(self):
self.create_pre_authenticated_session(self.founder_email)
self.browser.get(self.room_url)
# Open PICK SKY modal
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
self.browser.execute_script("arguments[0].click()", btn)
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_overlay"))
# Mock /sky/preview so schedulePreview resolves & SkyWheel.draw paints
# children into #id_sky_svg without hitting PySwiss.
self.browser.execute_script("""
const FIXTURE = """ + _json.dumps(_PICK_SKY_CHART_FIXTURE) + """;
window._origFetch = window.fetch;
window.fetch = function(url, opts) {
if (typeof url === 'string' && url.includes('/sky/preview')) {
return Promise.resolve({ok:true, json:()=>Promise.resolve(FIXTURE)});
}
return window._origFetch(url, opts);
};
""")
# Fill form → triggers schedulePreview → wheel renders
self.browser.execute_script("""
document.getElementById('id_nf_date').value = '2008-05-27';
document.getElementById('id_nf_lat').value = '38.3754';
document.getElementById('id_nf_lon').value = '-76.6955';
document.getElementById('id_nf_place').value = 'Morganza, MD';
document.getElementById('id_nf_tz').value = 'America/New_York';
document.getElementById('id_nf_date').dispatchEvent(
new Event('input', {bubbles:true})
);
""")
# Wait for the wheel to render (svg has children)
self.wait_for(lambda: self.assertTrue(
self.browser.find_elements(By.CSS_SELECTOR, "#id_sky_svg > *")
))
# localStorage was populated by _saveForm during typing
ls_key = self._ls_key()
self.assertIsNotNone(self.browser.execute_script(
f"return localStorage.getItem({_json.dumps(ls_key)});"
))
# DEL btn → guard portal → OK
del_btn = self.browser.find_element(By.ID, "id_sky_delete_btn")
del_btn.click()
portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))
portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()
# After OK: SVG empty, form fields blank, localStorage entry purged
self.wait_for(lambda: self.assertFalse(
self.browser.find_elements(By.CSS_SELECTOR, "#id_sky_svg > *"),
"Wheel SVG should be cleared after DEL",
))
values = self.browser.execute_script("""
return {
date: document.getElementById('id_nf_date').value,
lat: document.getElementById('id_nf_lat').value,
lon: document.getElementById('id_nf_lon').value,
place: document.getElementById('id_nf_place').value,
tz: document.getElementById('id_nf_tz').value,
};
""")
self.assertEqual(values["date"], "")
self.assertEqual(values["lat"], "")
self.assertEqual(values["lon"], "")
self.assertEqual(values["place"], "")
self.assertEqual(values["tz"], "")
self.assertIsNone(self.browser.execute_script(
f"return localStorage.getItem({_json.dumps(ls_key)});"
))