rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests - new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts - drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette - recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-* - _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes) - NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py - 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,7 @@ class FunctionalTest(StaticLiveServerTestCase):
|
||||
if self.test_server:
|
||||
self.live_server_url = 'http://' + self.test_server
|
||||
reset_database(self.test_server)
|
||||
Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
|
||||
Applet.objects.get_or_create(slug="new-post", defaults={"name": "New Post", "context": "billboard"})
|
||||
|
||||
def tearDown(self):
|
||||
if self._test_has_failed():
|
||||
@@ -156,7 +156,7 @@ class ChannelsFunctionalTest(ChannelsLiveServerTestCase):
|
||||
if self.test_server:
|
||||
self.live_server_url = 'http://' + self.test_server
|
||||
reset_database(self.test_server)
|
||||
Applet.objects.get_or_create(slug="new-note", defaults={"name": "New Note"})
|
||||
Applet.objects.get_or_create(slug="new-post", defaults={"name": "New Post", "context": "billboard"})
|
||||
|
||||
def tearDown(self):
|
||||
if self._test_has_failed():
|
||||
|
||||
@@ -3,11 +3,11 @@ from selenium.webdriver.common.by import By
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
class MyNotesPage:
|
||||
class MyPostsPage:
|
||||
def __init__(self, test):
|
||||
self.test = test
|
||||
|
||||
def go_to_my_notes_page(self, email):
|
||||
def go_to_my_posts_page(self, email):
|
||||
self.test.browser.get(self.test.live_server_url)
|
||||
user = User.objects.get(email=email)
|
||||
self.test.browser.get(
|
||||
@@ -4,27 +4,27 @@ from selenium.webdriver.common.keys import Keys
|
||||
from .base import wait
|
||||
|
||||
|
||||
class NotePage:
|
||||
class PostPage:
|
||||
def __init__(self, test):
|
||||
self.test = test
|
||||
|
||||
def get_table_rows(self):
|
||||
return self.test.browser.find_elements(By.CSS_SELECTOR, "#id_note_table tr")
|
||||
return self.test.browser.find_elements(By.CSS_SELECTOR, "#id_post_table tr")
|
||||
|
||||
@wait
|
||||
def wait_for_row_in_note_table(self, item_text, item_number):
|
||||
expected_row_text = f"{item_number}. {item_text}"
|
||||
def wait_for_row_in_post_table(self, line_text, line_number):
|
||||
expected_row_text = f"{line_number}. {line_text}"
|
||||
rows = self.get_table_rows()
|
||||
self.test.assertIn(expected_row_text, [row.text for row in rows])
|
||||
|
||||
def get_item_input_box(self):
|
||||
def get_line_input_box(self):
|
||||
return self.test.browser.find_element(By.ID, "id_text")
|
||||
|
||||
def add_note_item(self, item_text):
|
||||
new_item_no = len(self.get_table_rows()) + 1
|
||||
self.get_item_input_box().send_keys(item_text)
|
||||
self.get_item_input_box().send_keys(Keys.ENTER)
|
||||
self.wait_for_row_in_note_table(item_text, new_item_no)
|
||||
def add_post_line(self, line_text):
|
||||
new_line_no = len(self.get_table_rows()) + 1
|
||||
self.get_line_input_box().send_keys(line_text)
|
||||
self.get_line_input_box().send_keys(Keys.ENTER)
|
||||
self.wait_for_row_in_post_table(line_text, new_line_no)
|
||||
return self
|
||||
|
||||
def get_share_box(self):
|
||||
@@ -36,10 +36,10 @@ class NotePage:
|
||||
def get_shared_with_list(self):
|
||||
return self.test.browser.find_elements(
|
||||
By.CSS_SELECTOR,
|
||||
".note-recipient"
|
||||
".post-recipient"
|
||||
)
|
||||
|
||||
def share_note_with(self, email):
|
||||
def share_post_with(self, email):
|
||||
self.get_share_box().send_keys(email)
|
||||
self.get_share_box().send_keys(Keys.ENTER)
|
||||
self.test.wait_for(
|
||||
@@ -48,5 +48,5 @@ class NotePage:
|
||||
)
|
||||
)
|
||||
|
||||
def get_note_owner(self):
|
||||
return self.test.browser.find_element(By.ID, "id_note_owner").text
|
||||
def get_post_owner(self):
|
||||
return self.test.browser.find_element(By.ID, "id_post_owner").text
|
||||
@@ -1,44 +1,350 @@
|
||||
"""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
|
||||
from .note_page import NotePage
|
||||
from .my_notes_page import MyNotesPage
|
||||
|
||||
|
||||
class MyNotesTest(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 test_logged_in_users_notes_are_saved_as_my_notes(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
|
||||
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)
|
||||
note_page = NotePage(self)
|
||||
note_page.add_note_item("Reticulate splines")
|
||||
note_page.add_note_item("Regurgitate spines")
|
||||
first_note_url = self.browser.current_url
|
||||
|
||||
MyNotesPage(self).go_to_my_notes_page("disco@test.io")
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines")
|
||||
)
|
||||
self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(self.browser.current_url, first_note_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)
|
||||
note_page.add_note_item("Ribbon of death")
|
||||
second_note_url = self.browser.current_url
|
||||
|
||||
MyNotesPage(self).go_to_my_notes_page("disco@test.io")
|
||||
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.browser.find_element(By.LINK_TEXT, "Ribbon of death")
|
||||
lambda: self.assertRegex(self.browser.current_url, r"/billboard/recognition")
|
||||
)
|
||||
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
self.browser.find_elements(By.LINK_TEXT, "My notes"),
|
||||
[],
|
||||
)
|
||||
# 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"))
|
||||
|
||||
44
src/functional_tests/test_applet_my_posts.py
Normal file
44
src/functional_tests/test_applet_my_posts.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .post_page import PostPage
|
||||
from .my_posts_page import MyPostsPage
|
||||
|
||||
|
||||
class MyPostsTest(FunctionalTest):
|
||||
|
||||
def test_logged_in_users_posts_are_saved_as_my_posts(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
post_page.add_post_line("Reticulate splines")
|
||||
post_page.add_post_line("Regurgitate spines")
|
||||
first_post_url = self.browser.current_url
|
||||
|
||||
MyPostsPage(self).go_to_my_posts_page("disco@test.io")
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines")
|
||||
)
|
||||
self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(self.browser.current_url, first_post_url)
|
||||
)
|
||||
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page.add_post_line("Ribbon of death")
|
||||
second_post_url = self.browser.current_url
|
||||
|
||||
MyPostsPage(self).go_to_my_posts_page("disco@test.io")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.LINK_TEXT, "Ribbon of death")
|
||||
)
|
||||
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
self.browser.find_elements(By.LINK_TEXT, "My Posts"),
|
||||
[],
|
||||
)
|
||||
)
|
||||
@@ -1,80 +0,0 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .note_page import NotePage
|
||||
|
||||
|
||||
class ItemValidationTest(FunctionalTest):
|
||||
# Helper functions
|
||||
def get_error_element(self):
|
||||
return self.browser.find_element(By.CSS_SELECTOR, ".invalid-feedback")
|
||||
|
||||
# Test methods
|
||||
def test_cannot_add_empty_note_items(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
|
||||
)
|
||||
|
||||
note_page.get_item_input_box().send_keys("Purchase milk")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:valid")
|
||||
)
|
||||
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
note_page.wait_for_row_in_note_table("Purchase milk", 1)
|
||||
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
note_page.wait_for_row_in_note_table("Purchase milk", 1)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
|
||||
)
|
||||
|
||||
note_page.get_item_input_box().send_keys("Make tea")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"#id_text:valid",
|
||||
)
|
||||
)
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
note_page.wait_for_row_in_note_table("Make tea", 2)
|
||||
|
||||
def test_cannot_add_duplicate_items(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
note_page.add_note_item("Witness divinity")
|
||||
|
||||
note_page.get_item_input_box().send_keys("Witness divinity")
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
self.get_error_element().text,
|
||||
"You've already logged this to your note",
|
||||
)
|
||||
)
|
||||
|
||||
def test_error_messages_are_cleared_on_input(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
note_page.add_note_item("Gobbledygook")
|
||||
note_page.get_item_input_box().send_keys("Gobbledygook")
|
||||
note_page.get_item_input_box().send_keys(Keys.ENTER)
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(self.get_error_element().is_displayed())
|
||||
)
|
||||
|
||||
note_page.get_item_input_box().send_keys("a")
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(self.get_error_element().is_displayed())
|
||||
)
|
||||
@@ -2,61 +2,61 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .note_page import NotePage
|
||||
from .post_page import PostPage
|
||||
|
||||
|
||||
class NewVisitorTest(FunctionalTest):
|
||||
# Test methods
|
||||
def test_can_start_a_note(self):
|
||||
def test_can_start_a_post(self):
|
||||
self.create_pre_authenticated_session("alice@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
|
||||
self.assertIn('Earthman RPG', self.browser.title)
|
||||
header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
|
||||
self.assertIn('Welcome', header_text)
|
||||
|
||||
inputbox = note_page.get_item_input_box()
|
||||
self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a note item')
|
||||
inputbox = post_page.get_line_input_box()
|
||||
self.assertEqual(inputbox.get_attribute('placeholder'), 'Enter a post line')
|
||||
|
||||
inputbox.send_keys('Buy peacock feathers')
|
||||
|
||||
inputbox.send_keys(Keys.ENTER)
|
||||
note_page.wait_for_row_in_note_table("Buy peacock feathers", 1)
|
||||
post_page.wait_for_row_in_post_table("Buy peacock feathers", 1)
|
||||
|
||||
note_page.add_note_item("Use peacock feathers to make a fly")
|
||||
post_page.add_post_line("Use peacock feathers to make a fly")
|
||||
|
||||
note_page.wait_for_row_in_note_table("Use peacock feathers to make a fly", 2)
|
||||
note_page.wait_for_row_in_note_table("Buy peacock feathers", 1)
|
||||
post_page.wait_for_row_in_post_table("Use peacock feathers to make a fly", 2)
|
||||
post_page.wait_for_row_in_post_table("Buy peacock feathers", 1)
|
||||
|
||||
def test_multiple_users_can_start_notes_at_different_urls(self):
|
||||
def test_multiple_users_can_start_posts_at_different_urls(self):
|
||||
self.create_pre_authenticated_session("alice@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
note_page.add_note_item("Buy peacock feathers")
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
post_page.add_post_line("Buy peacock feathers")
|
||||
|
||||
edith_dash_url = self.browser.current_url
|
||||
edith_post_url = self.browser.current_url
|
||||
self.assertRegex(
|
||||
edith_dash_url,
|
||||
r'/dashboard/note/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
|
||||
edith_post_url,
|
||||
r'/dashboard/post/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
|
||||
)
|
||||
|
||||
self.browser.delete_all_cookies()
|
||||
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url)
|
||||
note_page = NotePage(self)
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
page_text = self.browser.find_element(By.TAG_NAME, 'body').text
|
||||
self.assertNotIn('Buy peacock feathers', page_text)
|
||||
|
||||
note_page.add_note_item("Buy milk")
|
||||
post_page.add_post_line("Buy milk")
|
||||
|
||||
francis_dash_url = self.browser.current_url
|
||||
francis_post_url = self.browser.current_url
|
||||
self.assertRegex(
|
||||
francis_dash_url,
|
||||
r'/dashboard/note/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
|
||||
francis_post_url,
|
||||
r'/dashboard/post/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/$',
|
||||
)
|
||||
self.assertNotEqual(francis_dash_url, edith_dash_url)
|
||||
self.assertNotEqual(francis_post_url, edith_post_url)
|
||||
|
||||
page_text = self.browser.find_element(By.TAG_NAME, 'body').text
|
||||
self.assertNotIn('Buy peacock feathers', page_text)
|
||||
80
src/functional_tests/test_applet_new_post_line_validation.py
Normal file
80
src/functional_tests/test_applet_new_post_line_validation.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .post_page import PostPage
|
||||
|
||||
|
||||
class LineValidationTest(FunctionalTest):
|
||||
# Helper functions
|
||||
def get_error_element(self):
|
||||
return self.browser.find_element(By.CSS_SELECTOR, ".invalid-feedback")
|
||||
|
||||
# Test methods
|
||||
def test_cannot_add_empty_post_lines(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
|
||||
)
|
||||
|
||||
post_page.get_line_input_box().send_keys("Purchase milk")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:valid")
|
||||
)
|
||||
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
post_page.wait_for_row_in_post_table("Purchase milk", 1)
|
||||
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
post_page.wait_for_row_in_post_table("Purchase milk", 1)
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_text:invalid")
|
||||
)
|
||||
|
||||
post_page.get_line_input_box().send_keys("Make tea")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"#id_text:valid",
|
||||
)
|
||||
)
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
post_page.wait_for_row_in_post_table("Make tea", 2)
|
||||
|
||||
def test_cannot_add_duplicate_lines(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
post_page.add_post_line("Witness divinity")
|
||||
|
||||
post_page.get_line_input_box().send_keys("Witness divinity")
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
self.get_error_element().text,
|
||||
"You've already logged this to your post",
|
||||
)
|
||||
)
|
||||
|
||||
def test_error_messages_are_cleared_on_input(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.get(self.live_server_url + '/billboard/')
|
||||
post_page = PostPage(self)
|
||||
post_page.add_post_line("Gobbledygook")
|
||||
post_page.get_line_input_box().send_keys("Gobbledygook")
|
||||
post_page.get_line_input_box().send_keys(Keys.ENTER)
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(self.get_error_element().is_displayed())
|
||||
)
|
||||
|
||||
post_page.get_line_input_box().send_keys("a")
|
||||
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(self.get_error_element().is_displayed())
|
||||
)
|
||||
@@ -1,350 +0,0 @@
|
||||
"""Functional tests for the Recognition system.
|
||||
|
||||
Recognition is Earthman's achievement analogue — account-level unlocks earned when
|
||||
the socius observes a qualifying action. These tests cover the Stargazer Recognition:
|
||||
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 Recognition 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 Recognition
|
||||
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 StargazerRecognitionFromDashboardTest(FunctionalTest):
|
||||
"""Stargazer Recognition 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-recognition",
|
||||
defaults={"name": "Recognition", "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_recognition(self):
|
||||
"""SAVE SKY is disabled before a valid preview fires.
|
||||
No Recognition 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, ".recog-banner"))
|
||||
|
||||
# ── T2a ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_first_valid_save_from_applet_fires_banner_and_leads_to_recognition_page(self):
|
||||
"""First valid SAVE SKY from the My Sky applet fires the Stargazer banner.
|
||||
FYI button navigates to /billboard/recognition/ 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, ".recog-banner")
|
||||
)
|
||||
self.assertIn(
|
||||
"Stargazer",
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__title").text,
|
||||
)
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__description")
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-banner__timestamp")
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-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 Recognition page
|
||||
fyi.click()
|
||||
self.wait_for(
|
||||
lambda: self.assertRegex(self.browser.current_url, r"/billboard/recognition")
|
||||
)
|
||||
|
||||
# Recognition page: one Stargazer item
|
||||
item = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-list .recog-item")
|
||||
)
|
||||
self.assertIn(
|
||||
"Stargazer",
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__title").text,
|
||||
)
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__description")
|
||||
item.find_element(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
|
||||
# ── T2b ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_recognition_page_palette_modal_flow(self):
|
||||
"""Recognition page palette modal: image-box opens modal, swatch preview,
|
||||
body-click restores modal, OK raises confirm submenu, confirm sets palette."""
|
||||
Recognition.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/recognition/")
|
||||
|
||||
image_box = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
)
|
||||
|
||||
# Clicking ? opens palette modal
|
||||
image_box.click()
|
||||
modal = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".recog-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 .recog-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, ".recog-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, ".recog-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, ".recog-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, ".recog-palette-modal")
|
||||
))
|
||||
item = self.browser.find_element(By.CSS_SELECTOR, ".recog-item")
|
||||
self.assertTrue(
|
||||
item.find_elements(By.CSS_SELECTOR, ".recog-item__palette.palette-bardo")
|
||||
)
|
||||
self.assertFalse(
|
||||
item.find_elements(By.CSS_SELECTOR, ".recog-item__image-box")
|
||||
)
|
||||
|
||||
# ── T2c ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_dashboard_palette_applet_reflects_recognition_palette_unlock(self):
|
||||
"""After palette unlock via Recognition, the Dashboard Palette applet shows
|
||||
the palette swatch as unlocked with Stargazer shoptalk."""
|
||||
Recognition.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 StargazerRecognitionFromSkyPageTest(FunctionalTest):
|
||||
"""Stargazer Recognition triggered from the standalone sky page.
|
||||
|
||||
T3 — disabled save on sky page does not fire Recognition.
|
||||
T4 — first valid save fires banner; NVM dismisses it.
|
||||
T5 — already-earned Recognition 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-recognition",
|
||||
defaults={"name": "Recognition", "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_recognition(self):
|
||||
"""On /dashboard/sky/, SAVE SKY is disabled until preview fires.
|
||||
No Recognition 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, ".recog-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, ".recog-banner")
|
||||
)
|
||||
self.assertIn(
|
||||
"Stargazer",
|
||||
banner.find_element(By.CSS_SELECTOR, ".recog-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, ".recog-banner")
|
||||
))
|
||||
|
||||
# ── T5 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_already_earned_recognition_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 Recognition banner."""
|
||||
Recognition.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, ".recog-banner"))
|
||||
Reference in New Issue
Block a user