"""Functional tests for the My Sky dashboard feature. My Sky is a dashboard applet linking to /dashboard/sky/ — a full-page natus (natal chart) interface where the user can save their personal sky to their account (stored on the User model, independent of any game room). """ import json as _json from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from apps.applets.models import Applet from apps.lyric.models import User from .base import FunctionalTest # Chart fixture — May 27 2008, 12:12 PM, Morganza MD (38.3754°N, 76.6955°W). # Sun (6.7° Gemini) and Venus (3.3° Gemini) are 3.4° apart — a clear conjunction. _CHART_FIXTURE = { "planets": { "Sun": {"sign": "Gemini", "degree": 66.7, "retrograde": False}, "Moon": {"sign": "Taurus", "degree": 43.0, "retrograde": False}, "Mercury": {"sign": "Taurus", "degree": 55.0, "retrograde": False}, "Venus": {"sign": "Gemini", "degree": 63.3, "retrograde": False}, "Mars": {"sign": "Leo", "degree": 132.0, "retrograde": False}, "Jupiter": {"sign": "Capricorn", "degree": 292.0, "retrograde": True}, "Saturn": {"sign": "Virgo", "degree": 153.0, "retrograde": False}, "Uranus": {"sign": "Pisces", "degree": 322.0, "retrograde": False}, "Neptune": {"sign": "Aquarius", "degree": 323.0, "retrograde": True}, "Pluto": {"sign": "Sagittarius", "degree": 269.0, "retrograde": True}, }, "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": { "1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0, "7": 1, "8": 0, "9": 2, "10": 1, "11": 1, "12": 2, }, "house_system": "O", "timezone": "America/New_York", } class MySkyAppletTest(FunctionalTest): """My Sky applet appears on the dashboard and links to the sky page.""" 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") # ── T1 ─────────────────────────────────────────────────────────────────── def test_my_sky_applet_links_to_sky_page_with_form(self): """Applet is visible on dashboard; link leads to /dashboard/sky/ with all natus form fields present.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) # 1. Applet is on the dashboard applet = self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_my_sky") ) # 2. Heading contains a link whose text is "My Sky" link = applet.find_element(By.CSS_SELECTOR, "h2 a") self.assertIn("MY SKY", link.text.upper()) # 3. Clicking the link navigates to /dashboard/sky/ link.click() self.wait_for( lambda: self.assertRegex(self.browser.current_url, r"/dashboard/sky/$") ) # 4. All natus form fields are present self.browser.find_element(By.ID, "id_nf_date") self.browser.find_element(By.ID, "id_nf_time") self.browser.find_element(By.ID, "id_nf_place") self.browser.find_element(By.ID, "id_nf_lat") self.browser.find_element(By.ID, "id_nf_lon") self.browser.find_element(By.ID, "id_nf_tz") self.browser.find_element(By.ID, "id_natus_confirm") class MySkyLocalStorageTest(FunctionalTest): """My Sky form fields persist to localStorage across visits.""" 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.sky_url = self.live_server_url + "/dashboard/sky/" def _fill_form(self): """Set date, lat, lon directly — bypasses Nominatim network call.""" self.browser.execute_script( "document.getElementById('id_nf_date').value = '1990-06-15';" "document.getElementById('id_nf_lat').value = '51.5074';" "document.getElementById('id_nf_lon').value = '-0.1278';" "document.getElementById('id_nf_place').value = 'London, UK';" "document.getElementById('id_nf_tz').value = 'Europe/London';" ) # Fire input events so the localStorage save listener triggers self.browser.execute_script(""" ['id_nf_date','id_nf_lat','id_nf_lon','id_nf_place','id_nf_tz'].forEach(id => { document.getElementById(id) .dispatchEvent(new Event('input', {bubbles: true})); }); """) def _field_values(self): return 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, }; """) # ── T2 ─────────────────────────────────────────────────────────────────── def test_sky_form_fields_repopulated_after_page_refresh(self): """Form values survive a full page refresh via localStorage.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.sky_url) self.wait_for(lambda: self.browser.find_element(By.ID, "id_nf_date")) self._fill_form() self.browser.refresh() self.wait_for(lambda: self.browser.find_element(By.ID, "id_nf_date")) values = self._field_values() self.assertEqual(values["date"], "1990-06-15") self.assertEqual(values["lat"], "51.5074") self.assertEqual(values["lon"], "-0.1278") self.assertEqual(values["place"], "London, UK") self.assertEqual(values["tz"], "Europe/London") class MySkyAppletWheelTest(FunctionalTest): """Saved natal chart renders as an interactive wheel inside the My Sky applet.""" 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 = "Lindenwold, NJ, US" self.gamer.save() # ── T3 ─────────────────────────────────────────────────────────────────── def test_saved_sky_wheel_renders_with_element_tooltip_in_applet(self): """When the user has saved sky data, the natal wheel appears in the My Sky applet and the element-ring tooltip fires on hover. (Planet hover tooltip is covered by NatusWheelSpec.js T3/T4/T5.)""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) # 1. Wheel SVG is drawn inside the applet self.wait_for(lambda: self.assertTrue( self.browser.find_element( By.CSS_SELECTOR, "#id_applet_my_sky .nw-root" ) )) # 2. Hovering an element-ring slice shows the tooltip slice_el = self.browser.find_element( By.CSS_SELECTOR, "#id_applet_my_sky .nw-element-group" ) ActionChains(self.browser).move_to_element(slice_el).perform() self.wait_for(lambda: self.assertEqual( self.browser.find_element(By.ID, "id_natus_tooltip") .value_of_css_property("display"), "block", )) class MySkyAppletFormTest(FunctionalTest): """My Sky applet shows natus entry form when no sky data is saved.""" 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") # ── T4 ─────────────────────────────────────────────────────────────────── def test_applet_shows_entry_form_when_no_sky_saved(self): """When no sky data is saved the My Sky applet shows all natus form fields and a disabled SAVE SKY button; no wheel is drawn yet.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) applet = self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_my_sky") ) applet.find_element(By.ID, "id_nf_date") applet.find_element(By.ID, "id_nf_time") applet.find_element(By.ID, "id_nf_place") applet.find_element(By.ID, "id_nf_lat") applet.find_element(By.ID, "id_nf_lon") applet.find_element(By.ID, "id_nf_tz") applet.find_element(By.ID, "id_natus_confirm") self.assertFalse(applet.find_elements(By.CSS_SELECTOR, ".nw-root")) # ── T5 ─────────────────────────────────────────────────────────────────── def test_applet_form_disappears_and_wheel_draws_after_save(self): """Filling the applet form and clicking SAVE SKY hides the form wrap and draws the natal wheel in its place.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_sky_form_wrap") ) # Mock fetch: preview → chart fixture; save → {saved: true} self.browser.execute_script(""" const FIXTURE = """ + _json.dumps(_CHART_FIXTURE) + """; window._origFetch = window.fetch; window.fetch = function(url, opts) { if (url.includes('/sky/preview/')) { return Promise.resolve({ ok: true, json: () => Promise.resolve(FIXTURE), }); } if (url.includes('/sky/save/')) { return Promise.resolve({ ok: true, json: () => Promise.resolve({saved: true}), }); } return window._origFetch(url, opts); }; """) # Fill required fields and fire input to trigger schedulePreview self.browser.execute_script(""" document.getElementById('id_nf_date').value = '1990-06-15'; document.getElementById('id_nf_lat').value = '51.5074'; document.getElementById('id_nf_lon').value = '-0.1278'; document.getElementById('id_nf_tz').value = 'Europe/London'; document.getElementById('id_nf_date').dispatchEvent( new Event('input', {bubbles: true}) ); """) # Wait for confirm button to be enabled (preview resolved) confirm_btn = self.browser.find_element(By.ID, "id_natus_confirm") self.wait_for(lambda: self.assertIsNone( confirm_btn.get_attribute("disabled") )) confirm_btn.click() # Form wrap should become hidden form_wrap = self.browser.find_element(By.ID, "id_applet_sky_form_wrap") self.wait_for(lambda: self.assertEqual( form_wrap.value_of_css_property("display"), "none" )) # Natal wheel should be drawn inside the applet self.wait_for(lambda: self.assertTrue( self.browser.find_element( By.CSS_SELECTOR, "#id_applet_my_sky .nw-root" ) )) class MySkyWheelConjunctionTest(FunctionalTest): """Tick lines, z-raise, and dual tooltip for conjunct planets.""" 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 = "Morganza, MD, US" self.gamer.save() def _load_wheel(self): self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) self.wait_for(lambda: self.assertTrue( self.browser.find_element(By.CSS_SELECTOR, "#id_applet_my_sky .nw-root") )) # ── T6 ─────────────────────────────────────────────────────────────────── def test_planet_tick_lines_present(self): """Every planet has one tick line in the SVG.""" self._load_wheel() self.wait_for(lambda: self.assertEqual( len(self.browser.find_elements( By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-tick" )), 10, )) # (T7 tick-extends-past-zodiac, T8 hover-raises-to-front, and T9 conjunction # dual-tooltip are covered by NatusWheelSpec.js T7/T8/T9j — ActionChains # planet-circle hover is unreliable in headless Firefox.)