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:
@@ -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
|
||||
|
||||
@@ -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)});"
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user