2026-04-16 01:57:02 -04:00
|
|
|
|
// ── NatusWheelSpec.js ─────────────────────────────────────────────────────────
|
|
|
|
|
|
//
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// Unit specs for natus-wheel.js — planet/element click-to-lock tooltips.
|
2026-04-16 01:57:02 -04:00
|
|
|
|
//
|
|
|
|
|
|
// DOM contract assumed:
|
|
|
|
|
|
// <svg id="id_natus_svg"> — target for NatusWheel.draw()
|
|
|
|
|
|
// <div id="id_natus_tooltip"> — tooltip portal (position:fixed on page)
|
|
|
|
|
|
//
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// Click-lock contract:
|
|
|
|
|
|
// click on [data-planet] group → adds .nw-planet--active class
|
|
|
|
|
|
// raises group to DOM front
|
|
|
|
|
|
// shows #id_natus_tooltip with
|
|
|
|
|
|
// planet name, in-sign degree, sign name,
|
|
|
|
|
|
// ℞ if retrograde, and "n / total" index
|
|
|
|
|
|
// click same planet again → removes .nw-planet--active; hides tooltip
|
|
|
|
|
|
// PRV / NXT buttons in tooltip → cycle to adjacent planet by ecliptic degree
|
2026-04-16 01:57:02 -04:00
|
|
|
|
//
|
|
|
|
|
|
// In-sign degree: ecliptic_longitude % 30 (e.g. 338.4° → 8.4° Pisces)
|
|
|
|
|
|
//
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// Shared chart — Sun (66.7°), Venus (63.3°), Mars (132.0°)
|
|
|
|
|
|
// Descending-degree (clockwise) order: Mars (132.0) → Sun (66.7) → Venus (63.3)
|
2026-04-19 00:16:05 -04:00
|
|
|
|
const CONJUNCTION_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
|
|
|
|
|
|
Venus: { sign: "Gemini", degree: 63.3, retrograde: false },
|
|
|
|
|
|
Mars: { sign: "Leo", degree: 132.0, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
|
|
|
|
|
|
asc: 180.0, mc: 90.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 1, Stone: 0, Air: 2, Water: 0, Time: 0, Space: 0 },
|
|
|
|
|
|
aspects: [],
|
|
|
|
|
|
distinctions: {
|
|
|
|
|
|
"1": 0, "2": 0, "3": 2, "4": 0, "5": 0, "6": 0,
|
|
|
|
|
|
"7": 0, "8": 0, "9": 1, "10": 0, "11": 0, "12": 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
house_system: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
describe("NatusWheel — planet click tooltips", () => {
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
|
|
|
|
|
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(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// ── T3 ── click planet shows name / sign / in-sign degree + glow ──────────
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
it("T3: clicking a planet group adds the active class and shows the tooltip with name, sign, and in-sign degree", () => {
|
2026-04-16 01:57:02 -04:00
|
|
|
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
expect(sun.classList.contains("nw-planet--active")).toBe(true);
|
2026-04-16 01:57:02 -04:00
|
|
|
|
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 ℞ ──────────────────────────────────────
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
it("T4: clicking a retrograde planet shows ℞ in the tooltip", () => {
|
2026-04-16 01:57:02 -04:00
|
|
|
|
const mercury = svgEl.querySelector("[data-planet='Mercury']");
|
|
|
|
|
|
expect(mercury).not.toBeNull("expected [data-planet='Mercury'] to exist in the SVG");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
mercury.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
expect(tooltipEl.textContent).toContain("℞");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// ── T5 ── clicking same planet again hides tooltip and removes active ──────
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
it("T5: clicking the same planet again hides the tooltip and removes the active class", () => {
|
2026-04-16 01:57:02 -04:00
|
|
|
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
expect(sun).not.toBeNull("expected [data-planet='Sun'] to exist in the SVG");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-16 01:57:02 -04:00
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-16 01:57:02 -04:00
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("none");
|
2026-04-19 17:27:52 -04:00
|
|
|
|
expect(sun.classList.contains("nw-planet--active")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── T6 ── tooltip shows PRV / NXT buttons ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
it("T6: tooltip contains PRV and NXT buttons after a planet click", () => {
|
|
|
|
|
|
const sun = svgEl.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.querySelector(".nw-tt-prv")).not.toBeNull("expected .nw-tt-prv button");
|
|
|
|
|
|
expect(tooltipEl.querySelector(".nw-tt-nxt")).not.toBeNull("expected .nw-tt-nxt button");
|
2026-04-16 01:57:02 -04:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
describe("NatusWheel — tick lines, raise, and cycle navigation", () => {
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
let svgEl2, tooltipEl;
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
|
|
|
|
svgEl2.setAttribute("id", "id_natus_svg_conj");
|
|
|
|
|
|
svgEl2.setAttribute("width", "400");
|
|
|
|
|
|
svgEl2.setAttribute("height", "400");
|
|
|
|
|
|
svgEl2.style.width = "400px";
|
|
|
|
|
|
svgEl2.style.height = "400px";
|
|
|
|
|
|
document.body.appendChild(svgEl2);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
tooltipEl.style.position = "fixed";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl2, CONJUNCTION_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl2.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// ── T7 ── tick present in DOM and extends past the zodiac ring ───────────
|
|
|
|
|
|
// Visibility is CSS-controlled (opacity-0 by default, revealed on --active).
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
|
|
|
|
|
it("T7: each planet has a tick line whose outer endpoint extends past the sign ring", () => {
|
|
|
|
|
|
const tick = svgEl2.querySelector(".nw-planet-tick");
|
|
|
|
|
|
expect(tick).not.toBeNull("expected at least one .nw-planet-tick element");
|
|
|
|
|
|
|
|
|
|
|
|
const cx = 200, cy = 200;
|
|
|
|
|
|
const x2 = parseFloat(tick.getAttribute("x2"));
|
|
|
|
|
|
const y2 = parseFloat(tick.getAttribute("y2"));
|
|
|
|
|
|
const rOuter = Math.sqrt((x2 - cx) ** 2 + (y2 - cy) ** 2);
|
|
|
|
|
|
// _r = Math.min(400,400) * 0.46 = 184; signOuter = _r * 0.90 = 165.6
|
|
|
|
|
|
const signOuter = 400 * 0.46 * 0.90;
|
|
|
|
|
|
expect(rOuter).toBeGreaterThan(signOuter);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// ── T8 ── click raises planet to front ────────────────────────────────────
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
it("T8: clicking a planet raises it to the last DOM position (visually on top)", () => {
|
2026-04-19 00:16:05 -04:00
|
|
|
|
const sun = svgEl2.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
const venus = svgEl2.querySelector("[data-planet='Venus']");
|
|
|
|
|
|
expect(sun).not.toBeNull("expected [data-planet='Sun']");
|
|
|
|
|
|
expect(venus).not.toBeNull("expected [data-planet='Venus']");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
venus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
|
|
|
|
|
const groups = Array.from(svgEl2.querySelectorAll(".nw-planet-group"));
|
|
|
|
|
|
expect(groups[groups.length - 1].getAttribute("data-planet")).toBe("Venus");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
// ── T9c ── NXT cycles clockwise (to lower ecliptic degree) ──────────────
|
|
|
|
|
|
// Descending order: Mars [idx 0] → Sun [idx 1] → Venus [idx 2]
|
|
|
|
|
|
// Clicking Sun (idx 1) then NXT should activate Venus (idx 2, lower degree = clockwise).
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
it("T9c: clicking NXT from Sun shows Venus (next planet clockwise = lower degree)", () => {
|
2026-04-19 00:16:05 -04:00
|
|
|
|
const sun = svgEl2.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
expect(sun).not.toBeNull("expected [data-planet='Sun']");
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
2026-04-19 00:16:05 -04:00
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
2026-04-19 17:27:52 -04:00
|
|
|
|
expect(tooltipEl.textContent).toContain("Sun");
|
|
|
|
|
|
|
|
|
|
|
|
const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
|
|
|
|
|
|
expect(nxtBtn).not.toBeNull("expected .nw-tt-nxt button in tooltip");
|
|
|
|
|
|
|
|
|
|
|
|
nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
expect(tooltipEl.textContent).toContain("Venus");
|
|
|
|
|
|
|
|
|
|
|
|
const venus = svgEl2.querySelector("[data-planet='Venus']");
|
|
|
|
|
|
expect(venus.classList.contains("nw-planet--active")).toBe(true);
|
|
|
|
|
|
expect(sun.classList.contains("nw-planet--active")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── T9n ── PRV cycles counterclockwise (to higher ecliptic degree) ────────
|
2026-04-26 18:42:47 -04:00
|
|
|
|
// CONJUNCTION_CHART merged sorted desc: ASC(180)→Mars(132)→MC(90)→Sun(66.7)→Venus(63.3)
|
|
|
|
|
|
// PRV from Sun (pos 3) → MC (pos 2, 90°) — angles and planets share the cycle.
|
2026-04-19 17:27:52 -04:00
|
|
|
|
|
2026-04-26 18:42:47 -04:00
|
|
|
|
it("T9n: clicking PRV from Sun shows MC (next higher ecliptic degree in merged cycle)", () => {
|
2026-04-19 17:27:52 -04:00
|
|
|
|
const sun = svgEl2.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
expect(sun).not.toBeNull("expected [data-planet='Sun']");
|
|
|
|
|
|
|
|
|
|
|
|
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const prvBtn = tooltipEl.querySelector(".nw-tt-prv");
|
|
|
|
|
|
prvBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
2026-04-26 18:42:47 -04:00
|
|
|
|
expect(tooltipEl.textContent).toContain("Midheaven");
|
|
|
|
|
|
const mc = svgEl2.querySelector("[data-angle='MC']");
|
|
|
|
|
|
expect(mc.classList.contains("nw-angle--active")).toBe(true);
|
|
|
|
|
|
expect(sun.classList.contains("nw-planet--active")).toBe(false);
|
2026-04-19 17:27:52 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-26 18:42:47 -04:00
|
|
|
|
// ── T9w ── NXT wraps clockwise from the lowest-degree item ───────────────
|
|
|
|
|
|
// Venus(63.3°) is lowest; NXT wraps to ASC(180°) — the highest-degree item.
|
2026-04-19 17:27:52 -04:00
|
|
|
|
|
2026-04-26 18:42:47 -04:00
|
|
|
|
it("T9w: cycling NXT from Venus (lowest degree) wraps clockwise to ASC (highest degree)", () => {
|
2026-04-19 17:27:52 -04:00
|
|
|
|
const venus = svgEl2.querySelector("[data-planet='Venus']");
|
|
|
|
|
|
venus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
|
|
|
|
|
|
nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
2026-04-26 18:42:47 -04:00
|
|
|
|
expect(tooltipEl.textContent).toContain("Ascendant");
|
|
|
|
|
|
const asc = svgEl2.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
expect(asc.classList.contains("nw-angle--active")).toBe(true);
|
2026-04-19 17:27:52 -04:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── Half-wheel tooltip positioning ───────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Tooltip lands in the opposite vertical half, with horizontal edge anchored
|
|
|
|
|
|
// to the item's screen edge on the same L/R side.
|
|
|
|
|
|
//
|
|
|
|
|
|
// SVG: 400×400 at viewport origin → centre = (200, 200).
|
|
|
|
|
|
// Tooltip offsetWidth/Height: 0 in JSDOM (no layout engine) → ttW=ttH=0.
|
|
|
|
|
|
// Clamping uses svgRect bounds (not window.inner*), so no viewport mock needed.
|
|
|
|
|
|
// REM = 16 px. Item circle: 20×20 px around mock centre.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Vertical results (item circle centre at y):
|
|
|
|
|
|
// y ≥ 200 (lower half): top = svgCY - REM - ttH = 200 - 16 - 0 = 184
|
|
|
|
|
|
// y < 200 (upper half): top = svgCY + REM = 200 + 16 = 216
|
|
|
|
|
|
//
|
|
|
|
|
|
// Horizontal results (item circle centre at x, radius=10):
|
|
|
|
|
|
// x < 200 (left side): left = iRect.left = x - 10
|
|
|
|
|
|
// x ≥ 200 (right side): left = iRect.right - ttW = x + 10 - 0 = x + 10
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-21 03:17:20 -04:00
|
|
|
|
// ── DON / DOFF aspect line persistence ───────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Aspect lines belong to the page session, not the tooltip:
|
|
|
|
|
|
// - DON draws lines into .nw-aspects and disables DON btn (shows ×)
|
|
|
|
|
|
// - closing the tooltip does NOT clear lines
|
|
|
|
|
|
// - re-opening the SAME planet preserves _aspectsVisible → DON still disabled
|
|
|
|
|
|
// - opening a DIFFERENT planet resets state: lines cleared, DON active
|
|
|
|
|
|
// - DOFF clears lines; re-opening same planet finds DON active
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("NatusWheel — DON/DOFF aspect line persistence", () => {
|
|
|
|
|
|
|
|
|
|
|
|
const ASPECT_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Capricorn", degree: 280.4, retrograde: false },
|
|
|
|
|
|
Moon: { sign: "Scorpio", degree: 220.1, retrograde: false },
|
|
|
|
|
|
Mars: { sign: "Taurus", degree: 40.7, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
|
|
|
|
|
asc: 0, mc: 270,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 0, Stone: 0, Air: 0, Water: 1, Time: 0, Space: 0 },
|
|
|
|
|
|
aspects: [
|
|
|
|
|
|
{ planet1: "Sun", planet2: "Mars", type: "Trine",
|
|
|
|
|
|
orb: 0.3, angle: 120, applying_planet: "Sun" },
|
|
|
|
|
|
{ planet1: "Sun", planet2: "Moon", type: "Sextile",
|
|
|
|
|
|
orb: 2.9, angle: 60, applying_planet: "Moon" },
|
|
|
|
|
|
],
|
|
|
|
|
|
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: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl, ASPECT_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function clickPlanet(name) {
|
|
|
|
|
|
svgEl.querySelector(`[data-planet="${name}"]`)
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
}
|
|
|
|
|
|
function clickDon() { tooltipEl.querySelector(".nw-asp-don") .dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
|
|
|
|
|
function clickDoff() { tooltipEl.querySelector(".nw-asp-doff").dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
|
|
|
|
|
function aspectLines() { return svgEl.querySelectorAll(".nw-aspects line").length; }
|
|
|
|
|
|
function donDisabled() { return tooltipEl.querySelector(".nw-asp-don").classList.contains("btn-disabled"); }
|
|
|
|
|
|
|
|
|
|
|
|
// T11a — DON draws lines
|
|
|
|
|
|
it("T11a: clicking DON draws aspect lines into .nw-aspects", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
expect(aspectLines()).toBe(0);
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
expect(aspectLines()).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T11b — closing tooltip must not clear aspect lines
|
|
|
|
|
|
it("T11b: closing the tooltip (outside click) does not clear aspect lines", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
const lineCount = aspectLines();
|
|
|
|
|
|
expect(lineCount).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("none");
|
|
|
|
|
|
expect(aspectLines()).toBe(lineCount);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T11c — re-opening same planet preserves DON-disabled state
|
|
|
|
|
|
it("T11c: re-opening the same planet after DON keeps DON disabled (lines still active)", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
expect(donDisabled()).toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
|
|
|
|
|
|
expect(donDisabled()).toBe(true);
|
|
|
|
|
|
expect(aspectLines()).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T11d — switching planet leaves previous DONned lines intact; DON active for new planet
|
|
|
|
|
|
it("T11d: opening a different planet leaves DONned lines intact — DON active for new planet", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
const lineCount = aspectLines();
|
|
|
|
|
|
expect(lineCount).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
clickPlanet("Moon");
|
|
|
|
|
|
|
|
|
|
|
|
expect(donDisabled()).toBe(false); // Moon's DON is fresh/active
|
|
|
|
|
|
expect(aspectLines()).toBe(lineCount); // Sun's lines still there
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T11f — DONning a second planet replaces the first planet's lines + tick
|
|
|
|
|
|
it("T11f: clicking DON on a second planet clears the first planet's lines", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
expect(aspectLines()).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
clickPlanet("Moon");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
|
|
|
|
|
|
expect(donDisabled()).toBe(true); // Moon's DON now disabled
|
|
|
|
|
|
// Moon aspects — Sun's lines replaced (lines may be 0 if Moon has no aspects)
|
|
|
|
|
|
const sunGrp = svgEl.querySelector('[data-planet="Sun"]');
|
|
|
|
|
|
expect(sunGrp.classList.contains('nw-planet--asp-active')).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T11e — DOFF clears lines; re-opening same planet starts fresh
|
|
|
|
|
|
it("T11e: DOFF clears lines; re-opening same planet finds DON active again", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
clickDoff();
|
|
|
|
|
|
expect(aspectLines()).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
document.body.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
|
|
|
|
|
|
expect(donDisabled()).toBe(false);
|
|
|
|
|
|
expect(aspectLines()).toBe(0);
|
|
|
|
|
|
});
|
2026-04-21 16:04:32 -04:00
|
|
|
|
|
|
|
|
|
|
// T11g — PRV/NXT navigation back to a DONned planet must restore DOFF state
|
|
|
|
|
|
it("T11g: navigating away via NXT then back via PRV restores DOFF-active state", () => {
|
|
|
|
|
|
clickPlanet("Sun");
|
|
|
|
|
|
clickDon();
|
|
|
|
|
|
expect(donDisabled()).toBe(true); // DON disabled, aspects active
|
|
|
|
|
|
|
|
|
|
|
|
// Navigate away to next planet
|
|
|
|
|
|
tooltipEl.querySelector(".nw-tt-nxt")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(donDisabled()).toBe(false); // new planet — DON fresh/active
|
|
|
|
|
|
|
|
|
|
|
|
// Navigate back to Sun
|
|
|
|
|
|
tooltipEl.querySelector(".nw-tt-prv")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
// Sun's aspects are still drawn — DOFF must be active, DON must be disabled
|
|
|
|
|
|
expect(donDisabled()).toBe(true);
|
|
|
|
|
|
expect(aspectLines()).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
2026-04-21 03:17:20 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-19 17:27:52 -04:00
|
|
|
|
xdescribe("NatusWheel — half-wheel tooltip positioning", () => {
|
|
|
|
|
|
|
|
|
|
|
|
const HALF_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
// Vesta 90° → SVG (200, 274) — BELOW centre
|
|
|
|
|
|
// Ceres 270° → SVG (200, 126) — ABOVE centre
|
|
|
|
|
|
Vesta: { sign: "Cancer", degree: 90, retrograde: false },
|
|
|
|
|
|
Ceres: { sign: "Capricorn", degree: 270, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
|
|
|
|
|
asc: 0, mc: 270,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 0, Stone: 1, Air: 0, Water: 1, Time: 0, Space: 0 },
|
|
|
|
|
|
aspects: [],
|
|
|
|
|
|
distinctions: {
|
|
|
|
|
|
"1": 0, "2": 0, "3": 0, "4": 1,
|
|
|
|
|
|
"5": 0, "6": 0, "7": 0, "8": 0,
|
|
|
|
|
|
"9": 0, "10": 1, "11": 0, "12": 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
house_system: "P",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl3, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
svgEl3 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
|
|
|
|
svgEl3.setAttribute("id", "id_natus_svg_half");
|
|
|
|
|
|
svgEl3.setAttribute("width", "400");
|
|
|
|
|
|
svgEl3.setAttribute("height", "400");
|
|
|
|
|
|
svgEl3.style.width = "400px";
|
|
|
|
|
|
svgEl3.style.height = "400px";
|
|
|
|
|
|
document.body.appendChild(svgEl3);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
tooltipEl.style.position = "fixed";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
// Simulate SVG occupying [0,400]×[0,400] in the viewport.
|
|
|
|
|
|
// Clamping uses svgRect bounds, so no need to mock window.inner*.
|
|
|
|
|
|
spyOn(svgEl3, "getBoundingClientRect").and.returnValue(
|
|
|
|
|
|
{ left: 0, top: 0, width: 400, height: 400, right: 400, bottom: 400 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl3, HALF_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl3.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function mockPlanetAt(name, screenX, screenY) {
|
|
|
|
|
|
const grp = svgEl3.querySelector(`[data-planet="${name}"]`);
|
|
|
|
|
|
const circle = grp && (grp.querySelector("circle") || grp);
|
|
|
|
|
|
if (circle) {
|
|
|
|
|
|
spyOn(circle, "getBoundingClientRect").and.returnValue({
|
|
|
|
|
|
left: screenX - 10, top: screenY - 10,
|
|
|
|
|
|
width: 20, height: 20,
|
|
|
|
|
|
right: screenX + 10, bottom: screenY + 10,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// T10a — lower half: lower edge of tooltip sits 1rem above centreline
|
|
|
|
|
|
it("T10a: planet in lower half places tooltip lower-edge 1rem above the centreline (top=184)", () => {
|
|
|
|
|
|
mockPlanetAt("Vesta", 200, 274);
|
|
|
|
|
|
svgEl3.querySelector("[data-planet='Vesta']")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(parseFloat(tooltipEl.style.top)).toBe(184);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T10b — upper half: upper edge of tooltip sits 1rem below centreline
|
|
|
|
|
|
it("T10b: planet in upper half places tooltip upper-edge 1rem below the centreline (top=216)", () => {
|
|
|
|
|
|
mockPlanetAt("Ceres", 200, 126);
|
|
|
|
|
|
svgEl3.querySelector("[data-planet='Ceres']")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(parseFloat(tooltipEl.style.top)).toBe(216);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T10c — left side: tooltip left edge aligns with item left edge (x=140 → left=130)
|
|
|
|
|
|
it("T10c: planet on left side of wheel aligns tooltip left edge with item left edge", () => {
|
|
|
|
|
|
mockPlanetAt("Vesta", 140, 274); // itemRect.left = 130
|
|
|
|
|
|
svgEl3.querySelector("[data-planet='Vesta']")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(parseFloat(tooltipEl.style.left)).toBe(130);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T10d — right side: tooltip right edge aligns with item right edge (x=260 → left=270-ttW=270)
|
|
|
|
|
|
it("T10d: planet on right side of wheel aligns tooltip right edge with item right edge", () => {
|
|
|
|
|
|
mockPlanetAt("Vesta", 260, 274); // iRect.right=270, ttW=0 → left=270
|
|
|
|
|
|
svgEl3.querySelector("[data-planet='Vesta']")
|
|
|
|
|
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(parseFloat(tooltipEl.style.left)).toBe(270);
|
2026-04-19 00:16:05 -04:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-21 20:07:40 -04:00
|
|
|
|
|
|
|
|
|
|
// ── T14 — element tooltip shows contributor planets (enriched data) ───────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// When element data arrives in enriched format {count, contributors/stellia/parades},
|
|
|
|
|
|
// clicking a classic-element slice lists contributor planet names in the tooltip.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("NatusWheel — element tooltip contributor display", () => {
|
|
|
|
|
|
|
|
|
|
|
|
const ENRICHED_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
|
|
|
|
|
|
Venus: { sign: "Gemini", degree: 63.3, retrograde: false },
|
|
|
|
|
|
Mars: { sign: "Leo", degree: 132.0, retrograde: false },
|
|
|
|
|
|
Saturn: { sign: "Virgo", degree: 153.0, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150],
|
|
|
|
|
|
asc: 180.0, mc: 90.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: {
|
|
|
|
|
|
Fire: { count: 2, contributors: [
|
|
|
|
|
|
{ planet: "Sun", sign: "Gemini" },
|
|
|
|
|
|
{ planet: "Mars", sign: "Leo" },
|
|
|
|
|
|
]},
|
|
|
|
|
|
Stone: { count: 1, contributors: [
|
|
|
|
|
|
{ planet: "Saturn", sign: "Virgo" },
|
|
|
|
|
|
]},
|
|
|
|
|
|
Air: { count: 0, contributors: [] },
|
|
|
|
|
|
Water: { count: 0, contributors: [] },
|
|
|
|
|
|
Time: { count: 1, stellia: [
|
|
|
|
|
|
{ sign: "Gemini", planets: [
|
|
|
|
|
|
{ planet: "Sun", sign: "Gemini" },
|
|
|
|
|
|
{ planet: "Venus", sign: "Gemini" },
|
|
|
|
|
|
]},
|
|
|
|
|
|
]},
|
|
|
|
|
|
Space: { count: 1, parades: [
|
|
|
|
|
|
{ signs: ["Gemini", "Leo", "Virgo"],
|
|
|
|
|
|
planets: [
|
|
|
|
|
|
{ planet: "Sun", sign: "Gemini" },
|
|
|
|
|
|
{ planet: "Mars", sign: "Leo" },
|
|
|
|
|
|
{ planet: "Saturn", sign: "Virgo" },
|
|
|
|
|
|
]},
|
|
|
|
|
|
]},
|
|
|
|
|
|
},
|
|
|
|
|
|
aspects: [],
|
|
|
|
|
|
distinctions: { "1":0,"2":0,"3":2,"4":0,"5":0,"6":0,"7":0,"8":0,"9":1,"10":0,"11":0,"12":0 },
|
|
|
|
|
|
house_system: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl, ENRICHED_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T14a — Fire slice lists contributor planet symbols ☉ (Sun) and ♂ (Mars)
|
|
|
|
|
|
it("T14a: clicking Fire element slice shows contributor planet symbols", () => {
|
|
|
|
|
|
const fireSlice = svgEl.querySelector("[data-element='Fire']");
|
|
|
|
|
|
expect(fireSlice).not.toBeNull();
|
|
|
|
|
|
fireSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).not.toBe("none");
|
|
|
|
|
|
const body = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(body).toContain("☉"); // Sun
|
|
|
|
|
|
expect(body).toContain("♂"); // Mars
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T14b — Space slice shows parade formation with planet symbols
|
|
|
|
|
|
it("T14b: clicking Space element slice shows parade formation block", () => {
|
|
|
|
|
|
const spaceSlice = svgEl.querySelector("[data-element='Space']");
|
|
|
|
|
|
spaceSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const body = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(body).toContain("Parade");
|
|
|
|
|
|
// Planet symbols for Sun ☉, Mars ♂, Saturn ♄ appear in the parade
|
|
|
|
|
|
expect(body).toContain("☉");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T14c — Time slice shows stellium formation with planet symbols
|
|
|
|
|
|
it("T14c: clicking Time element slice shows stellium formation block", () => {
|
|
|
|
|
|
const timeSlice = svgEl.querySelector("[data-element='Time']");
|
|
|
|
|
|
timeSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const body = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(body).toContain("Stellium");
|
|
|
|
|
|
// Sun ☉ and Venus ♀ are in the Gemini stellium
|
|
|
|
|
|
expect(body).toContain("☉");
|
|
|
|
|
|
expect(body).toContain("♀");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T14d — Air slice (count 0) shows em dash fallback, not an empty list
|
|
|
|
|
|
it("T14d: empty element slice shows em dash fallback", () => {
|
|
|
|
|
|
const airSlice = svgEl.querySelector("[data-element='Air']");
|
|
|
|
|
|
airSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const body = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(body).toContain("—");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── T12 — sign ring click tooltips ────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Clicking a sign slice shows:
|
|
|
|
|
|
// ♉ Taurus · Stone
|
|
|
|
|
|
// Clicking the same sign again closes the tooltip.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("NatusWheel — sign ring click tooltips", () => {
|
|
|
|
|
|
|
|
|
|
|
|
const SIGN_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Taurus", degree: 40.0, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
|
|
|
|
|
asc: 0,
|
|
|
|
|
|
mc: 270,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 0, Stone: 1, Air: 0, Water: 0, Time: 0, Space: 0 },
|
|
|
|
|
|
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: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl, SIGN_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T12a — clicking a sign shows symbol + name + element
|
|
|
|
|
|
it("T12a: clicking a sign slice shows sign symbol, name, and element", () => {
|
|
|
|
|
|
const taurusSlice = svgEl.querySelector("[data-sign-name='Taurus']");
|
|
|
|
|
|
expect(taurusSlice).not.toBeNull();
|
|
|
|
|
|
taurusSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).not.toBe("none");
|
|
|
|
|
|
const bodyText = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(bodyText).toContain("Taurus");
|
|
|
|
|
|
expect(bodyText).toContain("Stone");
|
|
|
|
|
|
expect(bodyText).toContain("♉");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T12b — clicking same sign again closes the tooltip
|
|
|
|
|
|
it("T12b: clicking the same sign again hides the tooltip", () => {
|
|
|
|
|
|
const taurusSlice = svgEl.querySelector("[data-sign-name='Taurus']");
|
|
|
|
|
|
taurusSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).not.toBe("none");
|
|
|
|
|
|
|
|
|
|
|
|
taurusSlice.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("none");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── T13 — house ring click tooltips ───────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Clicking a house slice shows:
|
|
|
|
|
|
// 3 · Education
|
|
|
|
|
|
// Clicking the same house again closes the tooltip.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("NatusWheel — house ring click tooltips", () => {
|
|
|
|
|
|
|
|
|
|
|
|
const HOUSE_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
|
|
|
|
|
asc: 0,
|
|
|
|
|
|
mc: 270,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 1, Stone: 0, Air: 0, Water: 0, Time: 0, Space: 0 },
|
|
|
|
|
|
aspects: [],
|
|
|
|
|
|
distinctions: { "1":0,"2":0,"3":1,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0 },
|
|
|
|
|
|
house_system: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl, HOUSE_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T13a — clicking a house slice shows house number + label
|
|
|
|
|
|
it("T13a: clicking a house slice shows house number and house label", () => {
|
|
|
|
|
|
// House 3 is at index 2 (zero-based), cusps[2]=60° span
|
|
|
|
|
|
const house3 = svgEl.querySelector("[data-house='3']");
|
|
|
|
|
|
expect(house3).not.toBeNull();
|
|
|
|
|
|
house3.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).not.toBe("none");
|
|
|
|
|
|
const bodyText = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(bodyText).toContain("3");
|
|
|
|
|
|
expect(bodyText).toContain("Education");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T13b — clicking same house again closes the tooltip
|
|
|
|
|
|
it("T13b: clicking the same house again hides the tooltip", () => {
|
|
|
|
|
|
const house3 = svgEl.querySelector("[data-house='3']");
|
|
|
|
|
|
house3.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).not.toBe("none");
|
|
|
|
|
|
|
|
|
|
|
|
house3.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("none");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-22 00:58:19 -04:00
|
|
|
|
|
|
|
|
|
|
// ── T15 — ASC / MC angle click tooltips + aspect lines ────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// ASC and MC labels are clickable groups ([data-angle='ASC'] / [data-angle='MC']).
|
|
|
|
|
|
// Clicking shows a tooltip similar to a planet tooltip:
|
|
|
|
|
|
// Title: "Ascendant" (ASC) or "Midheaven" (MC)
|
|
|
|
|
|
// Degree in sign + sign name, plus the house number the angle defines.
|
|
|
|
|
|
// Aspect list for planets within 10° orb of the angle (client-side computed).
|
|
|
|
|
|
// Aspect lines drawn to those planets.
|
|
|
|
|
|
// Clicking same angle again closes the tooltip.
|
|
|
|
|
|
// Planet tooltips include angle aspects in their own aspect lists.
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("NatusWheel — angle (ASC/MC) click tooltips", () => {
|
|
|
|
|
|
|
|
|
|
|
|
// ASC=0°(Aries): Sun@8° → Conjunction orb 8° ✓; Mars@188° → Opposition orb 8° ✓
|
|
|
|
|
|
// MC=90°(Cancer): Moon@97° → Conjunction orb 7° ✓
|
|
|
|
|
|
const ANGLE_CHART = {
|
|
|
|
|
|
planets: {
|
|
|
|
|
|
Sun: { sign: "Aries", degree: 8.0, retrograde: false },
|
|
|
|
|
|
Moon: { sign: "Cancer", degree: 97.0, retrograde: false },
|
|
|
|
|
|
Mars: { sign: "Libra", degree: 188.0, retrograde: false },
|
|
|
|
|
|
},
|
|
|
|
|
|
houses: {
|
|
|
|
|
|
cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
|
|
|
|
|
|
asc: 0.0,
|
|
|
|
|
|
mc: 90.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
elements: { Fire: 2, Stone: 0, Air: 1, Water: 1, Time: 0, Space: 0 },
|
|
|
|
|
|
aspects: [],
|
|
|
|
|
|
distinctions: {
|
|
|
|
|
|
"1":1,"2":0,"3":0,"4":1,"5":0,"6":0,
|
|
|
|
|
|
"7":1,"8":0,"9":0,"10":1,"11":0,"12":0,
|
|
|
|
|
|
},
|
|
|
|
|
|
house_system: "O",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let svgEl, tooltipEl;
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
tooltipEl = document.createElement("div");
|
|
|
|
|
|
tooltipEl.id = "id_natus_tooltip";
|
|
|
|
|
|
tooltipEl.className = "tt";
|
|
|
|
|
|
tooltipEl.style.display = "none";
|
|
|
|
|
|
document.body.appendChild(tooltipEl);
|
|
|
|
|
|
|
|
|
|
|
|
NatusWheel.draw(svgEl, ANGLE_CHART);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
NatusWheel.clear();
|
|
|
|
|
|
svgEl.remove();
|
|
|
|
|
|
tooltipEl.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15a — ASC label is a clickable group with data-angle='ASC'
|
|
|
|
|
|
it("T15a: clicking [data-angle='ASC'] shows tooltip with 'Ascendant' title and house 1", () => {
|
|
|
|
|
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
expect(ascGroup).not.toBeNull("expected [data-angle='ASC'] to exist in the SVG");
|
|
|
|
|
|
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
const text = tooltipEl.textContent;
|
|
|
|
|
|
expect(text).toContain("Ascendant");
|
|
|
|
|
|
// ASC=0° is the cusp of House 1
|
|
|
|
|
|
expect(text).toContain("1");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15b — MC label is a clickable group with data-angle='MC'
|
|
|
|
|
|
it("T15b: clicking [data-angle='MC'] shows tooltip with 'Midheaven' title and house 10", () => {
|
|
|
|
|
|
const mcGroup = svgEl.querySelector("[data-angle='MC']");
|
|
|
|
|
|
expect(mcGroup).not.toBeNull("expected [data-angle='MC'] to exist in the SVG");
|
|
|
|
|
|
|
|
|
|
|
|
mcGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
const text = tooltipEl.textContent;
|
|
|
|
|
|
expect(text).toContain("Midheaven");
|
|
|
|
|
|
expect(text).toContain("10");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15c — ASC tooltip shows degree-in-sign and sign name
|
|
|
|
|
|
it("T15c: ASC tooltip shows the in-sign degree and sign of the Ascendant", () => {
|
|
|
|
|
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const text = tooltipEl.textContent;
|
|
|
|
|
|
// ASC=0° → 0° Aries
|
|
|
|
|
|
expect(text).toContain("Aries");
|
|
|
|
|
|
expect(text).toContain("0.0");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15d — clicking same angle a second time hides the tooltip
|
|
|
|
|
|
it("T15d: clicking the same angle again hides the tooltip", () => {
|
|
|
|
|
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("block");
|
|
|
|
|
|
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
expect(tooltipEl.style.display).toBe("none");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15e — ASC tooltip lists planets within 10° orb (client-side computed)
|
|
|
|
|
|
it("T15e: ASC tooltip includes aspect rows for planets within 10° orb", () => {
|
|
|
|
|
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
// Sun at 8° → Conjunction (orb 8°) ✓ — _pSym emits data-planet attr
|
|
|
|
|
|
expect(bodyHtml).toContain('data-planet="Sun"');
|
|
|
|
|
|
// Mars at 188° → Opposition to ASC (0°), angular distance 172° → orb 8° ✓
|
|
|
|
|
|
expect(bodyHtml).toContain('data-planet="Mars"');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15f — planet tooltip includes ASC in its aspect list when within orb
|
|
|
|
|
|
it("T15f: planet tooltip for Sun lists ASC as an aspect partner", () => {
|
|
|
|
|
|
const sunGroup = svgEl.querySelector("[data-planet='Sun']");
|
|
|
|
|
|
expect(sunGroup).not.toBeNull();
|
|
|
|
|
|
|
|
|
|
|
|
sunGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const bodyHtml = tooltipEl.querySelector(".nw-tt-body").innerHTML;
|
|
|
|
|
|
expect(bodyHtml).toContain("ASC");
|
|
|
|
|
|
});
|
2026-04-26 18:42:47 -04:00
|
|
|
|
|
|
|
|
|
|
// T15g — NXT from a planet steps into an angle when angle is next by degree
|
|
|
|
|
|
// ANGLE_CHART sorted descending: Mars(188)→Moon(97)→MC(90)→Sun(8)→ASC(0)
|
|
|
|
|
|
// Moon is idx 1; NXT steps to MC (idx 2).
|
|
|
|
|
|
it("T15g: clicking NXT from Moon (97°) activates MC (90°, next clockwise)", () => {
|
|
|
|
|
|
const moonGroup = svgEl.querySelector("[data-planet='Moon']");
|
|
|
|
|
|
expect(moonGroup).not.toBeNull("expected [data-planet='Moon']");
|
|
|
|
|
|
moonGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
|
|
|
|
|
|
nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.textContent).toContain("Midheaven");
|
|
|
|
|
|
const mcGroup = svgEl.querySelector("[data-angle='MC']");
|
|
|
|
|
|
expect(mcGroup.classList.contains("nw-angle--active")).toBe(true);
|
|
|
|
|
|
expect(moonGroup.classList.contains("nw-planet--active")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15h — PRV from an angle steps back into a planet
|
|
|
|
|
|
it("T15h: clicking PRV from MC (90°) activates Moon (97°, previous counterclockwise)", () => {
|
|
|
|
|
|
const mcGroup = svgEl.querySelector("[data-angle='MC']");
|
|
|
|
|
|
mcGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const prvBtn = tooltipEl.querySelector(".nw-tt-prv");
|
|
|
|
|
|
prvBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const moonGroup = svgEl.querySelector("[data-planet='Moon']");
|
|
|
|
|
|
expect(moonGroup.classList.contains("nw-planet--active")).toBe(true);
|
|
|
|
|
|
expect(mcGroup.classList.contains("nw-angle--active")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// T15i — NXT from ASC (lowest degree, 0°) wraps to Mars (highest degree, 188°)
|
|
|
|
|
|
it("T15i: NXT from ASC (0°, lowest) wraps clockwise to Mars (188°, highest)", () => {
|
|
|
|
|
|
const ascGroup = svgEl.querySelector("[data-angle='ASC']");
|
|
|
|
|
|
ascGroup.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
const nxtBtn = tooltipEl.querySelector(".nw-tt-nxt");
|
|
|
|
|
|
nxtBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
|
|
|
|
|
|
|
|
|
|
expect(tooltipEl.textContent).toContain("Mars");
|
|
|
|
|
|
const marsGroup = svgEl.querySelector("[data-planet='Mars']");
|
|
|
|
|
|
expect(marsGroup.classList.contains("nw-planet--active")).toBe(true);
|
|
|
|
|
|
});
|
2026-04-22 00:58:19 -04:00
|
|
|
|
});
|