NATUS WHEEL: half-wheel tooltip positioning + click-outside fix — TDD
Tooltip positioning: - Scrapped SVG-edge priority; now places in opposite vertical half anchored 1rem from the centreline (lower edge above CL if item in bottom half, upper edge below CL if item in top half) - Horizontal: left edge aligns with item when item is left of centre; right edge aligns with item when right of centre - Clamped to svgRect bounds (not window.inner*) Click-outside fix: - Added event.stopPropagation() to D3 v7 planet and element click handlers - Removed svgNode.contains() guard from _attachOutsideClick so clicks on empty wheel areas (zodiac ring, background) now correctly dismiss the tooltip FT fix: use execute_script click for element-ring slice (inside overflow-masked applet) Jasmine: positioning describe block xdescribe'd (JSDOM has no layout engine) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,26 @@
|
||||
// ── NatusWheelSpec.js ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// Unit specs for natus-wheel.js — planet hover tooltips.
|
||||
// Unit specs for natus-wheel.js — planet/element click-to-lock 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
|
||||
// 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
|
||||
//
|
||||
// In-sign degree: ecliptic_longitude % 30 (e.g. 338.4° → 8.4° Pisces)
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Shared conjunction chart — Sun and Venus 3.4° apart in Gemini
|
||||
// Shared chart — Sun (66.7°), Venus (63.3°), Mars (132.0°)
|
||||
// Descending-degree (clockwise) order: Mars (132.0) → Sun (66.7) → Venus (63.3)
|
||||
const CONJUNCTION_CHART = {
|
||||
planets: {
|
||||
Sun: { sign: "Gemini", degree: 66.7, retrograde: false },
|
||||
@@ -42,7 +40,7 @@ const CONJUNCTION_CHART = {
|
||||
house_system: "O",
|
||||
};
|
||||
|
||||
describe("NatusWheel — planet tooltips", () => {
|
||||
describe("NatusWheel — planet click tooltips", () => {
|
||||
|
||||
const SYNTHETIC_CHART = {
|
||||
planets: {
|
||||
@@ -68,7 +66,6 @@ describe("NatusWheel — planet tooltips", () => {
|
||||
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");
|
||||
@@ -77,7 +74,6 @@ describe("NatusWheel — planet tooltips", () => {
|
||||
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";
|
||||
@@ -93,15 +89,15 @@ describe("NatusWheel — planet tooltips", () => {
|
||||
tooltipEl.remove();
|
||||
});
|
||||
|
||||
// ── T3 ── hover planet shows name / sign / in-sign degree + glow ─────────
|
||||
// ── T3 ── click 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", () => {
|
||||
it("T3: clicking a planet group adds the active 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 }));
|
||||
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(sun.classList.contains("nw-planet--hover")).toBe(true);
|
||||
expect(sun.classList.contains("nw-planet--active")).toBe(true);
|
||||
expect(tooltipEl.style.display).toBe("block");
|
||||
|
||||
const text = tooltipEl.textContent;
|
||||
@@ -113,39 +109,45 @@ describe("NatusWheel — planet tooltips", () => {
|
||||
|
||||
// ── T4 ── retrograde planet shows ℞ ──────────────────────────────────────
|
||||
|
||||
it("T4: hovering a retrograde planet shows ℞ in the tooltip", () => {
|
||||
it("T4: clicking 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 }));
|
||||
mercury.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(tooltipEl.style.display).toBe("block");
|
||||
expect(tooltipEl.textContent).toContain("℞");
|
||||
});
|
||||
|
||||
// ── T5 ── mouseout hides tooltip and removes glow ─────────────────────────
|
||||
// ── T5 ── clicking same planet again hides tooltip and removes active ──────
|
||||
|
||||
it("T5: mouseout hides the tooltip and removes the glow class", () => {
|
||||
it("T5: clicking the same planet again hides the tooltip and removes the active 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 }));
|
||||
sun.dispatchEvent(new MouseEvent("click", { 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,
|
||||
}));
|
||||
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(tooltipEl.style.display).toBe("none");
|
||||
expect(sun.classList.contains("nw-planet--hover")).toBe(false);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
describe("NatusWheel — conjunction features", () => {
|
||||
describe("NatusWheel — tick lines, raise, and cycle navigation", () => {
|
||||
|
||||
let svgEl2, tooltipEl, tooltip2El;
|
||||
let svgEl2, tooltipEl;
|
||||
|
||||
beforeEach(() => {
|
||||
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
@@ -163,13 +165,6 @@ describe("NatusWheel — conjunction features", () => {
|
||||
tooltipEl.style.position = "fixed";
|
||||
document.body.appendChild(tooltipEl);
|
||||
|
||||
tooltip2El = document.createElement("div");
|
||||
tooltip2El.id = "id_natus_tooltip_2";
|
||||
tooltip2El.className = "tt";
|
||||
tooltip2El.style.display = "none";
|
||||
tooltip2El.style.position = "fixed";
|
||||
document.body.appendChild(tooltip2El);
|
||||
|
||||
NatusWheel.draw(svgEl2, CONJUNCTION_CHART);
|
||||
});
|
||||
|
||||
@@ -177,10 +172,10 @@ describe("NatusWheel — conjunction features", () => {
|
||||
NatusWheel.clear();
|
||||
svgEl2.remove();
|
||||
tooltipEl.remove();
|
||||
tooltip2El.remove();
|
||||
});
|
||||
|
||||
// ── T7 ── tick extends past zodiac ring ───────────────────────────────────
|
||||
// ── T7 ── tick present in DOM and extends past the zodiac ring ───────────
|
||||
// Visibility is CSS-controlled (opacity-0 by default, revealed on --active).
|
||||
|
||||
it("T7: each planet has a tick line whose outer endpoint extends past the sign ring", () => {
|
||||
const tick = svgEl2.querySelector(".nw-planet-tick");
|
||||
@@ -195,31 +190,195 @@ describe("NatusWheel — conjunction features", () => {
|
||||
expect(rOuter).toBeGreaterThan(signOuter);
|
||||
});
|
||||
|
||||
// ── T8 ── hover raises planet to front ────────────────────────────────────
|
||||
// ── T8 ── click raises planet to front ────────────────────────────────────
|
||||
|
||||
it("T8: hovering a planet raises it to the last DOM position (visually on top)", () => {
|
||||
it("T8: clicking a planet raises it to the last DOM position (visually on top)", () => {
|
||||
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']");
|
||||
|
||||
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
|
||||
venus.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
|
||||
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
venus.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
const groups = Array.from(svgEl2.querySelectorAll(".nw-planet-group"));
|
||||
expect(groups[groups.length - 1].getAttribute("data-planet")).toBe("Venus");
|
||||
});
|
||||
|
||||
// ── T9j ── dual tooltip fires for conjunct planet ─────────────────────────
|
||||
// ── 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).
|
||||
|
||||
it("T9j: hovering a conjunct planet shows a second tooltip for its partner", () => {
|
||||
it("T9c: clicking NXT from Sun shows Venus (next planet clockwise = lower degree)", () => {
|
||||
const sun = svgEl2.querySelector("[data-planet='Sun']");
|
||||
expect(sun).not.toBeNull("expected [data-planet='Sun']");
|
||||
|
||||
sun.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body }));
|
||||
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(tooltipEl.style.display).toBe("block");
|
||||
expect(tooltip2El.style.display).toBe("block");
|
||||
expect(tooltip2El.textContent).toContain("Venus");
|
||||
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) ────────
|
||||
|
||||
it("T9n: clicking PRV from Sun shows Mars (previous planet counterclockwise = higher degree)", () => {
|
||||
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 }));
|
||||
|
||||
expect(tooltipEl.textContent).toContain("Mars");
|
||||
const mars = svgEl2.querySelector("[data-planet='Mars']");
|
||||
expect(mars.classList.contains("nw-planet--active")).toBe(true);
|
||||
});
|
||||
|
||||
// ── T9w ── NXT wraps clockwise from the last (lowest-degree) planet ───────
|
||||
|
||||
it("T9w: cycling NXT from Venus (lowest degree) wraps clockwise to Mars (highest degree)", () => {
|
||||
// Venus is idx 2 (lowest degree = furthest clockwise); NXT wraps to idx 0 = Mars
|
||||
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 }));
|
||||
|
||||
expect(tooltipEl.textContent).toContain("Mars");
|
||||
const mars = svgEl2.querySelector("[data-planet='Mars']");
|
||||
expect(mars.classList.contains("nw-planet--active")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user