demo'd old inventory area in room.html to make way for new content (hex table now centered in view); old test suite now targets Role card in #id_tray cells where appropriate, or skips Sig card select until aforementioned new feature deployed; new scripts & jasmine tests too; removed one irrelevant test case from apps.epic.tests.ITs.test_views.SelectRoleViewTest
This commit is contained in:
@@ -419,17 +419,6 @@ $seat-r-y: round($seat-r * 0.5); // 65px
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-inventory {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(var(--terUser), 0.3) transparent;
|
||||
}
|
||||
|
||||
.table-seat {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@@ -617,56 +606,6 @@ $card-h: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Inventory role card hand ───────────────────────────────────────────────
|
||||
//
|
||||
// Cards are stacked vertically: only a $strip-height peek of each card below
|
||||
// the first is visible by default, showing the role name at the top of the
|
||||
// card face. Hovering any card slides it right to pop it clear of the stack.
|
||||
|
||||
$inv-card-w: 100px;
|
||||
$inv-card-h: 150px;
|
||||
$inv-strip: 30px; // visible height of each stacked card after the first
|
||||
|
||||
#id_inv_role_card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card {
|
||||
width: $inv-card-w;
|
||||
height: $inv-card-h;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
// Every card after the first overlaps the one above it
|
||||
& + .card {
|
||||
margin-top: -($inv-card-h - $inv-strip);
|
||||
}
|
||||
|
||||
// Role name pinned to the top of the face so it reads in the strip
|
||||
.card-front {
|
||||
justify-content: flex-start;
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
|
||||
// Pop the hovered card to the right, above siblings
|
||||
&:hover {
|
||||
transform: translateX(1.5rem);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Partner indicator ─────────────────────────────────────────────────────
|
||||
|
||||
.partner-indicator {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Landscape mobile — aggressively scale down to fit short viewport
|
||||
@media (orientation: landscape) and (max-width: 1440px) {
|
||||
.gate-modal {
|
||||
|
||||
@@ -117,6 +117,27 @@ $handle-r: 1rem;
|
||||
&::before { border-color: rgba(var(--quaUser), 1); }
|
||||
}
|
||||
|
||||
// ─── Role card: arc-in animation (portrait) ─────────────────────────────────
|
||||
@keyframes tray-role-arc-in {
|
||||
from { opacity: 0; transform: scale(0.3) translate(-40%, -40%); }
|
||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
}
|
||||
|
||||
.tray-role-card {
|
||||
background: rgba(var(--quaUser), 0.25);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 0.2em;
|
||||
font-size: 0.65rem;
|
||||
color: rgba(var(--quaUser), 1);
|
||||
font-weight: 600;
|
||||
|
||||
&.arc-in {
|
||||
animation: tray-role-arc-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tray-wobble {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-8px); }
|
||||
@@ -276,6 +297,16 @@ $handle-r: 1rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// Role card arc-in for landscape
|
||||
@keyframes tray-role-arc-in-landscape {
|
||||
from { opacity: 0; transform: scale(0.3) translate(-40%, 40%); }
|
||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
}
|
||||
|
||||
.tray-role-card.arc-in {
|
||||
animation: tray-role-arc-in-landscape 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes tray-wobble-landscape {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
20% { transform: translateY(-8px); }
|
||||
|
||||
@@ -7,7 +7,6 @@ describe("RoleSelect", () => {
|
||||
<div class="room-page"
|
||||
data-select-role-url="/epic/room/test-uuid/select-role">
|
||||
</div>
|
||||
<div id="id_inv_role_card"></div>
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
@@ -102,12 +101,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card appends a .card to #id_inv_role_card", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("clicking a card POSTs to the select_role URL", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
@@ -117,11 +110,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("clicking a card results in exactly one card in inventory", () => {
|
||||
const card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
expect(document.querySelectorAll("#id_inv_role_card .card").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -135,11 +123,6 @@ describe("RoleSelect", () => {
|
||||
expect(document.getElementById("id_role_select")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector(".role-select-backdrop").click();
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -259,20 +242,13 @@ describe("RoleSelect", () => {
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("tray card after successful role selection", () => {
|
||||
let grid, guardConfirm;
|
||||
let guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Minimal tray grid matching room.html structure
|
||||
grid = document.createElement("div");
|
||||
grid.id = "id_tray_grid";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "tray-cell";
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
spyOn(Tray, "open");
|
||||
// Spy on Tray.placeCard: call the onComplete callback immediately.
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
if (cb) cb();
|
||||
});
|
||||
|
||||
// Capturing guard spy — holds onConfirm so we can fire it per-test
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
@@ -283,54 +259,128 @@ describe("RoleSelect", () => {
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
});
|
||||
|
||||
it("prepends a .tray-role-card to #id_tray_grid on success", async () => {
|
||||
it("calls Tray.placeCard() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).not.toBeNull();
|
||||
expect(Tray.placeCard).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tray-role-card is the first child of #id_tray_grid", async () => {
|
||||
it("passes the role code string to Tray.placeCard", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.firstElementChild.classList.contains("tray-role-card")).toBe(true);
|
||||
const roleCode = Tray.placeCard.calls.mostRecent().args[0];
|
||||
expect(typeof roleCode).toBe("string");
|
||||
expect(roleCode.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("tray-role-card carries the selected role as data-role", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
const trayCard = grid.querySelector(".tray-role-card");
|
||||
expect(trayCard.dataset.role).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls Tray.open() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not prepend a tray-role-card on server rejection", async () => {
|
||||
it("does not call Tray.placeCard() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.querySelector(".tray-role-card")).toBeNull();
|
||||
expect(Tray.placeCard).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Tray.open() on server rejection", async () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: false })
|
||||
// ------------------------------------------------------------------ //
|
||||
// WS turn_changed pause during animation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("WS turn_changed pause during placeCard animation", () => {
|
||||
let stack, guardConfirm;
|
||||
|
||||
beforeEach(() => {
|
||||
// Six table seats, slot 1 starts active
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat" + (i === 1 ? " active" : "");
|
||||
seat.dataset.slot = String(i);
|
||||
testDiv.appendChild(seat);
|
||||
}
|
||||
stack = document.createElement("div");
|
||||
stack.className = "card-stack";
|
||||
stack.dataset.state = "ineligible";
|
||||
stack.dataset.userSlots = "1";
|
||||
stack.dataset.starterRoles = "";
|
||||
testDiv.appendChild(stack);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.id = "id_tray_grid";
|
||||
testDiv.appendChild(grid);
|
||||
|
||||
// placeCard spy that holds the onComplete callback
|
||||
let heldCallback = null;
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
heldCallback = cb; // don't call immediately — simulate animation in-flight
|
||||
});
|
||||
spyOn(Tray, "forceClose");
|
||||
|
||||
// Expose heldCallback so tests can fire it
|
||||
Tray._testFirePlaceCardComplete = () => {
|
||||
if (heldCallback) heldCallback();
|
||||
};
|
||||
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
(anchor, message, onConfirm) => { guardConfirm = onConfirm; }
|
||||
);
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(Tray.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("grid grows by exactly 1 on success", async () => {
|
||||
const before = grid.children.length;
|
||||
afterEach(() => {
|
||||
delete Tray._testFirePlaceCardComplete;
|
||||
RoleSelect._testReset();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not call Tray.forceClose immediately", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve(); // fetch resolves; placeCard called; animation pending
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("turn_changed during animation does not immediately move the active seat", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(grid.children.length).toBe(before + 1);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("1"); // still slot 1
|
||||
});
|
||||
|
||||
it("deferred turn_changed is processed when animation completes", async () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
|
||||
// Fire onComplete — deferred turn_changed should now run
|
||||
Tray._testFirePlaceCardComplete();
|
||||
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
|
||||
it("turn_changed after animation completes is processed immediately", () => {
|
||||
// No animation in flight — turn_changed should run right away
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).toHaveBeenCalled();
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,9 +483,6 @@ describe("RoleSelect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("appends a .card to #id_inv_role_card", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissing the guard (NVM or outside click)", () => {
|
||||
@@ -463,10 +510,6 @@ describe("RoleSelect", () => {
|
||||
expect(window.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores normal mouseleave behaviour on the card", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter"));
|
||||
card.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
|
||||
@@ -422,4 +422,97 @@ describe("Tray", () => {
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// placeCard() //
|
||||
// ---------------------------------------------------------------------- //
|
||||
//
|
||||
// placeCard(roleCode, onComplete):
|
||||
// 1. Marks the first .tray-cell with .tray-role-card + data-role.
|
||||
// 2. Opens the tray.
|
||||
// 3. Arc-in animates the cell (.arc-in class, animationend fires).
|
||||
// 4. forceClose() — tray closes instantly.
|
||||
// 5. Calls onComplete.
|
||||
//
|
||||
// The grid always has exactly 8 .tray-cell elements (from the template);
|
||||
// no new elements are inserted.
|
||||
//
|
||||
// ---------------------------------------------------------------------- //
|
||||
|
||||
describe("placeCard()", () => {
|
||||
let grid, firstCell;
|
||||
|
||||
beforeEach(() => {
|
||||
grid = document.createElement("div");
|
||||
grid.id = "id_tray_grid";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "tray-cell";
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
document.body.appendChild(grid);
|
||||
// Re-init so _grid is set (reset() in outer afterEach clears it)
|
||||
Tray.init();
|
||||
firstCell = grid.querySelector(".tray-cell");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
grid.remove();
|
||||
});
|
||||
|
||||
it("adds .tray-role-card to the first .tray-cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
});
|
||||
|
||||
it("sets data-role on the first cell", () => {
|
||||
Tray.placeCard("NC", null);
|
||||
expect(firstCell.dataset.role).toBe("NC");
|
||||
});
|
||||
|
||||
it("grid cell count stays at 8", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(grid.children.length).toBe(8);
|
||||
});
|
||||
|
||||
it("opens the tray", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("adds .arc-in to the first cell", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(firstCell.classList.contains("arc-in")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes .arc-in and force-closes after animationend", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
expect(Tray.isOpen()).toBe(true);
|
||||
firstCell.dispatchEvent(new Event("animationend"));
|
||||
expect(firstCell.classList.contains("arc-in")).toBe(false);
|
||||
expect(Tray.isOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("calls onComplete after the tray closes", () => {
|
||||
let called = false;
|
||||
Tray.placeCard("PC", () => { called = true; });
|
||||
firstCell.dispatchEvent(new Event("animationend"));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("landscape: same behaviour — first cell gets role card", () => {
|
||||
Tray._testSetLandscape(true);
|
||||
Tray.init();
|
||||
Tray.placeCard("EC", null);
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(true);
|
||||
expect(firstCell.dataset.role).toBe("EC");
|
||||
});
|
||||
|
||||
it("reset() removes .tray-role-card and data-role from cells", () => {
|
||||
Tray.placeCard("PC", null);
|
||||
Tray.reset();
|
||||
expect(firstCell.classList.contains("tray-role-card")).toBe(false);
|
||||
expect(firstCell.dataset.role).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user