Files
python-tdd/src/static/tests/SkyWheelSpec.js
Disco DeDisco 9ed877168e
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
SEED MAP shared wheel rim: the table's OWN sky (planets-only, canonical signs) rings the tessellation — one frame for all six gamers — TDD
The Voronoi felt gains a stripped sky-wheel rim drawn from the ROOM's own sky
(Room.sky_chart) — identical for every gamer, updating toward the shared map —
with the tessellation sized into the wheel's freed hub. Roadmap step 21, Step
2's coordinate frame.

- Room.convened_at + Room.sky_chart (migration 0019); pick_roles stamps
  convened_at at gate-close (stamp only — no HTTP on the transition)
- epic.table_sky: lazy planets-only chart via PySwiss at the null location
  (geocentric longitudes need only the convened TIME; houses/ASC/MC need a
  birth LOCATION a virtual table lacks → omitted), cached on Room.sky_chart;
  legacy rooms key off created_at; seated-gamer gated, 502 on PySwiss down
- SkyWheel.drawRim(svg, data): pure static renderer — canonical asc=0 frame,
  signs ring + planet glyphs only, NO element ring / centre disc / houses /
  axes / aspects / tooltips; never writes the interactive wheel's singleton
  state; returns {size, cx, cy, r, hubR} so the felt sizes the map into the hub
- _seed_map_overlay.html: rim draws on open; map svg shrinks to 2×hubR +
  .voronoi-map--rimmed clip; lazy table-sky fetch on open; preload-then-repaint
  so a cold-cache open doesn't strand the zodiac glyphs; ResizeObserver on the
  col (not the self-sized map svg)
- _sky.scss: stacked centred svgs in .seed-map-col; .seed-wheel pointer-events
  none; circle clip on the rimmed map
- room_sky_json ctx in _role_select_context; rootvars: --sixUser/--octUser
  nudged within the Trs ramp (parallel palette tune)
