role-select UX: tray timing delays, seat/circle state polish, 394 ITs green
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- _animationPending set before fetch (not in .then()) — blocks WS turn advance during in-flight request
- _placeCardDelay (3s) + _postTrayDelay (3s) give gamer time to see each step; both zeroed by _testReset()
- .role-confirmed class: full-opacity chair after placeCard completes; server-rendered on reload
- Slot circles disappear in join order (slot 1 first) via count-based logic, not role-label matching
- data-active-slot on card-stack; handleTurnChanged writes it for selectRole() to read
- #id_tray_wrap not rendered during gate phase ({% if room.table_status %})
- Tray slide/arc-in slowed to 1s for diagnostics; wobble kept at 0.45s
- Obsolete test_roles_revealed_simultaneously FT removed; T8 tray FT uses ROLE_SELECT room
- Jasmine macrotask flush pattern: await new Promise(r => setTimeout(r, 0)) after fetch .then()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -239,68 +239,6 @@ html:has(.gate-backdrop) {
|
||||
}
|
||||
}
|
||||
|
||||
.gate-slots {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $gate-gap;
|
||||
|
||||
.gate-slot {
|
||||
position: relative;
|
||||
width: $gate-node;
|
||||
height: $gate-node;
|
||||
border-radius: 50%;
|
||||
border: $gate-line solid rgba(var(--terUser), 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.filled,
|
||||
&.reserved {
|
||||
background: rgba(var(--terUser), 0.2);
|
||||
}
|
||||
|
||||
&.filled:hover,
|
||||
&.reserved:hover {
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
|
||||
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1),
|
||||
;
|
||||
}
|
||||
|
||||
.slot-number {
|
||||
font-size: 0.7em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.slot-gamer { display: none; }
|
||||
|
||||
form {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// CARTE drop-target circle — matches .reserved appearance
|
||||
&:has(.drop-token-btn) {
|
||||
background: rgba(var(--terUser), 0.2);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
|
||||
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1),
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -317,24 +255,6 @@ html:has(.gate-backdrop) {
|
||||
}
|
||||
|
||||
.token-slot { min-width: 150px; }
|
||||
|
||||
.gate-slots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 52px);
|
||||
grid-template-rows: repeat(2, 52px);
|
||||
gap: 24px;
|
||||
|
||||
.gate-slot {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
&:nth-child(1) { grid-column: 1; grid-row: 1; }
|
||||
&:nth-child(2) { grid-column: 2; grid-row: 1; }
|
||||
&:nth-child(3) { grid-column: 3; grid-row: 1; }
|
||||
&:nth-child(4) { grid-column: 1; grid-row: 2; }
|
||||
&:nth-child(5) { grid-column: 2; grid-row: 2; }
|
||||
&:nth-child(6) { grid-column: 3; grid-row: 2; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,59 +286,104 @@ $seat-r: 130px;
|
||||
$seat-r-x: round($seat-r * 0.866); // 113px
|
||||
$seat-r-y: round($seat-r * 0.5); // 65px
|
||||
|
||||
// .table-position anchors at edge midpoints (pointy-top hex).
|
||||
// Seat edge-midpoint geometry (pointy-top hex).
|
||||
// Apothem ≈ 80px + 30px clearance = 110px total push from centre.
|
||||
$pos-d: 110px;
|
||||
$pos-d-x: round($pos-d * 0.5); // 55px
|
||||
$pos-d-y: round($pos-d * 0.866); // 95px
|
||||
|
||||
.table-position {
|
||||
// ─── Position strip ────────────────────────────────────────────────────────
|
||||
// Numbered gate-slot circles rendered above the backdrop (z 130 > overlay 120
|
||||
// > backdrop 100). .room-page is position:relative with no z-index, so its
|
||||
// absolute children share the root stacking context with the fixed overlays.
|
||||
.position-strip {
|
||||
position: absolute;
|
||||
z-index: 110;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 0.5rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 130;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
justify-content: center;
|
||||
gap: round($gate-gap * 0.6);
|
||||
pointer-events: none;
|
||||
|
||||
// Edge midpoints, clockwise from 3 o'clock (slot drop order → role order)
|
||||
&[data-slot="1"] { left: calc(50% + #{$pos-d}); top: 50%; }
|
||||
&[data-slot="2"] { left: calc(50% + #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
|
||||
&[data-slot="3"] { left: calc(50% - #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
|
||||
&[data-slot="4"] { left: calc(50% - #{$pos-d}); top: 50%; }
|
||||
&[data-slot="5"] { left: calc(50% - #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
|
||||
&[data-slot="6"] { left: calc(50% + #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
|
||||
|
||||
.position-body {
|
||||
.gate-slot {
|
||||
position: relative;
|
||||
width: round($gate-node * 0.75);
|
||||
height: round($gate-node * 0.75);
|
||||
border-radius: 50%;
|
||||
border: $gate-line solid rgba(var(--terUser), 0.5);
|
||||
background: rgba(var(--priUser), 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
font-size: 1.8rem;
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--priUser), 0.12)
|
||||
;
|
||||
|
||||
.fa-chair {
|
||||
font-size: 1.1rem;
|
||||
color: rgba(var(--secUser), 0.4);
|
||||
}
|
||||
&.role-assigned {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
pointer-events: none;
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terUser), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terUser), 0.12)
|
||||
;
|
||||
}
|
||||
|
||||
.position-role-label {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
color: rgba(var(--secUser), 0.5);
|
||||
}
|
||||
&.filled, &.reserved {
|
||||
background: rgba(var(--terUser), 0.9);
|
||||
border-color: rgba(var(--terUser), 1);
|
||||
color: rgba(var(--priUser), 1);
|
||||
}
|
||||
|
||||
.position-status-icon {
|
||||
font-size: 0.65rem;
|
||||
&.fa-ban { color: rgba(var(--priRd), 1); }
|
||||
&.fa-circle-check { color: rgba(var(--priGn), 1); }
|
||||
}
|
||||
&.filled:hover, &.reserved:hover {
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
|
||||
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
.fa-chair {
|
||||
color: rgba(var(--terUser), 1);
|
||||
filter: drop-shadow(0 0 4px rgba(var(--ninUser), 1));
|
||||
.slot-number { font-size: 0.7em; opacity: 0.5; }
|
||||
.slot-gamer { display: none; }
|
||||
|
||||
form {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:has(.drop-token-btn) {
|
||||
background: rgba(var(--terUser), 1);
|
||||
border-color: rgba(var(--ninUser), 0.5);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
|
||||
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.position-strip {
|
||||
gap: round($gate-gap * 0.3);
|
||||
|
||||
.gate-slot {
|
||||
width: round($gate-node * 0.75);
|
||||
height: round($gate-node * 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,20 +447,61 @@ $pos-d-y: round($pos-d * 0.866); // 95px
|
||||
|
||||
.table-seat {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.25rem;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
// Centre the element on its anchor point
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
|
||||
// Clockwise from top — slot drop order during ROLE_SELECT
|
||||
&[data-slot="1"] { left: 50%; top: calc(50% - #{$seat-r}); }
|
||||
&[data-slot="2"] { left: calc(50% + #{$seat-r-x}); top: calc(50% - #{$seat-r-y}); }
|
||||
&[data-slot="3"] { left: calc(50% + #{$seat-r-x}); top: calc(50% + #{$seat-r-y}); }
|
||||
&[data-slot="4"] { left: 50%; top: calc(50% + #{$seat-r}); }
|
||||
&[data-slot="5"] { left: calc(50% - #{$seat-r-x}); top: calc(50% + #{$seat-r-y}); }
|
||||
&[data-slot="6"] { left: calc(50% - #{$seat-r-x}); top: calc(50% - #{$seat-r-y}); }
|
||||
// Edge midpoints, clockwise from 3 o'clock (slot drop order → role order)
|
||||
&[data-slot="1"] { left: calc(50% + #{$pos-d}); top: 50%; }
|
||||
&[data-slot="2"] { left: calc(50% + #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
|
||||
&[data-slot="3"] { left: calc(50% - #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
|
||||
&[data-slot="4"] { left: calc(50% - #{$pos-d}); top: 50%; }
|
||||
&[data-slot="5"] { left: calc(50% - #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
|
||||
&[data-slot="6"] { left: calc(50% + #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
|
||||
|
||||
// Chair: col 1, spans both rows
|
||||
.fa-chair {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
font-size: 1.6rem;
|
||||
color: rgba(var(--secUser), 0.4);
|
||||
transition: color 0.6s ease, filter 0.6s ease;
|
||||
}
|
||||
|
||||
// Abbreviation: col 2, row 1
|
||||
.seat-role-label {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
color: rgba(var(--secUser), 1);
|
||||
}
|
||||
|
||||
// Status icon: col 2, row 2, centred under the abbreviation
|
||||
.position-status-icon {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
justify-self: center;
|
||||
font-size: 0.8rem;
|
||||
&.fa-ban { color: rgba(var(--priRd), 1); }
|
||||
&.fa-circle-check { color: rgba(var(--priGn), 1); }
|
||||
}
|
||||
|
||||
&.active .fa-chair {
|
||||
color: rgba(var(--terUser), 1);
|
||||
filter: drop-shadow(0 0 4px rgba(var(--ninUser), 1));
|
||||
}
|
||||
|
||||
// After role confirmed: chair settles to full-opacity --secUser (no glow)
|
||||
&.role-confirmed .fa-chair {
|
||||
color: rgba(var(--secUser), 1);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.seat-portrait {
|
||||
width: 36px;
|
||||
@@ -690,17 +696,6 @@ $card-h: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.gate-slots {
|
||||
gap: 14px;
|
||||
|
||||
.gate-slot {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
.slot-number { font-size: 0.6em; }
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 0.75rem;
|
||||
h3 { font-size: 0.85rem; margin: 0.5rem 0; }
|
||||
|
||||
@@ -24,6 +24,10 @@ $handle-rect-h: 72px;
|
||||
$handle-exposed: 48px;
|
||||
$handle-r: 1rem;
|
||||
|
||||
#id_tray_wrap.role-select-phase {
|
||||
#id_tray_handle { visibility: hidden; pointer-events: none; }
|
||||
}
|
||||
|
||||
#id_tray_wrap {
|
||||
position: fixed;
|
||||
// left set by JS: closed = vw - handleW; open = vw - wrapW
|
||||
@@ -37,11 +41,11 @@ $handle-r: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
transition: left 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: left 1.0s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.tray-dragging { transition: none; }
|
||||
&.wobble { animation: tray-wobble 0.45s ease; }
|
||||
&.snap { animation: tray-snap 0.30s ease; }
|
||||
&.wobble { animation: tray-wobble .45s ease; }
|
||||
&.snap { animation: tray-snap 1.0s ease; }
|
||||
}
|
||||
|
||||
#id_tray_handle {
|
||||
@@ -134,7 +138,7 @@ $handle-r: 1rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.arc-in {
|
||||
animation: tray-role-arc-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
animation: tray-role-arc-in 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,11 +222,11 @@ $handle-r: 1rem;
|
||||
right: $sidebar-w;
|
||||
top: auto; // JS controls style.top for the Y-axis slide
|
||||
bottom: auto;
|
||||
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: top 1.0s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.tray-dragging { transition: none; }
|
||||
&.wobble { animation: tray-wobble-landscape 0.45s ease; }
|
||||
&.snap { animation: tray-snap-landscape 0.30s ease; }
|
||||
&.snap { animation: tray-snap-landscape 1.0s ease; }
|
||||
|
||||
}
|
||||
|
||||
@@ -304,7 +308,7 @@ $handle-r: 1rem;
|
||||
}
|
||||
|
||||
.tray-role-card.arc-in {
|
||||
animation: tray-role-arc-in-landscape 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
animation: tray-role-arc-in-landscape 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes tray-wobble-landscape {
|
||||
|
||||
@@ -2,6 +2,7 @@ describe("RoleSelect", () => {
|
||||
let testDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
RoleSelect._testReset(); // zero _placeCardDelay; clear _animationPending/_pendingTurnChange
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div class="room-page"
|
||||
@@ -152,7 +153,7 @@ describe("RoleSelect", () => {
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("room:turn_changed event", () => {
|
||||
let stack;
|
||||
let stack, trayWrap;
|
||||
|
||||
beforeEach(() => {
|
||||
// Six table seats, slot 1 starts active
|
||||
@@ -169,6 +170,12 @@ describe("RoleSelect", () => {
|
||||
stack.dataset.userSlots = "1";
|
||||
stack.dataset.starterRoles = "";
|
||||
testDiv.appendChild(stack);
|
||||
|
||||
trayWrap = document.createElement("div");
|
||||
trayWrap.id = "id_tray_wrap";
|
||||
// Simulate server-side class during ROLE_SELECT
|
||||
trayWrap.className = "role-select-phase";
|
||||
testDiv.appendChild(trayWrap);
|
||||
});
|
||||
|
||||
it("calls Tray.forceClose() on turn change", () => {
|
||||
@@ -179,13 +186,19 @@ describe("RoleSelect", () => {
|
||||
expect(Tray.forceClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("moves .active to the newly active seat", () => {
|
||||
it("re-adds role-select-phase to tray wrap on turn change", () => {
|
||||
trayWrap.classList.remove("role-select-phase"); // simulate it was shown
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2 }
|
||||
}));
|
||||
expect(
|
||||
testDiv.querySelector(".table-seat.active").dataset.slot
|
||||
).toBe("2");
|
||||
expect(trayWrap.classList.contains("role-select-phase")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears .active from all seats on turn change", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2 }
|
||||
}));
|
||||
expect(testDiv.querySelector(".table-seat.active")).toBeNull();
|
||||
});
|
||||
|
||||
it("removes .active from the previously active seat", () => {
|
||||
@@ -231,6 +244,119 @@ describe("RoleSelect", () => {
|
||||
stack.click();
|
||||
expect(document.querySelector(".role-select-backdrop")).toBeNull();
|
||||
});
|
||||
|
||||
it("updates seat icon to fa-circle-check when role appears in starter_roles", () => {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat";
|
||||
seat.dataset.role = "PC";
|
||||
const ban = document.createElement("i");
|
||||
ban.className = "position-status-icon fa-solid fa-ban";
|
||||
seat.appendChild(ban);
|
||||
testDiv.appendChild(seat);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: ["PC"] }
|
||||
}));
|
||||
|
||||
expect(seat.querySelector(".fa-ban")).toBeNull();
|
||||
expect(seat.querySelector(".fa-circle-check")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("adds role-confirmed to seat when role appears in starter_roles", () => {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat";
|
||||
seat.dataset.role = "PC";
|
||||
testDiv.appendChild(seat);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: ["PC"] }
|
||||
}));
|
||||
|
||||
expect(seat.classList.contains("role-confirmed")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves seat icon as fa-ban when role not in starter_roles", () => {
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat";
|
||||
seat.dataset.role = "NC";
|
||||
const ban = document.createElement("i");
|
||||
ban.className = "position-status-icon fa-solid fa-ban";
|
||||
seat.appendChild(ban);
|
||||
testDiv.appendChild(seat);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: ["PC"] }
|
||||
}));
|
||||
|
||||
expect(seat.querySelector(".fa-ban")).not.toBeNull();
|
||||
expect(seat.querySelector(".fa-circle-check")).toBeNull();
|
||||
});
|
||||
|
||||
it("adds role-assigned to slot-1 circle when 1 role assigned", () => {
|
||||
const circle = document.createElement("div");
|
||||
circle.className = "gate-slot filled";
|
||||
circle.dataset.slot = "1";
|
||||
testDiv.appendChild(circle);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: ["PC"] }
|
||||
}));
|
||||
|
||||
expect(circle.classList.contains("role-assigned")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves slot-2 circle visible when only 1 role assigned", () => {
|
||||
const circle = document.createElement("div");
|
||||
circle.className = "gate-slot filled";
|
||||
circle.dataset.slot = "2";
|
||||
testDiv.appendChild(circle);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: ["PC"] }
|
||||
}));
|
||||
|
||||
expect(circle.classList.contains("role-assigned")).toBe(false);
|
||||
});
|
||||
|
||||
it("updates data-active-slot on card stack to the new active slot", () => {
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(stack.dataset.activeSlot).toBe("2");
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// selectRole slot-circle fade-out //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("selectRole() slot-circle behaviour", () => {
|
||||
let circle, stack;
|
||||
|
||||
beforeEach(() => {
|
||||
// Gate-slot circle for slot 1 (active turn)
|
||||
circle = document.createElement("div");
|
||||
circle.className = "gate-slot filled";
|
||||
circle.dataset.slot = "1";
|
||||
testDiv.appendChild(circle);
|
||||
|
||||
// Card stack with active-slot=1 so selectRole() knows which circle to hide
|
||||
stack = document.createElement("div");
|
||||
stack.className = "card-stack";
|
||||
stack.dataset.state = "eligible";
|
||||
stack.dataset.starterRoles = "";
|
||||
stack.dataset.userSlots = "1";
|
||||
stack.dataset.activeSlot = "1";
|
||||
testDiv.appendChild(stack);
|
||||
|
||||
spyOn(Tray, "placeCard");
|
||||
});
|
||||
|
||||
it("adds role-assigned to the active slot's circle immediately on confirm", () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
expect(circle.classList.contains("role-assigned")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -242,9 +368,14 @@ describe("RoleSelect", () => {
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("tray card after successful role selection", () => {
|
||||
let guardConfirm;
|
||||
let guardConfirm, trayWrap;
|
||||
|
||||
beforeEach(() => {
|
||||
trayWrap = document.createElement("div");
|
||||
trayWrap.id = "id_tray_wrap";
|
||||
trayWrap.className = "role-select-phase";
|
||||
testDiv.appendChild(trayWrap);
|
||||
|
||||
// Spy on Tray.placeCard: call the onComplete callback immediately.
|
||||
spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
|
||||
if (cb) cb();
|
||||
@@ -261,13 +392,15 @@ describe("RoleSelect", () => {
|
||||
|
||||
it("calls Tray.placeCard() on success", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve(); // flush fetch .then()
|
||||
await new Promise(r => setTimeout(r, 0)); // flush _placeCardDelay setTimeout
|
||||
expect(Tray.placeCard).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the role code string to Tray.placeCard", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve(); // flush fetch .then()
|
||||
await new Promise(r => setTimeout(r, 0)); // flush _placeCardDelay setTimeout
|
||||
const roleCode = Tray.placeCard.calls.mostRecent().args[0];
|
||||
expect(typeof roleCode).toBe("string");
|
||||
expect(roleCode.length).toBeGreaterThan(0);
|
||||
@@ -281,6 +414,27 @@ describe("RoleSelect", () => {
|
||||
await Promise.resolve();
|
||||
expect(Tray.placeCard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes role-select-phase from tray wrap on successful pick", async () => {
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
expect(trayWrap.classList.contains("role-select-phase")).toBe(false);
|
||||
});
|
||||
|
||||
it("adds role-confirmed class to the seated position after placeCard completes", async () => {
|
||||
// Add a seat element matching the first available role (PC)
|
||||
const seat = document.createElement("div");
|
||||
seat.className = "table-seat";
|
||||
seat.dataset.role = "PC";
|
||||
seat.innerHTML = '<i class="position-status-icon fa-solid fa-ban"></i>';
|
||||
testDiv.appendChild(seat);
|
||||
|
||||
guardConfirm();
|
||||
await Promise.resolve(); // fetch resolves + placeCard fires
|
||||
await new Promise(r => setTimeout(r, 0)); // flush _postTrayDelay setTimeout
|
||||
|
||||
expect(seat.classList.contains("role-confirmed")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -360,17 +514,20 @@ describe("RoleSelect", () => {
|
||||
RoleSelect.openFan();
|
||||
document.querySelector("#id_role_select .card").click();
|
||||
guardConfirm();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve(); // flush fetch .then()
|
||||
await new Promise(r => setTimeout(r, 0)); // flush _placeCardDelay → placeCard called, heldCallback set
|
||||
|
||||
window.dispatchEvent(new CustomEvent("room:turn_changed", {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
|
||||
// Fire onComplete — deferred turn_changed should now run
|
||||
// Fire onComplete — post-tray delay (0 in tests) still uses setTimeout
|
||||
Tray._testFirePlaceCardComplete();
|
||||
await new Promise(r => setTimeout(r, 0)); // flush _postTrayDelay setTimeout
|
||||
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
// Seat glow is JS-only (tray animation window); after deferred
|
||||
// handleTurnChanged runs, all seat glows are cleared.
|
||||
expect(testDiv.querySelector(".table-seat.active")).toBeNull();
|
||||
});
|
||||
|
||||
it("turn_changed after animation completes is processed immediately", () => {
|
||||
@@ -379,8 +536,8 @@ describe("RoleSelect", () => {
|
||||
detail: { active_slot: 2, starter_roles: [] }
|
||||
}));
|
||||
expect(Tray.forceClose).toHaveBeenCalled();
|
||||
const activeSeat = testDiv.querySelector(".table-seat.active");
|
||||
expect(activeSeat && activeSeat.dataset.slot).toBe("2");
|
||||
// Seats are not persistently glowed; all active cleared
|
||||
expect(testDiv.querySelector(".table-seat.active")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user