Three fixes to the my-sea spectator (bud-sea), all flowing over the existing `mysea_<owner>` spectate consumer: VISIT CAPTIONS (.sea-pos-label) — two bugs left every CROWN/COVER/… caption blank on my_sea_visit: - empty-hand: `label_by_position` was built from `latest_draw_slots`, which returns [] when the owner's hand is empty (only a significator placed) — so an owner mid-setup showed no captions, while her OWN my_sea (whose JS seeds labels from the POSITION_LABELS constant) showed them. Now the view pulls captions straight from POSITION_LABELS[spread], drawn-cards-independent. - `--seciUser` typo (used once, never defined) → invalid colour dropped → the labels inherited the body colour, contrasting on some palettes but blending into the felt on others (read as "missing"). → `--secUser`. SPREAD-SYNC — the owner's live draw pushed only the hand, not the spread, so a post-DEL spread switch landed the new cards into the OLD spread's cells (the asymmetry the user hit: owner on desire-obstacle-solution, visitor still laid out as escape-velocity). The spread now rides each `sea_draw` broadcast; `_applySpread` re-sets `data-spread` (CSS keys cell visibility off it), re-captions from a server-sourced POSITION_LABELS json_script, + clears stale fills before `_applyHand` repopulates against the right layout. OWNER-SIDE LIVE SEAT PUSH — the owner's my_sea now subscribes to her own spectate WS for `sea_seats`, so visitors arriving (deposit → 2C-6C) / leaving (BYE) appear without a refresh, same broadcast the spectators get. The visit page's inline `_renderSeats` is hoisted into my-sea-seats.js as the shared `mySeaRenderSeats(seats, myToken)` (+ `mySeaConnectSeatRing`); each page passes its own self-token (owner page passes '' — her 1C isn't --self server-side). Coverage: - ITs: MySeaVisitEmptyHandLabelsTest (captions present + rendered for an empty hand); MySeaLockHandViewTest broadcast test asserts the spread arg; spectate consumer test asserts the hand+spread relay (channels). - Jasmine: 6 new MySeaSeatsSpec cases for mySeaRenderSeats (per-seat rebuild, --self by token, owner-page no-self, no-duplicate re-render, one-shot flare). - Live-verified in Firefox: captions paint khaki on the brown palette; a desire-obstacle-solution sync flips data-spread + relabels Solution/Obstacle/ Desire + hides leave/cover/lay. [[feedback-jsonfield-exclude-sqlite-null]] not implicated; spread map is a plain dict lookup. 304 gameboard ITs + Jasmine green. Code architected by Disco DeDisco <discodedisco@outlook.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
5.1 KiB
JavaScript
127 lines
5.1 KiB
JavaScript
// Jasmine spec for my-sea-seats.js — the one-shot "seated" glow module
|
|
// (Phase B of the my-sea invite/voice sprint). Verifies the localStorage-
|
|
// gated first-view behaviour + the timed flare-class removal.
|
|
describe('my-sea-seats one-shot seated glow', function () {
|
|
var seat;
|
|
|
|
beforeEach(function () {
|
|
window.localStorage.clear();
|
|
seat = document.createElement('div');
|
|
seat.className = 'table-seat seated';
|
|
document.body.appendChild(seat);
|
|
});
|
|
|
|
afterEach(function () {
|
|
if (seat && seat.parentNode) seat.parentNode.removeChild(seat);
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it('exposes playSeatGlow globally', function () {
|
|
expect(typeof window.playSeatGlow).toBe('function');
|
|
});
|
|
|
|
it('adds the seat-just-seated flare class', function () {
|
|
window.playSeatGlow(seat);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(true);
|
|
});
|
|
|
|
it('marks a tokened seat seen in localStorage', function () {
|
|
seat.setAttribute('data-seat-token', 'visit-42');
|
|
window.playSeatGlow(seat);
|
|
expect(window.localStorage.getItem('mysea-seat-seen:visit-42')).toBe('1');
|
|
});
|
|
|
|
it('does not replay the flare for an already-seen token', function () {
|
|
seat.setAttribute('data-seat-token', 'visit-42');
|
|
window.localStorage.setItem('mysea-seat-seen:visit-42', '1');
|
|
window.playSeatGlow(seat);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(false);
|
|
});
|
|
|
|
it('always animates a tokenless seat (no persistence)', function () {
|
|
window.playSeatGlow(seat);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(true);
|
|
});
|
|
|
|
it('removes the flare class after the 2s glow window', function () {
|
|
jasmine.clock().install();
|
|
window.playSeatGlow(seat);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(true);
|
|
// Still flaring mid-window (bumped 1.5s → 2s, user-spec 2026-05-29).
|
|
jasmine.clock().tick(1600);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(true);
|
|
jasmine.clock().tick(500);
|
|
expect(seat.classList.contains('seat-just-seated')).toBe(false);
|
|
jasmine.clock().uninstall();
|
|
});
|
|
});
|
|
|
|
// mySeaRenderSeats — the shared seat-ring re-render driven by a `sea_seats`
|
|
// broadcast. Used by BOTH the owner's my_sea AND the spectator's my_sea_visit
|
|
// (DRY, 2026-05-30); each passes its own `myToken` to mark its own chair.
|
|
describe('my-sea-seats mySeaRenderSeats shared ring re-render', function () {
|
|
var scene;
|
|
|
|
beforeEach(function () {
|
|
window.localStorage.clear();
|
|
scene = document.createElement('div');
|
|
scene.className = 'room-table-scene';
|
|
document.body.appendChild(scene);
|
|
});
|
|
|
|
afterEach(function () {
|
|
if (scene && scene.parentNode) scene.parentNode.removeChild(scene);
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
var SEATS = [
|
|
{ n: 1, label: '1C', present: true, token: 'owner-9-3' },
|
|
{ n: 2, label: '2C', present: true, token: 'visit-42' },
|
|
{ n: 3, label: '3C', present: false, token: '' },
|
|
];
|
|
|
|
it('exposes mySeaRenderSeats globally', function () {
|
|
expect(typeof window.mySeaRenderSeats).toBe('function');
|
|
});
|
|
|
|
it('rebuilds one .table-seat per seat with the present/seated state', function () {
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
var seats = scene.querySelectorAll('.table-seat');
|
|
expect(seats.length).toBe(3);
|
|
expect(seats[0].classList.contains('seated')).toBe(true);
|
|
expect(seats[2].classList.contains('seated')).toBe(false);
|
|
// Present seats get the check icon; absent seats the ban icon.
|
|
expect(seats[0].querySelector('.fa-circle-check')).not.toBeNull();
|
|
expect(seats[2].querySelector('.fa-ban')).not.toBeNull();
|
|
expect(seats[1].querySelector('.seat-position-label').textContent).toBe('2C');
|
|
});
|
|
|
|
it('marks only the seat matching myToken as --self', function () {
|
|
window.mySeaRenderSeats(SEATS, 'visit-42');
|
|
var self = scene.querySelectorAll('.table-seat--self');
|
|
expect(self.length).toBe(1);
|
|
expect(self[0].getAttribute('data-slot')).toBe('2');
|
|
});
|
|
|
|
it('marks no seat --self when myToken is empty (owner page)', function () {
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
expect(scene.querySelectorAll('.table-seat--self').length).toBe(0);
|
|
});
|
|
|
|
it('clears prior seats before re-rendering (no duplicates)', function () {
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
expect(scene.querySelectorAll('.table-seat').length).toBe(3);
|
|
});
|
|
|
|
it('flares freshly-seated tokened seats once (localStorage-gated)', function () {
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
var owner = scene.querySelector('.table-seat[data-slot="1"]');
|
|
expect(owner.classList.contains('seat-just-seated')).toBe(true);
|
|
// Second render for the same token does not replay the flare.
|
|
window.mySeaRenderSeats(SEATS, '');
|
|
owner = scene.querySelector('.table-seat[data-slot="1"]');
|
|
expect(owner.classList.contains('seat-just-seated')).toBe(false);
|
|
});
|
|
});
|