- drawRim Jasmine suite (R1–R6: signs+planets, strip, hub geometry, static
  placement, singleton untouched, signs-only fallback) in both spec copies;
  epic TableSkyViewTest + convened_at stamp + seed-overlay rim ITs; FT rim
  assertions (12 signs, 3 planets, no stripped/located layers, hub sizing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:33:23 -04:00

1149 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── SkyWheelSpec.js ─────────────────────────────────────────────────────────
//
// Unit specs for sky-wheel.js — planet/element click-to-lock tooltips.
//
// DOM contract assumed:
// <svg id="id_sky_svg"> — target for SkyWheel.draw()
// <div id="id_sky_tooltip"> — tooltip portal (position:fixed on page)
//
// Click-lock contract:
// click on [data-planet] group → adds .nw-planet--active class
// raises group to DOM front
// shows #id_sky_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 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 },
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",
};
describe("SkyWheel — planet click 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(() => {
svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.setAttribute("id", "id_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, SYNTHETIC_CHART);
});
afterEach(() => {
SkyWheel.clear();
svgEl.remove();
tooltipEl.remove();
});
// ── T3 ── click planet shows name / sign / in-sign degree + glow ──────────
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("click", { bubbles: true }));
expect(sun.classList.contains("nw-planet--active")).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: 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("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
expect(tooltipEl.textContent).toContain("℞");
});
// ── T5 ── clicking same planet again hides tooltip and removes active ──────
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("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
sun.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("none");
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("SkyWheel — tick lines, raise, and cycle navigation", () => {
let svgEl2, tooltipEl;
beforeEach(() => {
svgEl2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl2.setAttribute("id", "id_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
tooltipEl.style.position = "fixed";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl2, CONJUNCTION_CHART);
});
afterEach(() => {
SkyWheel.clear();
svgEl2.remove();
tooltipEl.remove();
});
// ── 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");
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);
});
// ── T8 ── click raises planet to front ────────────────────────────────────
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("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");
});
// ── 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("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("click", { bubbles: true }));
expect(tooltipEl.style.display).toBe("block");
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);
});
// ── Aspect mini-portal id-separation (regression) ───────────────────────
// The Aspected/Unaspected micro-portal must live in its OWN element
// (#id_aspect_mini_portal), NOT the wallet/kit's #id_mini_tooltip_portal —
// on the home page they shared that id and the wallet's inline display:none
// stuck, hiding the aspect tooltip. Activating a planet must touch ONLY the
// aspect element and leave the wallet element untouched.
it("aspect micro-portal uses its own #id_aspect_mini_portal, not the wallet's id", () => {
const wallet = document.createElement("div");
wallet.id = "id_mini_tooltip_portal";
wallet.style.display = "none"; // the wallet's leftover inline state
wallet.textContent = "In-Use: Foo"; // the wallet's own content
document.body.appendChild(wallet);
const aspect = document.createElement("div");
aspect.id = "id_aspect_mini_portal";
aspect.className = "token-tooltip token-tooltip--mini";
document.body.appendChild(aspect);
// Re-draw so _injectTooltipControls re-binds to the now-present aspect
// portal, then activate a planet.
SkyWheel.draw(svgEl2, CONJUNCTION_CHART);
svgEl2.querySelector("[data-planet='Sun']")
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// The aspect element got the swap text + .active …
expect(aspect.classList.contains("active")).toBe(true);
expect(["Aspected", "Unaspected"]).toContain(aspect.textContent);
// … and the wallet's element is untouched (no battle).
expect(wallet.textContent).toBe("In-Use: Foo");
expect(wallet.classList.contains("active")).toBe(false);
wallet.remove();
aspect.remove();
});
// ── T9n ── PRV cycles counterclockwise (to higher ecliptic degree) ────────
// 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.
it("T9n: clicking PRV from Sun shows MC (next higher ecliptic degree in merged cycle)", () => {
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("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);
});
// ── T9w ── NXT wraps clockwise from the lowest-degree item ───────────────
// Venus(63.3°) is lowest; NXT wraps to ASC(180°) — the highest-degree item.
it("T9w: cycling NXT from Venus (lowest degree) wraps clockwise to ASC (highest degree)", () => {
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("Ascendant");
const asc = svgEl2.querySelector("[data-angle='ASC']");
expect(asc.classList.contains("nw-angle--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
// ─────────────────────────────────────────────────────────────────────────────
// ── 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("SkyWheel — 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_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, ASPECT_CHART);
});
afterEach(() => {
SkyWheel.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);
});
// 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);
});
});
xdescribe("SkyWheel — 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_sky_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_sky_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 }
);
SkyWheel.draw(svgEl3, HALF_CHART);
});
afterEach(() => {
SkyWheel.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);
});
});
// ── 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("SkyWheel — 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_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, ENRICHED_CHART);
});
afterEach(() => {
SkyWheel.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("SkyWheel — 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_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, SIGN_CHART);
});
afterEach(() => {
SkyWheel.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("SkyWheel — 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_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, HOUSE_CHART);
});
afterEach(() => {
SkyWheel.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");
});
});
// ── 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("SkyWheel — 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_sky_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_sky_tooltip";
tooltipEl.className = "tt";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(svgEl, ANGLE_CHART);
});
afterEach(() => {
SkyWheel.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");
});
// 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);
});
});
// ── SkyWheel.drawRim — the SEED MAP's shared wheel rim ──────────────────────
//
// The STRIPPED wheel ringing the Voronoi tessellation (roadmap step 21,
// Step 2): the ROOM's own sky — planets only, since a virtual table has a
// convened TIME but no birth LOCATION (so houses/ASC/MC never exist on the
// shared rim) — around the canonical signs ring (asc=0 frame, identical for
// all six gamers). drawRim is a PURE renderer: no tooltips, no cycle, no
// transitions, and CRUCIALLY no singleton writes — the interactive saved
// wheel on the same page (_svg/_currentData/#id_sky_tooltip) must survive a
// rim draw untouched.
describe("SkyWheel — drawRim (SEED MAP shared rim)", () => {
const ROOM_SKY = {
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 },
},
aspects: [],
};
let rimSvg, skySvg, tooltipEl;
beforeEach(() => {
rimSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
rimSvg.setAttribute("id", "id_seed_wheel_svg");
rimSvg.setAttribute("width", "400");
rimSvg.setAttribute("height", "400");
rimSvg.style.width = "400px";
rimSvg.style.height = "400px";
document.body.appendChild(rimSvg);
});
afterEach(() => {
rimSvg.remove();
if (skySvg) { SkyWheel.clear(); skySvg.remove(); skySvg = null; }
if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; }
});
it("R1: renders the canonical sign ring + the room sky's planet glyphs", () => {
SkyWheel.drawRim(rimSvg, ROOM_SKY);
expect(rimSvg.querySelectorAll(".nw-sign-group").length).toBe(12);
expect(rimSvg.querySelectorAll(".nw-planet-group").length).toBe(3);
expect(rimSvg.querySelector(".nw-outer-ring")).not.toBeNull();
// Mercury's retrograde badge carries over to the rim.
expect(rimSvg.querySelector("[data-planet='Mercury'] .nw-rx")).not.toBeNull();
});
it("R2: strips the element ring, centre disc, aspect web, houses and axes", () => {
SkyWheel.drawRim(rimSvg, ROOM_SKY);
["nw-elements", "nw-inner-disc", "nw-aspects", "nw-houses", "nw-axes"]
.forEach((cls) => {
expect(rimSvg.querySelectorAll("." + cls).length)
.toBe(0, `expected no .${cls} on the shared rim`);
});
});
it("R3: returns the hub geometry the tessellation slots into", () => {
const geo = SkyWheel.drawRim(rimSvg, ROOM_SKY);
expect(geo.size).toBe(400);
expect(geo.cx).toBe(200);
expect(geo.cy).toBe(200);
expect(geo.r).toBeCloseTo(400 * 0.46, 6);
// The freed hub: just inside the planet band (planetR 0.59 glyph 0.05).
expect(geo.hubR).toBeCloseTo(400 * 0.46 * 0.52, 6);
});
it("R4: places planets statically at their canonical (asc=0) angles — no entry transition", () => {
SkyWheel.drawRim(rimSvg, ROOM_SKY);
const sun = rimSvg.querySelector("[data-planet='Sun'] circle");
const a = (-(338.4) - 180) * Math.PI / 180; // _toAngle(338.4, asc=0)
const r = 400 * 0.46;
expect(parseFloat(sun.getAttribute("cx"))).toBeCloseTo(200 + r * 0.59 * Math.cos(a), 1);
expect(parseFloat(sun.getAttribute("cy"))).toBeCloseTo(200 + r * 0.59 * Math.sin(a), 1);
});
it("R5: leaves the interactive singleton untouched — the sky felt's wheel keeps its svg + tooltip", () => {
skySvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
skySvg.setAttribute("id", "id_sky_svg");
skySvg.style.width = "400px";
skySvg.style.height = "400px";
document.body.appendChild(skySvg);
tooltipEl = document.createElement("div");
tooltipEl.id = "id_sky_tooltip";
tooltipEl.style.display = "none";
document.body.appendChild(tooltipEl);
SkyWheel.draw(skySvg, CONJUNCTION_CHART);
SkyWheel.drawRim(rimSvg, ROOM_SKY);
// The tooltip controls draw() injected survive the rim draw.
expect(tooltipEl.querySelector(".nw-tt-prv")).not.toBeNull();
// The singleton still points at the INTERACTIVE svg: clear() empties
// it, not the rim.
SkyWheel.clear();
expect(skySvg.children.length).toBe(0);
expect(rimSvg.querySelectorAll(".nw-sign-group").length).toBe(12);
});
it("R6: renders the signs-only canonical frame when the room sky is absent", () => {
const geo = SkyWheel.drawRim(rimSvg, null);
expect(rimSvg.querySelectorAll(".nw-sign-group").length).toBe(12);
expect(rimSvg.querySelectorAll(".nw-planet-group").length).toBe(0);
expect(geo.hubR).toBeCloseTo(400 * 0.46 * 0.52, 6);
});
});