PICK SKY: natal wheel planet tooltips + FT modernisation
- natus-wheel.js: per-planet <g> group with data-planet/sign/degree/retrograde attrs; mouseover/mouseout on group (pointer-events:none on child text/℞ so the whole apparatus triggers hover); tooltip uses .tt-title/.tt-description; in-sign degree via _inSignDeg() (ecliptic % 30); D3 switched from CDN to local d3.min.js - _natus.scss: .nw-planet--hover glow; #id_natus_tooltip position:fixed z-200 - _natus_overlay.html: tooltip div uses .tt; local d3.min.js script tag - T3/T4/T5 converted from Selenium execute_script to Jasmine unit tests (NatusWheelSpec.js) — NatusWheel was never defined in headless GeckoDriver; SpecRunner.html updated to load D3 + natus-wheel.js - test_pick_sky.py: NatusWheelTooltipTest removed (replaced by Jasmine) - test_component_cards_tarot / test_trinket_carte_blanche: equip assertions updated from legacy .equip-deck-btn/.equip-trinket-btn mini-tooltip pattern to current DON|DOFF (.btn-equip in main portal); mini-portal text assertions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1030,6 +1030,9 @@ def natus_preview(request, room_id):
|
|||||||
return HttpResponse(status=502)
|
return HttpResponse(status=502)
|
||||||
|
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
# PySwiss uses "Earth"; the wheel and SCSS use "Stone".
|
||||||
|
if 'elements' in data and 'Earth' in data['elements']:
|
||||||
|
data['elements']['Stone'] = data['elements'].pop('Earth')
|
||||||
data['distinctions'] = _compute_distinctions(data['planets'], data['houses'])
|
data['distinctions'] = _compute_distinctions(data['planets'], data['houses'])
|
||||||
data['timezone'] = tz_str
|
data['timezone'] = tz_str
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|||||||
2
src/apps/gameboard/static/apps/gameboard/d3.min.js
vendored
Normal file
2
src/apps/gameboard/static/apps/gameboard/d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -98,6 +98,11 @@ const NatusWheel = (() => {
|
|||||||
return v || fallback;
|
return v || fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ecliptic longitude → degrees within the sign (0–29.999…). */
|
||||||
|
function _inSignDeg(ecliptic) {
|
||||||
|
return ((ecliptic % 360) + 360) % 360 % 30;
|
||||||
|
}
|
||||||
|
|
||||||
function _layout(svgEl) {
|
function _layout(svgEl) {
|
||||||
const rect = svgEl.getBoundingClientRect();
|
const rect = svgEl.getBoundingClientRect();
|
||||||
const size = Math.min(rect.width || 400, rect.height || 400);
|
const size = Math.min(rect.width || 400, rect.height || 400);
|
||||||
@@ -264,16 +269,48 @@ const NatusWheel = (() => {
|
|||||||
const finalA = _toAngle(pdata.degree, asc);
|
const finalA = _toAngle(pdata.degree, asc);
|
||||||
const el = PLANET_ELEMENTS[name] || '';
|
const el = PLANET_ELEMENTS[name] || '';
|
||||||
|
|
||||||
|
// Per-planet group — data attrs + hover events live here so the
|
||||||
|
// symbol text and ℞ indicator don't block mouse events on the circle.
|
||||||
|
const planetEl = planetGroup.append('g')
|
||||||
|
.attr('class', 'nw-planet-group')
|
||||||
|
.attr('data-planet', name)
|
||||||
|
.attr('data-sign', pdata.sign)
|
||||||
|
.attr('data-degree', pdata.degree.toFixed(1))
|
||||||
|
.attr('data-retrograde', pdata.retrograde ? 'true' : 'false')
|
||||||
|
.on('mouseover', function (event) {
|
||||||
|
d3.select(this).classed('nw-planet--hover', true);
|
||||||
|
const tooltip = document.getElementById('id_natus_tooltip');
|
||||||
|
if (!tooltip) return;
|
||||||
|
const sym = PLANET_SYMBOLS[name] || name[0];
|
||||||
|
const signData = SIGNS.find(s => s.name === pdata.sign) || {};
|
||||||
|
const signSym = signData.symbol || '';
|
||||||
|
const inDeg = _inSignDeg(pdata.degree).toFixed(1);
|
||||||
|
const rx = pdata.retrograde ? ' ℞' : '';
|
||||||
|
tooltip.innerHTML =
|
||||||
|
`<div class="tt-title">${name} (${sym})</div>` +
|
||||||
|
`<div class="tt-description">${inDeg}° ${pdata.sign} ${signSym}${rx}</div>`;
|
||||||
|
tooltip.style.left = (event.clientX + 14) + 'px';
|
||||||
|
tooltip.style.top = (event.clientY - 10) + 'px';
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
})
|
||||||
|
.on('mouseout', function (event) {
|
||||||
|
// Ignore mouseout when moving between children of this group
|
||||||
|
if (planetEl.node().contains(event.relatedTarget)) return;
|
||||||
|
d3.select(this).classed('nw-planet--hover', false);
|
||||||
|
const tooltip = document.getElementById('id_natus_tooltip');
|
||||||
|
if (tooltip) tooltip.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
// Circle behind symbol
|
// Circle behind symbol
|
||||||
const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle';
|
const circleBase = pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle';
|
||||||
const circle = planetGroup.append('circle')
|
const circle = planetEl.append('circle')
|
||||||
.attr('cx', _cx + R.planetR * Math.cos(ascAngle))
|
.attr('cx', _cx + R.planetR * Math.cos(ascAngle))
|
||||||
.attr('cy', _cy + R.planetR * Math.sin(ascAngle))
|
.attr('cy', _cy + R.planetR * Math.sin(ascAngle))
|
||||||
.attr('r', _r * 0.05)
|
.attr('r', _r * 0.05)
|
||||||
.attr('class', el ? `${circleBase} nw-planet--${el}` : circleBase);
|
.attr('class', el ? `${circleBase} nw-planet--${el}` : circleBase);
|
||||||
|
|
||||||
// Symbol
|
// Symbol — pointer-events:none so hover is handled by the group
|
||||||
const label = planetGroup.append('text')
|
const label = planetEl.append('text')
|
||||||
.attr('x', _cx + R.planetR * Math.cos(ascAngle))
|
.attr('x', _cx + R.planetR * Math.cos(ascAngle))
|
||||||
.attr('y', _cy + R.planetR * Math.sin(ascAngle))
|
.attr('y', _cy + R.planetR * Math.sin(ascAngle))
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
@@ -281,17 +318,20 @@ const NatusWheel = (() => {
|
|||||||
.attr('dy', '0.1em')
|
.attr('dy', '0.1em')
|
||||||
.attr('font-size', `${_r * 0.09}px`)
|
.attr('font-size', `${_r * 0.09}px`)
|
||||||
.attr('class', el ? `nw-planet-label nw-planet-label--${el}` : 'nw-planet-label')
|
.attr('class', el ? `nw-planet-label nw-planet-label--${el}` : 'nw-planet-label')
|
||||||
|
.attr('pointer-events', 'none')
|
||||||
.text(PLANET_SYMBOLS[name] || name[0]);
|
.text(PLANET_SYMBOLS[name] || name[0]);
|
||||||
|
|
||||||
// Retrograde indicator
|
// Retrograde indicator — also pointer-events:none
|
||||||
|
let rxLabel = null;
|
||||||
if (pdata.retrograde) {
|
if (pdata.retrograde) {
|
||||||
planetGroup.append('text')
|
rxLabel = planetEl.append('text')
|
||||||
.attr('x', _cx + (R.planetR + _r * 0.055) * Math.cos(ascAngle))
|
.attr('x', _cx + (R.planetR + _r * 0.055) * Math.cos(ascAngle))
|
||||||
.attr('y', _cy + (R.planetR + _r * 0.055) * Math.sin(ascAngle))
|
.attr('y', _cy + (R.planetR + _r * 0.055) * Math.sin(ascAngle))
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('dominant-baseline', 'middle')
|
.attr('dominant-baseline', 'middle')
|
||||||
.attr('font-size', `${_r * 0.040}px`)
|
.attr('font-size', `${_r * 0.040}px`)
|
||||||
.attr('class', 'nw-rx')
|
.attr('class', 'nw-rx')
|
||||||
|
.attr('pointer-events', 'none')
|
||||||
.text('℞');
|
.text('℞');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,10 +351,8 @@ const NatusWheel = (() => {
|
|||||||
.attrTween('x', () => t => _cx + R.planetR * Math.cos(interpAngle(t)))
|
.attrTween('x', () => t => _cx + R.planetR * Math.cos(interpAngle(t)))
|
||||||
.attrTween('y', () => t => _cy + R.planetR * Math.sin(interpAngle(t)));
|
.attrTween('y', () => t => _cy + R.planetR * Math.sin(interpAngle(t)));
|
||||||
|
|
||||||
// Retrograde ℞ — move together with planet
|
if (rxLabel) {
|
||||||
if (pdata.retrograde) {
|
rxLabel.transition(transition())
|
||||||
planetGroup.select('.nw-rx:last-child')
|
|
||||||
.transition(transition())
|
|
||||||
.attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t)))
|
.attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t)))
|
||||||
.attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t)));
|
.attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,11 +272,12 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
self.assertIn("Earthman", portal.text)
|
self.assertIn("Earthman", portal.text)
|
||||||
self.assertIn("108", portal.text)
|
self.assertIn("108", portal.text)
|
||||||
|
|
||||||
# Mini tooltip shows Equip button — Earthman is NOT currently equipped
|
# Mini shows "Not Equipped"; DON button is active in the main portal
|
||||||
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
equip_btn = mini.find_element(By.CSS_SELECTOR, ".equip-deck-btn")
|
self.assertIn("Not Equipped", mini.text)
|
||||||
self.assertEqual(equip_btn.text, "Equip Deck?")
|
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
|
||||||
|
self.assertNotIn("btn-disabled", don.get_attribute("class"))
|
||||||
|
|
||||||
# ── Hover over Fiorentine Minchiate deck ─────────────────────────
|
# ── Hover over Fiorentine Minchiate deck ─────────────────────────
|
||||||
fiorentine_el = self.browser.find_element(By.ID, "id_kit_fiorentine_deck")
|
fiorentine_el = self.browser.find_element(By.ID, "id_kit_fiorentine_deck")
|
||||||
@@ -299,7 +300,7 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
self.assertIn("Equipped", mini.text)
|
self.assertIn("Equipped", mini.text)
|
||||||
|
|
||||||
# ── Hover back to Earthman and click Equip ────────────────────────
|
# ── Hover back to Earthman and click DON ─────────────────────────
|
||||||
ActionChains(self.browser).move_to_element(earthman_el).perform()
|
ActionChains(self.browser).move_to_element(earthman_el).perform()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertIn(
|
lambda: self.assertIn(
|
||||||
@@ -307,24 +308,21 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
|||||||
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
self.browser.find_element(By.ID, "id_tooltip_portal").text,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
portal = self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||||
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
mini.find_element(By.CSS_SELECTOR, ".equip-deck-btn").click()
|
portal.find_element(By.CSS_SELECTOR, ".btn-equip").click()
|
||||||
|
|
||||||
# Both portals close after equip
|
# DON becomes disabled; mini updates to "Equipped"; data attr set optimistically
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
lambda: self.assertIn(
|
||||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
"btn-disabled",
|
||||||
|
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.assertIn("Equipped", self.browser.find_element(By.ID, "id_mini_tooltip_portal").text)
|
||||||
# Game Kit data attribute now reflects Earthman's id
|
|
||||||
game_kit = self.browser.find_element(By.ID, "id_game_kit")
|
game_kit = self.browser.find_element(By.ID, "id_game_kit")
|
||||||
self.wait_for(
|
self.assertNotEqual(game_kit.get_attribute("data-equipped-deck-id"), "")
|
||||||
lambda: self.assertNotEqual(
|
|
||||||
game_kit.get_attribute("data-equipped-deck-id"), ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Test 6 — new user's Game Kit shows only the default Earthman deck #
|
# Test 6 — new user's Game Kit shows only the default Earthman deck #
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
|||||||
def _fill_form(self):
|
def _fill_form(self):
|
||||||
"""Set date, lat, lon directly (bypasses Nominatim network call)."""
|
"""Set date, lat, lon directly (bypasses Nominatim network call)."""
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"document.getElementById('id_nf_date').value = '1990-02-28';"
|
"document.getElementById('id_nf_date').value = '2008-05-27';"
|
||||||
"document.getElementById('id_nf_lat').value = '39.8244';"
|
"document.getElementById('id_nf_lat').value = '38.3754';"
|
||||||
"document.getElementById('id_nf_lon').value = '-74.9970';"
|
"document.getElementById('id_nf_lon').value = '-76.6955';"
|
||||||
"document.getElementById('id_nf_place').value = 'Lindenwold, NJ';"
|
"document.getElementById('id_nf_place').value = 'Morganza, MD';"
|
||||||
"document.getElementById('id_nf_tz').value = 'America/New_York';"
|
"document.getElementById('id_nf_tz').value = 'America/New_York';"
|
||||||
)
|
)
|
||||||
# Fire input events so the save listener triggers
|
# Fire input events so the save listener triggers
|
||||||
@@ -95,10 +95,10 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
|||||||
self._open_overlay()
|
self._open_overlay()
|
||||||
|
|
||||||
values = self._field_values()
|
values = self._field_values()
|
||||||
self.assertEqual(values["date"], "1990-02-28")
|
self.assertEqual(values["date"], "2008-05-27")
|
||||||
self.assertEqual(values["lat"], "39.8244")
|
self.assertEqual(values["lat"], "38.3754")
|
||||||
self.assertEqual(values["lon"], "-74.9970")
|
self.assertEqual(values["lon"], "-76.6955")
|
||||||
self.assertEqual(values["place"], "Lindenwold, NJ")
|
self.assertEqual(values["place"], "Morganza, MD")
|
||||||
self.assertEqual(values["tz"], "America/New_York")
|
self.assertEqual(values["tz"], "America/New_York")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -118,8 +118,10 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
|||||||
self._open_overlay()
|
self._open_overlay()
|
||||||
|
|
||||||
values = self._field_values()
|
values = self._field_values()
|
||||||
self.assertEqual(values["date"], "1990-02-28")
|
self.assertEqual(values["date"], "2008-05-27")
|
||||||
self.assertEqual(values["lat"], "39.8244")
|
self.assertEqual(values["lat"], "38.3754")
|
||||||
self.assertEqual(values["lon"], "-74.9970")
|
self.assertEqual(values["lon"], "-76.6955")
|
||||||
self.assertEqual(values["place"], "Lindenwold, NJ")
|
self.assertEqual(values["place"], "Morganza, MD")
|
||||||
self.assertEqual(values["tz"], "America/New_York")
|
self.assertEqual(values["tz"], "America/New_York")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class CarteBlancheTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Hover Carte Blanche — main tooltip present; mini tooltip shows "Equip Trinket?"
|
# 3. Hover Carte Blanche — main tooltip present; mini shows "Not Equipped"; DON active
|
||||||
carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche")
|
carte_el = self.browser.find_element(By.ID, "id_kit_carte_blanche")
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"arguments[0].scrollIntoView({block: 'center'})", carte_el
|
"arguments[0].scrollIntoView({block: 'center'})", carte_el
|
||||||
@@ -122,14 +122,16 @@ class CarteBlancheTest(FunctionalTest):
|
|||||||
self.assertIn("no expiry", portal.text)
|
self.assertIn("no expiry", portal.text)
|
||||||
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
equip_btn = mini.find_element(By.CSS_SELECTOR, ".equip-trinket-btn")
|
self.assertIn("Not Equipped", mini.text)
|
||||||
self.assertEqual(equip_btn.text, "Equip Trinket?")
|
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
|
||||||
|
self.assertNotIn("btn-disabled", don.get_attribute("class"))
|
||||||
|
|
||||||
# 4. Click "Equip Trinket?" — DB switches; both portals close
|
# 4. Click DON — DON becomes disabled; data-equipped-id set optimistically
|
||||||
equip_btn.click()
|
don.click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
lambda: self.assertIn(
|
||||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
"btn-disabled",
|
||||||
|
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -141,13 +143,8 @@ class CarteBlancheTest(FunctionalTest):
|
|||||||
str(self.carte.pk),
|
str(self.carte.pk),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# NOTE: re-hovering carte_el here to assert "Equipped" in mini is unreliable in
|
|
||||||
# headless GeckoDriver — move_to_element uses a different scroll-into-view algorithm
|
|
||||||
# than scrollIntoView({block:'center'}), so the computed element centre can match the
|
|
||||||
# cursor's current position and no mousemove fires. The equip round-trip is validated
|
|
||||||
# implicitly by the DB-side check below (step 6: Pass now shows "Equip Trinket?").
|
|
||||||
|
|
||||||
# 6. Hover Backstage Pass — mini tooltip shows "Equip Trinket?" (Pass no longer equipped)
|
# 6. Hover Backstage Pass — mini shows "Not Equipped" (Pass no longer equipped)
|
||||||
pass_el = self.browser.find_element(By.ID, "id_kit_pass")
|
pass_el = self.browser.find_element(By.ID, "id_kit_pass")
|
||||||
ActionChains(self.browser).move_to_element(pass_el).perform()
|
ActionChains(self.browser).move_to_element(pass_el).perform()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -158,7 +155,7 @@ class CarteBlancheTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
|
||||||
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
|
||||||
self.assertTrue(mini.find_element(By.CSS_SELECTOR, ".equip-trinket-btn").is_displayed())
|
self.assertIn("Not Equipped", mini.text)
|
||||||
|
|
||||||
# ── GATEKEEPER PHASE ─────────────────────────────────────────────────
|
# ── GATEKEEPER PHASE ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
124
src/static/tests/NatusWheelSpec.js
Normal file
124
src/static/tests/NatusWheelSpec.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// ── NatusWheelSpec.js ─────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Unit specs for natus-wheel.js — planet hover tooltips.
|
||||||
|
//
|
||||||
|
// DOM contract assumed:
|
||||||
|
// <svg id="id_natus_svg"> — target for NatusWheel.draw()
|
||||||
|
// <div id="id_natus_tooltip"> — tooltip portal (position:fixed on page)
|
||||||
|
//
|
||||||
|
// Public API under test:
|
||||||
|
// NatusWheel.draw(svgEl, data) — renders wheel; attaches hover listeners
|
||||||
|
// NatusWheel.clear() — empties the SVG (used in afterEach)
|
||||||
|
//
|
||||||
|
// Hover contract:
|
||||||
|
// mouseover on [data-planet] group → adds .nw-planet--hover class
|
||||||
|
// shows #id_natus_tooltip with
|
||||||
|
// planet name, in-sign degree, sign name
|
||||||
|
// and ℞ if retrograde
|
||||||
|
// mouseout on [data-planet] group → removes .nw-planet--hover
|
||||||
|
// hides #id_natus_tooltip
|
||||||
|
//
|
||||||
|
// In-sign degree: ecliptic_longitude % 30 (e.g. 338.4° → 8.4° Pisces)
|
||||||
|
//
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("NatusWheel — planet tooltips", () => {
|
||||||
|
|
||||||
|
const SYNTHETIC_CHART = {
|
||||||
|
planets: {
|
||||||
|
Sun: { sign: "Pisces", degree: 338.4, retrograde: false },
|
||||||
|
Moon: { sign: "Capricorn", degree: 295.1, retrograde: false },
|
||||||
|
Mercury: { sign: "Aquarius", degree: 312.8, retrograde: true },
|
||||||
|
},
|
||||||
|
houses: {
|
||||||
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
||||||
|
asc: 0,
|
||||||
|
mc: 270,
|
||||||
|
},
|
||||||
|
elements: { Fire: 1, Stone: 2, Air: 1, Water: 3, Time: 1, Space: 2 },
|
||||||
|
aspects: [],
|
||||||
|
distinctions: {
|
||||||
|
"1": 0, "2": 0, "3": 0, "4": 0,
|
||||||
|
"5": 0, "6": 0, "7": 0, "8": 0,
|
||||||
|
"9": 0, "10": 0, "11": 0, "12": 0,
|
||||||
|
},
|
||||||
|
house_system: "P",
|
||||||
|
};
|
||||||
|
|
||||||
|
let svgEl, tooltipEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// SVG element — D3 draws into this
|
||||||
|
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svgEl.setAttribute("id", "id_natus_svg");
|
||||||
|
svgEl.setAttribute("width", "400");
|
||||||
|
svgEl.setAttribute("height", "400");
|
||||||
|
svgEl.style.width = "400px";
|
||||||
|
svgEl.style.height = "400px";
|
||||||
|
document.body.appendChild(svgEl);
|
||||||
|
|
||||||
|
// Tooltip portal — same markup as _natus_overlay.html
|
||||||
|
tooltipEl = document.createElement("div");
|
||||||
|
tooltipEl.id = "id_natus_tooltip";
|
||||||
|
tooltipEl.className = "tt";
|
||||||
|
tooltipEl.style.display = "none";
|
||||||
|
document.body.appendChild(tooltipEl);
|
||||||
|
|
||||||
|
NatusWheel.draw(svgEl, SYNTHETIC_CHART);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
NatusWheel.clear();
|
||||||
|
svgEl.remove();
|
||||||
|
tooltipEl.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T3 ── hover planet shows name / sign / in-sign degree + glow ─────────
|
||||||
|
|
||||||
|
it("T3: hovering a planet group adds the glow class and shows the tooltip with name, sign, and in-sign degree", () => {
|
||||||
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
||||||
|
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(sun.classList.contains("nw-planet--hover")).toBe(true);
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Sun");
|
||||||
|
expect(text).toContain("Pisces");
|
||||||
|
// in-sign degree: 338.4° ecliptic − 330° (Pisces start) = 8.4°
|
||||||
|
expect(text).toContain("8.4");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T4 ── retrograde planet shows ℞ ──────────────────────────────────────
|
||||||
|
|
||||||
|
it("T4: hovering a retrograde planet shows ℞ in the tooltip", () => {
|
||||||
|
const mercury = svgEl.querySelector("[data-planet='Mercury']");
|
||||||
|
expect(mercury).not.toBeNull("expected [data-planet='Mercury'] to exist in the SVG");
|
||||||
|
|
||||||
|
mercury.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
expect(tooltipEl.textContent).toContain("℞");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T5 ── mouseout hides tooltip and removes glow ─────────────────────────
|
||||||
|
|
||||||
|
it("T5: mouseout hides the tooltip and removes the glow class", () => {
|
||||||
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
||||||
|
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
// relatedTarget is document.body — outside the planet group
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseout", {
|
||||||
|
bubbles: true,
|
||||||
|
relatedTarget: document.body,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
|
expect(sun.classList.contains("nw-planet--hover")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,11 +22,14 @@
|
|||||||
<script src="RoleSelectSpec.js"></script>
|
<script src="RoleSelectSpec.js"></script>
|
||||||
<script src="TraySpec.js"></script>
|
<script src="TraySpec.js"></script>
|
||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||||
<!-- Jasmine env config (optional) -->
|
<!-- Jasmine env config (optional) -->
|
||||||
<script src="lib/jasmine-6.0.1/boot1.js"></script>
|
<script src="lib/jasmine-6.0.1/boot1.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -384,16 +384,16 @@ html.natus-open .natus-modal-wrap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Icon bg circles — element fill + matching border
|
// Icon bg circles — element fill + matching border
|
||||||
.nw-sign-icon-bg--fire { fill: rgba(var(--terRd), 1); stroke: rgba(var(--priOr), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--fire { fill: rgba(var(--terRd), 0.92); stroke: rgba(var(--priOr), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--stone { fill: rgba(var(--priYl), 1); stroke: rgba(var(--priLm), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--stone { fill: rgba(var(--priYl), 0.92); stroke: rgba(var(--priLm), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--air { fill: rgba(var(--terGn), 1); stroke: rgba(var(--priTk), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--air { fill: rgba(var(--terGn), 0.92); stroke: rgba(var(--priTk), 1); stroke-width: 1px; }
|
||||||
.nw-sign-icon-bg--water { fill: rgba(var(--priCy), 1); stroke: rgba(var(--priBl), 1); stroke-width: 1px; }
|
.nw-sign-icon-bg--water { fill: rgba(var(--priCy), 0.92); stroke: rgba(var(--priBl), 1); stroke-width: 1px; }
|
||||||
|
|
||||||
// Inline SVG path icons — per-element colors
|
// Inline SVG path icons — per-element colors
|
||||||
.nw-sign-icon--fire { fill: rgba(var(--priOr), 1); }
|
.nw-sign-icon--fire { fill: rgba(var(--priOr), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
||||||
.nw-sign-icon--stone { fill: rgba(var(--terLm), 1); }
|
.nw-sign-icon--stone { fill: rgba(var(--terLm), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
||||||
.nw-sign-icon--air { fill: rgba(var(--priTk), 1); }
|
.nw-sign-icon--air { fill: rgba(var(--priTk), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
||||||
.nw-sign-icon--water { fill: rgba(var(--terBl), 1); }
|
.nw-sign-icon--water { fill: rgba(var(--terBl), 1); text-shadow: 2px 2px 2px rgba(0, 0, 0, 1); }
|
||||||
|
|
||||||
// House ring — uniform --priFs bg
|
// House ring — uniform --priFs bg
|
||||||
.nw-house-cusp { stroke: rgba(var(--terUser), 1); stroke-width: 1.2px; }
|
.nw-house-cusp { stroke: rgba(var(--terUser), 1); stroke-width: 1.2px; }
|
||||||
@@ -431,6 +431,12 @@ html.natus-open .natus-modal-wrap {
|
|||||||
.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
|
.nw-planet-label--pu { fill: rgba(var(--sixPu), 1); stroke: rgba(var(--sixPu), 0.6); }
|
||||||
.nw-rx { fill: rgba(var(--terUser), 1); }
|
.nw-rx { fill: rgba(var(--terUser), 1); }
|
||||||
|
|
||||||
|
// Planet hover glow (--ninUser)
|
||||||
|
.nw-planet--hover {
|
||||||
|
filter: drop-shadow(0 0 5px rgba(var(--ninUser), 0.9));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
// Aspects
|
// Aspects
|
||||||
.nw-aspects { opacity: 0.8; }
|
.nw-aspects { opacity: 0.8; }
|
||||||
|
|
||||||
@@ -444,6 +450,14 @@ html.natus-open .natus-modal-wrap {
|
|||||||
.nw-element-label--time { fill: rgba(var(--priYl, 192, 160, 48), 1); }
|
.nw-element-label--time { fill: rgba(var(--priYl, 192, 160, 48), 1); }
|
||||||
.nw-element-label--space { fill: rgba(var(--priGn, 64, 96, 64), 1); }
|
.nw-element-label--space { fill: rgba(var(--priGn, 64, 96, 64), 1); }
|
||||||
|
|
||||||
|
// ── Planet hover tooltip — uses .tt base styles; overrides position + z ───────
|
||||||
|
|
||||||
|
#id_natus_tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sidebar z-index sink (landscape sidebars must go below backdrop) ───────────
|
// ── Sidebar z-index sink (landscape sidebars must go below backdrop) ───────────
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
|
|||||||
124
src/static_src/tests/NatusWheelSpec.js
Normal file
124
src/static_src/tests/NatusWheelSpec.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// ── NatusWheelSpec.js ─────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Unit specs for natus-wheel.js — planet hover tooltips.
|
||||||
|
//
|
||||||
|
// DOM contract assumed:
|
||||||
|
// <svg id="id_natus_svg"> — target for NatusWheel.draw()
|
||||||
|
// <div id="id_natus_tooltip"> — tooltip portal (position:fixed on page)
|
||||||
|
//
|
||||||
|
// Public API under test:
|
||||||
|
// NatusWheel.draw(svgEl, data) — renders wheel; attaches hover listeners
|
||||||
|
// NatusWheel.clear() — empties the SVG (used in afterEach)
|
||||||
|
//
|
||||||
|
// Hover contract:
|
||||||
|
// mouseover on [data-planet] group → adds .nw-planet--hover class
|
||||||
|
// shows #id_natus_tooltip with
|
||||||
|
// planet name, in-sign degree, sign name
|
||||||
|
// and ℞ if retrograde
|
||||||
|
// mouseout on [data-planet] group → removes .nw-planet--hover
|
||||||
|
// hides #id_natus_tooltip
|
||||||
|
//
|
||||||
|
// In-sign degree: ecliptic_longitude % 30 (e.g. 338.4° → 8.4° Pisces)
|
||||||
|
//
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("NatusWheel — planet tooltips", () => {
|
||||||
|
|
||||||
|
const SYNTHETIC_CHART = {
|
||||||
|
planets: {
|
||||||
|
Sun: { sign: "Pisces", degree: 338.4, retrograde: false },
|
||||||
|
Moon: { sign: "Capricorn", degree: 295.1, retrograde: false },
|
||||||
|
Mercury: { sign: "Aquarius", degree: 312.8, retrograde: true },
|
||||||
|
},
|
||||||
|
houses: {
|
||||||
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
||||||
|
asc: 0,
|
||||||
|
mc: 270,
|
||||||
|
},
|
||||||
|
elements: { Fire: 1, Stone: 2, Air: 1, Water: 3, Time: 1, Space: 2 },
|
||||||
|
aspects: [],
|
||||||
|
distinctions: {
|
||||||
|
"1": 0, "2": 0, "3": 0, "4": 0,
|
||||||
|
"5": 0, "6": 0, "7": 0, "8": 0,
|
||||||
|
"9": 0, "10": 0, "11": 0, "12": 0,
|
||||||
|
},
|
||||||
|
house_system: "P",
|
||||||
|
};
|
||||||
|
|
||||||
|
let svgEl, tooltipEl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// SVG element — D3 draws into this
|
||||||
|
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
svgEl.setAttribute("id", "id_natus_svg");
|
||||||
|
svgEl.setAttribute("width", "400");
|
||||||
|
svgEl.setAttribute("height", "400");
|
||||||
|
svgEl.style.width = "400px";
|
||||||
|
svgEl.style.height = "400px";
|
||||||
|
document.body.appendChild(svgEl);
|
||||||
|
|
||||||
|
// Tooltip portal — same markup as _natus_overlay.html
|
||||||
|
tooltipEl = document.createElement("div");
|
||||||
|
tooltipEl.id = "id_natus_tooltip";
|
||||||
|
tooltipEl.className = "tt";
|
||||||
|
tooltipEl.style.display = "none";
|
||||||
|
document.body.appendChild(tooltipEl);
|
||||||
|
|
||||||
|
NatusWheel.draw(svgEl, SYNTHETIC_CHART);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
NatusWheel.clear();
|
||||||
|
svgEl.remove();
|
||||||
|
tooltipEl.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T3 ── hover planet shows name / sign / in-sign degree + glow ─────────
|
||||||
|
|
||||||
|
it("T3: hovering a planet group adds the glow class and shows the tooltip with name, sign, and in-sign degree", () => {
|
||||||
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
||||||
|
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(sun.classList.contains("nw-planet--hover")).toBe(true);
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
const text = tooltipEl.textContent;
|
||||||
|
expect(text).toContain("Sun");
|
||||||
|
expect(text).toContain("Pisces");
|
||||||
|
// in-sign degree: 338.4° ecliptic − 330° (Pisces start) = 8.4°
|
||||||
|
expect(text).toContain("8.4");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T4 ── retrograde planet shows ℞ ──────────────────────────────────────
|
||||||
|
|
||||||
|
it("T4: hovering a retrograde planet shows ℞ in the tooltip", () => {
|
||||||
|
const mercury = svgEl.querySelector("[data-planet='Mercury']");
|
||||||
|
expect(mercury).not.toBeNull("expected [data-planet='Mercury'] to exist in the SVG");
|
||||||
|
|
||||||
|
mercury.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
expect(tooltipEl.textContent).toContain("℞");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── T5 ── mouseout hides tooltip and removes glow ─────────────────────────
|
||||||
|
|
||||||
|
it("T5: mouseout hides the tooltip and removes the glow class", () => {
|
||||||
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
||||||
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
||||||
|
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
||||||
|
expect(tooltipEl.style.display).toBe("block");
|
||||||
|
|
||||||
|
// relatedTarget is document.body — outside the planet group
|
||||||
|
sun.dispatchEvent(new MouseEvent("mouseout", {
|
||||||
|
bubbles: true,
|
||||||
|
relatedTarget: document.body,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(tooltipEl.style.display).toBe("none");
|
||||||
|
expect(sun.classList.contains("nw-planet--hover")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,11 +22,14 @@
|
|||||||
<script src="RoleSelectSpec.js"></script>
|
<script src="RoleSelectSpec.js"></script>
|
||||||
<script src="TraySpec.js"></script>
|
<script src="TraySpec.js"></script>
|
||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||||
<!-- Jasmine env config (optional) -->
|
<!-- Jasmine env config (optional) -->
|
||||||
<script src="lib/jasmine-6.0.1/boot1.js"></script>
|
<script src="lib/jasmine-6.0.1/boot1.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,10 @@
|
|||||||
</div>{# /.natus-modal-wrap #}
|
</div>{# /.natus-modal-wrap #}
|
||||||
</div>{# /.natus-overlay #}
|
</div>{# /.natus-overlay #}
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
{# Planet hover tooltip — position:fixed so it escapes overflow:hidden on the modal #}
|
||||||
|
<div id="id_natus_tooltip" class="tt" style="display:none;"></div>
|
||||||
|
|
||||||
|
<script src="{% static 'apps/gameboard/d3.min.js' %}"></script>
|
||||||
<script src="{% static 'apps/gameboard/natus-wheel.js' %}"></script>
|
<script src="{% static 'apps/gameboard/natus-wheel.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user