tray sig-card tooltip: portal w. PRV|NXT pager — TDD
Phase 2 of the apps.tooltips integration on the tray. Hovering
.tray-sig-card > .sig-stage-card opens #id_tooltip_portal w. an FYI panel
that mirrors #id_fan_fyi_panel (Energy / Operation entries cycled via
PRV|NXT), but w.o. the stage block, w.o. Reversal entries, & w.o. the fan
stage's click-to-dismiss handler — the panel-body click is reserved for
future drag-and-drop on .tray-sig-card:active.
- _partials/_sig_fyi_panel.html — new partial, the .sig-info + PRV|NXT
block extracted out of game_kit.html, _sig_select_overlay.html, &
_sea_overlay.html. {% include %}d back from those 3 callers; pure
copy-paste extraction (no behavioural change to fan stage, sig select,
or sea select).
- room.html: .tray-sig-card > .sig-stage-card gains data-energies +
data-operations (the only attrs StageCard.buildInfoData reads), keyed
off my_tray_sig.energies_json / .operations_json (existing TarotCard
properties).
- tray-tooltip.js: new sig branch — _showSig() builds the panel inline,
paints via StageCard.renderFyi, & wires PRV|NXT cycle handlers; the
mousemove union now covers the .fyi-prev / .fyi-next btn rects (the
btns hang past the portal's left & right edges) so mouse-over them
keeps the panel alive. Click stopPropagation on the btns prevents the
panel-body click from reaching anything else.
- TrayTooltipSpec: 6 new sig-branch specs (panel structure; first energy
entry rendered; PRV|NXT cycling; body click no-dismiss; pointer over
btn rects keeps panel alive; pointer outside full union clears).
- test_component_tray_tooltip.py: 4 sig FTs (hover populates portal w.
Energy/TESTLIBIDO/effect/1-of-2; PRV|NXT cycle; body click does NOT
dismiss; mouseleave clears).
FT helper note — the sig FT's _hover dispatches a synthetic mouseenter
via JS rather than ActionChains.move_to_element, because the role-card
& sig-card cells sit side-by-side in the tray grid: the pointer's
animated path crosses the role-card on its way to the sig-card &
opens the role tooltip mid-flight, which then occludes the sig stage
by the time the move lands. Direct dispatch lands the event on the
intended trigger w.o. the cross-cell drag-by.
313 epic ITs + 335 Jasmine specs (incl. 6 new) + 6 tray-tooltip FTs all
green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,139 @@ describe("TrayTooltip", () => {
|
||||
// Clamping — portal left stays inside the viewport //
|
||||
// ---------------------------------------------------------------------- //
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Phase 2 — sig-card branch //
|
||||
// ---------------------------------------------------------------------- //
|
||||
//
|
||||
// Hovering .tray-sig-card > .sig-stage-card populates the SAME portal with
|
||||
// a .sig-info FYI panel (Energy/Operation entries cycled via PRV/NXT btns).
|
||||
// No click-to-dismiss; mouseleave union covers the overhanging btn rects.
|
||||
|
||||
describe("sig-card branch", () => {
|
||||
let sigCell, sigStage;
|
||||
|
||||
beforeEach((done) => {
|
||||
sigCell = document.createElement("div");
|
||||
sigCell.className = "tray-cell tray-sig-card";
|
||||
|
||||
sigStage = document.createElement("div");
|
||||
sigStage.className = "sig-stage-card sea-sig-card";
|
||||
// Two FYI entries — 1 energy + 1 operation — so PRV/NXT have
|
||||
// something to cycle through.
|
||||
sigStage.dataset.energies = JSON.stringify([
|
||||
{ type: "TESTLIBIDO", effect: "First energy effect." },
|
||||
]);
|
||||
sigStage.dataset.operations = JSON.stringify([
|
||||
{ type: "TESTOP", effect: "First operation effect." },
|
||||
]);
|
||||
|
||||
sigCell.appendChild(sigStage);
|
||||
grid.appendChild(sigCell);
|
||||
|
||||
spyOn(sigStage, "getBoundingClientRect").and.returnValue({
|
||||
left: 100, right: 160, top: 300, bottom: 396,
|
||||
width: 60, height: 96,
|
||||
});
|
||||
// MutationObserver fires asynchronously — yield once so the new
|
||||
// sig cell is bound before the spec body runs.
|
||||
setTimeout(done, 0);
|
||||
});
|
||||
|
||||
it("populates the portal with a .sig-info panel + PRV/NXT btns", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(portal.querySelector(".sig-info-title")).not.toBe(null);
|
||||
expect(portal.querySelector(".sig-info-type")).not.toBe(null);
|
||||
expect(portal.querySelector(".sig-info-effect")).not.toBe(null);
|
||||
expect(portal.querySelector(".sig-info-index")).not.toBe(null);
|
||||
expect(portal.querySelector(".fyi-prev")).not.toBe(null);
|
||||
expect(portal.querySelector(".fyi-next")).not.toBe(null);
|
||||
});
|
||||
|
||||
it("renders the first energy entry on hover", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(portal.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||
expect(portal.querySelector(".sig-info-type").textContent).toBe("TESTLIBIDO");
|
||||
expect(portal.querySelector(".sig-info-effect").textContent).toBe("First energy effect.");
|
||||
expect(portal.querySelector(".sig-info-index").textContent).toBe("1 / 2");
|
||||
});
|
||||
|
||||
it("NXT advances to the operation entry; PRV cycles back", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
portal.querySelector(".fyi-next").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true })
|
||||
);
|
||||
expect(portal.querySelector(".sig-info-title").textContent).toBe("Operation");
|
||||
expect(portal.querySelector(".sig-info-type").textContent).toBe("TESTOP");
|
||||
expect(portal.querySelector(".sig-info-index").textContent).toBe("2 / 2");
|
||||
|
||||
portal.querySelector(".fyi-prev").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true })
|
||||
);
|
||||
expect(portal.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||
expect(portal.querySelector(".sig-info-index").textContent).toBe("1 / 2");
|
||||
});
|
||||
|
||||
it("clicking the panel body does NOT dismiss (departure from fan stage)", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
portal.querySelector(".sig-info-effect").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true })
|
||||
);
|
||||
expect(portal.classList.contains("active")).toBe(true);
|
||||
expect(portal.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("keeps portal active when pointer is over a fyi-prev / fyi-next btn rect", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
// Stub the portal + btn rects so the union test is deterministic.
|
||||
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||
left: 80, right: 280, top: 200, bottom: 320,
|
||||
width: 200, height: 120,
|
||||
});
|
||||
const prv = portal.querySelector(".fyi-prev");
|
||||
const nxt = portal.querySelector(".fyi-next");
|
||||
spyOn(prv, "getBoundingClientRect").and.returnValue({
|
||||
left: 60, right: 92, top: 240, bottom: 272, width: 32, height: 32,
|
||||
});
|
||||
spyOn(nxt, "getBoundingClientRect").and.returnValue({
|
||||
left: 268, right: 300, top: 240, bottom: 272, width: 32, height: 32,
|
||||
});
|
||||
// Pointer over the PRV btn (left:60–92) — outside the portal's left
|
||||
// edge (80) but inside the btn rect — should keep the portal alive.
|
||||
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||
bubbles: true, clientX: 70, clientY: 256,
|
||||
}));
|
||||
expect(portal.classList.contains("active")).toBe(true);
|
||||
|
||||
// Pointer over the NXT btn (right:268–300, past the portal right
|
||||
// edge 280) — likewise stays alive.
|
||||
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||
bubbles: true, clientX: 290, clientY: 256,
|
||||
}));
|
||||
expect(portal.classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears portal when pointer leaves trigger + portal + btn rect union", () => {
|
||||
sigStage.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
spyOn(portal, "getBoundingClientRect").and.returnValue({
|
||||
left: 80, right: 280, top: 200, bottom: 320,
|
||||
width: 200, height: 120,
|
||||
});
|
||||
const prv = portal.querySelector(".fyi-prev");
|
||||
const nxt = portal.querySelector(".fyi-next");
|
||||
spyOn(prv, "getBoundingClientRect").and.returnValue({
|
||||
left: 60, right: 92, top: 240, bottom: 272, width: 32, height: 32,
|
||||
});
|
||||
spyOn(nxt, "getBoundingClientRect").and.returnValue({
|
||||
left: 268, right: 300, top: 240, bottom: 272, width: 32, height: 32,
|
||||
});
|
||||
document.dispatchEvent(new MouseEvent("mousemove", {
|
||||
bubbles: true, clientX: 9999, clientY: 9999,
|
||||
}));
|
||||
expect(portal.classList.contains("active")).toBe(false);
|
||||
expect(portal.style.display).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("position clamping", () => {
|
||||
it("clamps to the right when the trigger is near the left edge", () => {
|
||||
// Trigger near x=0 should push portal centre rightward to halfW + 8.
|
||||
|
||||
Reference in New Issue
Block a user