seat tray: tray.js, SCSS, FTs, Jasmine specs

- new apps.epic.static tray.js: IIFE with drag-open/click-close/wobble
  behaviour; document-level pointermove+mouseup listeners; reset() for
  Jasmine afterEach; try/catch around setPointerCapture for synthetic events
- _room.scss: #id_tray_wrap fixed-right flex container; #id_tray_handle +
  #id_tray_grip (box-shadow frame, transparent inner window, border-radius
  clip); #id_tray_btn grab cursor; #id_tray bevel box-shadows, margin-left
  gap, height removed (align-items:stretch handles it); tray-wobble keyframes
- _applets.scss + _game-kit.scss: z-index raised (312-318) for primacy over
  tray (310)
- room.html: #id_tray_wrap + children markup; tray.js script tag
- FTs test_room_tray: 5 tests (T1-T5); _simulate_drag via execute_script
  pointer events (replaces unreliable ActionChains drag); wobble asserts on
  #id_tray_wrap not btn
- static_src/tests/TraySpec.js + SpecRunner.html: Jasmine unit tests for
  all tray.js branches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-03-28 18:52:46 -04:00
parent 30ea0fad9d
commit b35c9b483e
22 changed files with 16193 additions and 18 deletions

View File

@@ -20,9 +20,11 @@
<!-- spec files -->
<script src="Spec.js"></script>
<script src="RoleSelectSpec.js"></script>
<script src="TraySpec.js"></script>
<!-- src files -->
<script src="/static/apps/dashboard/dashboard.js"></script>
<script src="/static/apps/epic/role-select.js"></script>
<script src="/static/apps/epic/tray.js"></script>
<!-- Jasmine env config (optional) -->
<script src="lib/jasmine-6.0.1/boot1.js"></script>

View File

@@ -0,0 +1,206 @@
// ── TraySpec.js ───────────────────────────────────────────────────────────────
//
// Unit specs for tray.js — the per-seat, per-room slide-out panel anchored
// to the right edge of the viewport.
//
// DOM contract assumed by the module:
// #id_tray_wrap — outermost container; JS sets style.left for positioning
// #id_tray_btn — the drawer-handle button
// #id_tray — the tray panel (hidden by default)
//
// Public API under test:
// Tray.init() — compute bounds, apply vertical bounds, attach listeners
// Tray.open() — reveal tray, animate wrap to minLeft
// Tray.close() — hide tray, animate wrap to maxLeft
// Tray.isOpen() — state predicate
// Tray.reset() — restore initial state (for afterEach)
//
// Drag model: tray follows pointer in real-time; position persists on release.
// Any leftward drag opens the tray.
// Drag > 10px suppresses the subsequent click event.
//
// ─────────────────────────────────────────────────────────────────────────────
describe("Tray", () => {
let btn, tray, wrap;
beforeEach(() => {
wrap = document.createElement("div");
wrap.id = "id_tray_wrap";
btn = document.createElement("button");
btn.id = "id_tray_btn";
tray = document.createElement("div");
tray.id = "id_tray";
tray.style.display = "none";
wrap.appendChild(btn);
document.body.appendChild(wrap);
document.body.appendChild(tray);
Tray.init();
});
afterEach(() => {
Tray.reset();
wrap.remove();
tray.remove();
});
// ---------------------------------------------------------------------- //
// open() //
// ---------------------------------------------------------------------- //
describe("open()", () => {
it("makes #id_tray visible", () => {
Tray.open();
expect(tray.style.display).not.toBe("none");
});
it("adds .open to #id_tray_btn", () => {
Tray.open();
expect(btn.classList.contains("open")).toBe(true);
});
it("sets wrap left to minLeft (0)", () => {
Tray.open();
expect(wrap.style.left).toBe("0px");
});
it("calling open() twice does not duplicate .open", () => {
Tray.open();
Tray.open();
const openCount = btn.className.split(" ").filter(c => c === "open").length;
expect(openCount).toBe(1);
});
});
// ---------------------------------------------------------------------- //
// close() //
// ---------------------------------------------------------------------- //
describe("close()", () => {
beforeEach(() => Tray.open());
it("hides #id_tray", () => {
Tray.close();
expect(tray.style.display).toBe("none");
});
it("removes .open from #id_tray_btn", () => {
Tray.close();
expect(btn.classList.contains("open")).toBe(false);
});
it("sets wrap left to maxLeft", () => {
Tray.close();
expect(parseInt(wrap.style.left, 10)).toBeGreaterThan(0);
});
it("does not throw if already closed", () => {
Tray.close();
expect(() => Tray.close()).not.toThrow();
});
});
// ---------------------------------------------------------------------- //
// isOpen() //
// ---------------------------------------------------------------------- //
describe("isOpen()", () => {
it("returns false by default", () => {
expect(Tray.isOpen()).toBe(false);
});
it("returns true after open()", () => {
Tray.open();
expect(Tray.isOpen()).toBe(true);
});
it("returns false after close()", () => {
Tray.open();
Tray.close();
expect(Tray.isOpen()).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Click when closed — wobble wrap, do not open //
// ---------------------------------------------------------------------- //
describe("clicking btn when closed", () => {
it("adds .wobble to wrap", () => {
btn.click();
expect(wrap.classList.contains("wobble")).toBe(true);
});
it("does not open the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
it("removes .wobble once animationend fires on wrap", () => {
btn.click();
wrap.dispatchEvent(new Event("animationend"));
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Click when open — close, no wobble //
// ---------------------------------------------------------------------- //
describe("clicking btn when open", () => {
beforeEach(() => Tray.open());
it("closes the tray", () => {
btn.click();
expect(Tray.isOpen()).toBe(false);
});
it("does not add .wobble", () => {
btn.click();
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// Drag interaction — continuous positioning //
// ---------------------------------------------------------------------- //
describe("drag interaction", () => {
function simulateDrag(deltaX) {
const startX = 800;
btn.dispatchEvent(new PointerEvent("pointerdown", { clientX: startX, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointermove", { clientX: startX + deltaX, bubbles: true }));
btn.dispatchEvent(new PointerEvent("pointerup", { clientX: startX + deltaX, bubbles: true }));
}
it("dragging left opens the tray", () => {
simulateDrag(-60);
expect(Tray.isOpen()).toBe(true);
});
it("any leftward drag opens the tray", () => {
simulateDrag(-20);
expect(Tray.isOpen()).toBe(true);
});
it("dragging right does not open the tray", () => {
simulateDrag(100);
expect(Tray.isOpen()).toBe(false);
});
it("drag > 10px suppresses the subsequent click", () => {
simulateDrag(-60);
btn.click(); // should be swallowed — tray stays open
expect(Tray.isOpen()).toBe(true);
});
it("does not add .wobble during drag", () => {
simulateDrag(-60);
expect(wrap.classList.contains("wobble")).toBe(false);
});
});
});