"""Functional tests for the Note system. Note is Earthman's achievement analogue — account-level unlocks earned when the socius observes a qualifying action. These tests cover the Stargazer Note: earned on the first valid personal sky save (dashboard My Sky applet or sky page), outside of any game room. Two test classes — one per surface that can trigger the unlock — each asserting both the negative (disabled/incomplete save does nothing) and positive (first valid save fires the banner) conditions. T2 (Dashboard full flow) is split across three focused tests: T2a — save → banner → FYI → recognition page item T2b — palette modal flow on recognition page T2c — dashboard palette applet reflects Note palette unlock """ import json as _json 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 # Shared natal chart fixture — same birth data as test_applet_my_sky.py. _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": "Europe/London", } def _mock_preview_js(fixture): """Intercepts /sky/preview with a fixture; lets /sky/save reach the real server.""" return f""" const FIXTURE = {_json.dumps(fixture)}; window._origFetch = window.fetch; window.fetch = function(url, opts) {{ if (url.includes('/sky/preview')) {{ return Promise.resolve({{ ok: true, json: () => Promise.resolve(FIXTURE), }}); }} return window._origFetch(url, opts); }}; """ def _fill_valid_sky_form(browser): """Populate form fields and fire input events to trigger schedulePreview.""" 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}) ); """) # ────────────────────────────────────────────────────────────────────────────── # Surface A — My Sky applet on the Dashboard # ────────────────────────────────────────────────────────────────────────────── class StargazerNoteFromDashboardTest(FunctionalTest): """Stargazer Note triggered from the My Sky applet.""" def setUp(self): super().setUp() self.browser.set_window_size(800, 1200) Applet.objects.get_or_create( slug="my-sky", defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"}, ) Applet.objects.get_or_create( slug="palette", defaults={"name": "Palettes", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"}, ) Applet.objects.get_or_create( slug="billboard-notes", defaults={"name": "Note", "grid_cols": 4, "grid_rows": 4, "context": "billboard"}, ) self.gamer = User.objects.create(email="stargazer@test.io") # ── T1 ─────────────────────────────────────────────────────────────────── def test_disabled_save_button_does_not_unlock_note(self): """SAVE SKY is disabled before a valid preview fires. No Note banner appears while the button remains disabled.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) confirm_btn = self.wait_for( lambda: self.browser.find_element(By.ID, "id_natus_confirm") ) self.assertIsNotNone(confirm_btn.get_attribute("disabled")) self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner")) # ── T2a ────────────────────────────────────────────────────────────────── def test_first_valid_save_from_applet_fires_banner_and_leads_to_note_page(self): """First valid SAVE SKY from the My Sky applet fires the Stargazer banner. FYI button navigates to /billboard/my-notes/ showing the Stargazer item.""" 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")) self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE)) _fill_valid_sky_form(self.browser) 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() # Banner slides in below the Dash h2 banner = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner") ) self.assertIn( "Stargazer", banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text, ) banner.find_element(By.CSS_SELECTOR, ".note-banner__description") banner.find_element(By.CSS_SELECTOR, ".note-banner__timestamp") banner.find_element(By.CSS_SELECTOR, ".note-banner__image") banner.find_element(By.CSS_SELECTOR, ".btn.btn-danger") # NVM fyi = banner.find_element(By.CSS_SELECTOR, ".btn.btn-caution") # FYI # FYI navigates to Note page fyi.click() self.wait_for( lambda: self.assertRegex(self.browser.current_url, r"/billboard/recognition") ) # Note page: one Stargazer item item = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-list .note-item") ) self.assertIn( "Stargazer", item.find_element(By.CSS_SELECTOR, ".note-item__title").text, ) item.find_element(By.CSS_SELECTOR, ".note-item__description") item.find_element(By.CSS_SELECTOR, ".note-item__image-box") # ── T2b ────────────────────────────────────────────────────────────────── def test_note_page_palette_modal_flow(self): """Note page palette modal: image-box opens modal, swatch preview, body-click restores modal, OK raises confirm submenu, confirm sets palette.""" Note.objects.create( user=self.gamer, slug="stargazer", earned_at=timezone.now(), ) self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url + "/billboard/my-notes/") image_box = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item__image-box") ) # Clicking ? opens palette modal image_box.click() modal = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal") ) modal.find_element(By.CSS_SELECTOR, ".palette-bardo") modal.find_element(By.CSS_SELECTOR, ".palette-sheol") # Clicking a swatch body previews the palette and dismisses the modal bardo_body = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .note-swatch-body") self.browser.execute_script( "arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))", bardo_body, ) self.wait_for(lambda: self.assertFalse( self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal") )) # Clicking elsewhere ends preview and restores the modal self.browser.find_element(By.TAG_NAME, "body").click() modal = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal") ) # Clicking OK on the swatch raises a confirmation submenu ok_btn = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .btn.btn-confirm") ok_btn.click() confirm_menu = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm") ) # Confirming sets palette, closes modal, replaces image-box with palette swatch confirm_menu.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click() self.wait_for(lambda: self.assertFalse( self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal") )) item = self.browser.find_element(By.CSS_SELECTOR, ".note-item") self.assertTrue( item.find_elements(By.CSS_SELECTOR, ".note-item__palette.palette-bardo") ) self.assertFalse( item.find_elements(By.CSS_SELECTOR, ".note-item__image-box") ) # ── T2c ────────────────────────────────────────────────────────────────── def test_dashboard_palette_applet_reflects_note_palette_unlock(self): """After palette unlock via Note, the Dashboard Palette applet shows the palette swatch as unlocked with Stargazer shoptalk.""" Note.objects.create( user=self.gamer, slug="stargazer", earned_at=timezone.now(), palette="palette-bardo", ) self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.live_server_url) palette_applet = self.wait_for( lambda: self.browser.find_element(By.ID, "id_applet_palette") ) bardo = palette_applet.find_element(By.CSS_SELECTOR, ".swatch.palette-bardo") self.assertNotIn("locked", bardo.get_attribute("class")) bardo_ok = bardo.find_element(By.CSS_SELECTOR, ".palette-ok") self.assertIn("btn-confirm", bardo_ok.get_attribute("class")) self.assertNotIn("btn-disabled", bardo_ok.get_attribute("class")) self.assertIn("Stargazer", bardo.get_attribute("data-shoptalk")) # ────────────────────────────────────────────────────────────────────────────── # Surface B — /dashboard/sky/ standalone page # ────────────────────────────────────────────────────────────────────────────── class StargazerNoteFromSkyPageTest(FunctionalTest): """Stargazer Note triggered from the standalone sky page. T3 — disabled save on sky page does not fire Note. T4 — first valid save fires banner; NVM dismisses it. T5 — already-earned Note does not re-show banner on subsequent save. """ def setUp(self): super().setUp() self.browser.set_window_size(800, 1200) Applet.objects.get_or_create( slug="my-sky", defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"}, ) Applet.objects.get_or_create( slug="billboard-notes", defaults={"name": "Note", "grid_cols": 4, "grid_rows": 4, "context": "billboard"}, ) self.gamer = User.objects.create(email="stargazer@test.io") self.sky_url = self.live_server_url + "/dashboard/sky/" # ── T3 ─────────────────────────────────────────────────────────────────── def test_disabled_save_on_sky_page_does_not_unlock_note(self): """On /dashboard/sky/, SAVE SKY is disabled until preview fires. No Note banner appears while the button is disabled.""" self.create_pre_authenticated_session("stargazer@test.io") self.browser.get(self.sky_url) confirm_btn = self.wait_for( lambda: self.browser.find_element(By.ID, "id_natus_confirm") ) self.assertIsNotNone(confirm_btn.get_attribute("disabled")) self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner")) # ── T4 ─────────────────────────────────────────────────────────────────── def test_first_valid_save_on_sky_page_fires_banner_and_nvm_dismisses(self): """First valid SAVE SKY on /dashboard/sky/ fires the Stargazer banner. NVM (.btn.btn-danger) dismisses it.""" 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_natus_confirm")) self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE)) _fill_valid_sky_form(self.browser) 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() banner = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner") ) self.assertIn( "Stargazer", banner.find_element(By.CSS_SELECTOR, ".note-banner__title").text, ) banner.find_element(By.CSS_SELECTOR, ".btn.btn-danger").click() self.wait_for(lambda: self.assertFalse( self.browser.find_elements(By.CSS_SELECTOR, ".note-banner") )) # ── T5 ─────────────────────────────────────────────────────────────────── def test_already_earned_note_does_not_show_banner_on_subsequent_save(self): """When Stargazer is already in the database for this user, a valid sky save does not fire another Note banner.""" Note.objects.create( user=self.gamer, slug="stargazer", earned_at=timezone.now(), ) 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_natus_confirm")) self.browser.execute_script(_mock_preview_js(_CHART_FIXTURE)) _fill_valid_sky_form(self.browser) 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() # Wait for save to complete (wheel renders) then assert no banner self.wait_for(lambda: self.assertTrue( self.browser.find_elements(By.CSS_SELECTOR, ".nw-root") )) self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".note-banner"))