Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped

This commit is contained in:
Disco DeDisco
2026-03-17 00:24:23 -04:00
parent c9defa5a81
commit 01de6e7548
32 changed files with 2148 additions and 63 deletions

View File

@@ -0,0 +1,240 @@
describe("RoleSelect", () => {
let testDiv;
beforeEach(() => {
testDiv = document.createElement("div");
testDiv.innerHTML = `
<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(
Promise.resolve({ ok: true })
);
});
afterEach(() => {
RoleSelect.closeFan();
testDiv.remove();
});
// ------------------------------------------------------------------ //
// openFan() //
// ------------------------------------------------------------------ //
describe("openFan()", () => {
it("creates .role-select-backdrop in the DOM", () => {
RoleSelect.openFan();
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("creates #id_role_select inside the backdrop", () => {
RoleSelect.openFan();
expect(document.getElementById("id_role_select")).not.toBeNull();
});
it("renders exactly 6 .card elements", () => {
RoleSelect.openFan();
const cards = document.querySelectorAll("#id_role_select .card");
expect(cards.length).toBe(6);
});
it("does not open a second backdrop if already open", () => {
RoleSelect.openFan();
RoleSelect.openFan();
expect(document.querySelectorAll(".role-select-backdrop").length).toBe(1);
});
});
// ------------------------------------------------------------------ //
// closeFan() //
// ------------------------------------------------------------------ //
describe("closeFan()", () => {
it("removes .role-select-backdrop from the DOM", () => {
RoleSelect.openFan();
RoleSelect.closeFan();
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
it("removes #id_role_select from the DOM", () => {
RoleSelect.openFan();
RoleSelect.closeFan();
expect(document.getElementById("id_role_select")).toBeNull();
});
it("does not throw if no fan is open", () => {
expect(() => RoleSelect.closeFan()).not.toThrow();
});
});
// ------------------------------------------------------------------ //
// Card interactions //
// ------------------------------------------------------------------ //
describe("card interactions", () => {
beforeEach(() => {
RoleSelect.openFan();
});
it("mouseenter adds .flipped to the card", () => {
const card = document.querySelector("#id_role_select .card");
card.dispatchEvent(new MouseEvent("mouseenter"));
expect(card.classList.contains("flipped")).toBe(true);
});
it("mouseleave removes .flipped from the card", () => {
const card = document.querySelector("#id_role_select .card");
card.dispatchEvent(new MouseEvent("mouseenter"));
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(false);
});
it("clicking a card closes the fan", () => {
const card = document.querySelector("#id_role_select .card");
card.click();
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();
expect(window.fetch).toHaveBeenCalledWith(
"/epic/room/test-uuid/select-role",
jasmine.objectContaining({ method: "POST" })
);
});
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);
});
});
// ------------------------------------------------------------------ //
// Backdrop click //
// ------------------------------------------------------------------ //
describe("backdrop click", () => {
it("closes the fan", () => {
RoleSelect.openFan();
document.querySelector(".role-select-backdrop").click();
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();
});
});
// ------------------------------------------------------------------ //
// room:roles_revealed event //
// ------------------------------------------------------------------ //
describe("room:roles_revealed event", () => {
let reloadCalled;
beforeEach(() => {
reloadCalled = false;
RoleSelect.setReload(() => { reloadCalled = true; });
});
afterEach(() => {
RoleSelect.setReload(() => { window.location.reload(); });
});
it("triggers a page reload", () => {
window.dispatchEvent(new CustomEvent("room:roles_revealed", { detail: {} }));
expect(reloadCalled).toBe(true);
});
});
// ------------------------------------------------------------------ //
// room:turn_changed event //
// ------------------------------------------------------------------ //
describe("room:turn_changed event", () => {
let stack;
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);
seat.innerHTML = '<div class="seat-card-arc"></div>';
testDiv.appendChild(seat);
}
stack = document.createElement("div");
stack.className = "card-stack";
stack.dataset.state = "ineligible";
stack.dataset.userSlots = "1";
stack.dataset.takenRoles = "";
testDiv.appendChild(stack);
});
it("moves .active to the newly active seat", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(
testDiv.querySelector(".table-seat.active").dataset.slot
).toBe("2");
});
it("removes .active from the previously active seat", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(
testDiv.querySelector(".table-seat[data-slot='1']").classList.contains("active")
).toBe(false);
});
it("sets data-state to eligible when active_slot matches user slot", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
expect(stack.dataset.state).toBe("eligible");
});
it("sets data-state to ineligible when active_slot does not match", () => {
stack.dataset.state = "eligible";
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
expect(stack.dataset.state).toBe("ineligible");
});
it("clicking stack opens fan when newly eligible", () => {
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
stack.click();
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("clicking stack does not open fan when ineligible", () => {
// Make eligible first (adds listener), then flip back to ineligible
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 1 }
}));
window.dispatchEvent(new CustomEvent("room:turn_changed", {
detail: { active_slot: 2 }
}));
stack.click();
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
});
});

View File

@@ -19,8 +19,10 @@
<script src="lib/jasmine-6.0.1/boot0.js"></script>
<!-- spec files -->
<script src="Spec.js"></script>
<script src="RoleSelectSpec.js"></script>
<!-- src files -->
<script src="/static/apps/scripts/dashboard.js"></script>
<script src="/static/apps/dashboard/dashboard.js"></script>
<script src="/static/apps/epic/role-select.js"></script>
<!-- Jasmine env config (optional) -->
<script src="lib/jasmine-6.0.1/boot1.js"></script>