describe("SeaDeal", () => {
let testDiv, overlay, stage, stageCard, statBlock;
const CARD = {
id: 99, name: "Queen of Crowns",
arcana: "MIDDLE", suit: "CROWNS", number: 13,
corner_rank: "Q", suit_icon: "fa-crown",
name_group: "", name_title: "Queen of Crowns",
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
reversal_qualifier: "Vacant",
keywords_upright: ["nurturing", "practical", "abundance"],
keywords_reversed: ["financial dependence", "smothering"],
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
operations: [{ type: "COVER", effect: "Operation entry." }],
};
function makeFixture({ polarity = "levity" } = {}) {
testDiv = document.createElement("div");
testDiv.innerHTML = `
`;
document.body.appendChild(testDiv);
overlay = testDiv.querySelector("#id_sea_overlay");
stage = testDiv.querySelector("#id_sea_stage");
stageCard = testDiv.querySelector(".sea-stage-card");
statBlock = testDiv.querySelector(".sea-stat-block");
SeaDeal._testInit();
}
afterEach(() => {
if (testDiv) testDiv.remove();
// Purge any stale overlays left by tests that called makeFixture() twice
document.querySelectorAll('#id_sea_overlay').forEach(el => el.remove());
});
// ── openStage ────────────────────────────────────────────────────────── //
describe("openStage()", () => {
beforeEach(() => makeFixture());
it("makes #id_sea_stage visible", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
expect(stage.style.display).not.toBe("none");
});
it("populates corner rank on stage card", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
expect(rank.textContent).toBe("Q");
});
it("shows suit icon on stage card", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
const icon = stageCard.querySelector(".fan-card-corner--tl .stage-suit-icon");
expect(icon.style.display).not.toBe("none");
expect(icon.classList.contains("fa-crown")).toBe(true);
});
it("populates upright card name for non-major", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
expect(stageCard.querySelector(".fan-card-name").textContent).toBe("Queen of Crowns");
});
it("puts levity qualifier in qualifier-above for non-major", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Elevated");
});
it("puts gravity qualifier in qualifier-above for non-major gravity", () => {
// Re-init with gravity polarity (no double-append; beforeEach already ran)
overlay.dataset.seaUserPolarity = "gravity";
SeaDeal._testInit();
stageCard = testDiv.querySelector(".sea-stage-card");
SeaDeal.openStage(CARD, ".sea-pos-cover", false);
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("puts reversal word in reversal-qualifier slot", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
});
it("populates upright keywords in stat block", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
const items = testDiv.querySelectorAll("#id_sea_stat_upright li");
expect(items.length).toBe(3);
expect(items[0].textContent).toBe("nurturing");
});
it("populates reversed keywords in stat block", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
const items = testDiv.querySelectorAll("#id_sea_stat_reversed li");
expect(items.length).toBe(2);
});
it("fills the target position slot", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
});
it("resets SPIN to upright when opening a new card", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
stageCard.classList.add("stage-card--reversed");
statBlock.classList.add("is-reversed");
SeaDeal.openStage(CARD, ".sea-pos-cross", true);
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── SPIN in sea stage ─────────────────────────────────────────────────── //
describe("SPIN btn in sea stage", () => {
beforeEach(() => {
makeFixture();
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
});
it("toggles is-reversed on stat block", () => {
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("toggles stage-card--reversed on stage card", () => {
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
});
it("second SPIN click restores upright", () => {
const btn = testDiv.querySelector(".spin-btn");
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── FYI in sea stage ──────────────────────────────────────────────────── //
describe("FYI btn in sea stage", () => {
beforeEach(() => {
makeFixture();
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
});
it("FYI click shows the info panel", () => {
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
});
it("shows first energy entry title as 'Energy'", () => {
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
});
it("shows first entry type", () => {
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
});
it("NXT advances to operation entry", () => {
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
testDiv.querySelector(".fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
});
});
// ── Backdrop dismiss ──────────────────────────────────────────────────── //
describe("stage backdrop click", () => {
beforeEach(() => {
makeFixture();
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
});
it("hides the sea stage", () => {
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stage.style.display).toBe("none");
});
it("leaves the slot filled after dismiss", () => {
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
});
});
// ── Re-open from deposited slot (two-step tap) ────────────────────────── //
describe("clicking a deposited slot", () => {
let slot;
beforeEach(() => {
makeFixture();
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
// Dismiss first — adds --visible to slot
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
});
it("first click focuses the slot (does not yet open stage)", () => {
slot.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(slot.classList.contains("sea-card-slot--focused")).toBeTrue();
expect(stage.style.display).toBe("none");
});
it("re-opens the sea stage on second click", () => {
slot.dispatchEvent(new MouseEvent("click", { bubbles: true })); // focus
slot.dispatchEvent(new MouseEvent("click", { bubbles: true })); // open
expect(stage.style.display).not.toBe("none");
});
it("re-populates stage with the same card rank on second click", () => {
slot.dispatchEvent(new MouseEvent("click", { bubbles: true })); // focus
slot.dispatchEvent(new MouseEvent("click", { bubbles: true })); // open
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
expect(rank.textContent).toBe("Q");
});
});
// ── Reversed-card auto-rotate on modal open ─────────────────────────────── //
//
// Spec 2026-05-26: a reversed card opens rightside-up (so the user sees
// the upright card frame + a dimmed upright title + a highlighted
// upside-down reversal title) and then SLOWLY auto-rotates 180° to land
// upside-down. Duration is 2× the normal SPIN transition (0.4s → 0.8s).
// The stat-block gets `.is-reversed` immediately (REVERSAL label
// highlights as the modal opens); the card itself gets `.stage-card--
// reversed` only after the auto-rotate lands. `[data-spinning]` is set
// for the rotation window so the FLIP btn hides during the spin (the
// SCSS-side companion rule also repositions FLIP to visual bottom-left
// post-rotation, matching the game-kit fan precedent).
describe("reversed-card open", () => {
const REVERSED_CARD = Object.assign({}, CARD, { reversed: true });
beforeEach(() => {
jasmine.clock().install();
makeFixture();
});
afterEach(() => jasmine.clock().uninstall());
it("sets is-reversed on stat-block immediately", () => {
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("does NOT set stage-card--reversed immediately (card starts rightside-up)", () => {
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
});
it("does NOT set data-spinning before the flip-in completes", () => {
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
expect(stageCard.dataset.spinning).toBeUndefined();
});
it("kicks off the auto-rotate after the flip-in window — data-spinning set + stage-card--reversed added", () => {
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
jasmine.clock().tick(500); // past sea-flip-in (0.35s) + buffer
expect(stageCard.dataset.spinning).toBe("1");
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
});
it("clears data-spinning once the auto-rotate window has elapsed", () => {
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
jasmine.clock().tick(500 + 900); // past flip-in + 0.8s rotate
expect(stageCard.dataset.spinning).toBeUndefined();
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
});
it("does NOT auto-rotate an upright card", () => {
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
jasmine.clock().tick(2000);
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
expect(stageCard.dataset.spinning).toBeUndefined();
});
});
// ── SPIN click hides FLIP during rotation ───────────────────────────────── //
describe("SPIN click hides FLIP via [data-spinning]", () => {
beforeEach(() => {
jasmine.clock().install();
makeFixture();
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
});
afterEach(() => jasmine.clock().uninstall());
it("sets data-spinning on SPIN click so the FLIP btn hides during the rotation", () => {
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.dataset.spinning).toBe("1");
});
it("clears data-spinning after the SPIN transition window elapses", () => {
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
jasmine.clock().tick(500); // past 0.4s SPIN transition
expect(stageCard.dataset.spinning).toBeUndefined();
});
});
});