diff --git a/webclient/eslint.config.mjs b/webclient/eslint.config.mjs index 8bfbcef3f..665859066 100644 --- a/webclient/eslint.config.mjs +++ b/webclient/eslint.config.mjs @@ -62,7 +62,6 @@ export default tseslint.config( 'max-len': ['error', { code: 140 }], 'no-eq-null': ['off'], 'no-func-assign': ['error'], - 'no-inline-comments': ['error'], 'no-mixed-spaces-and-tabs': ['error'], 'no-multi-spaces': ['error'], 'no-trailing-spaces': ['error'], diff --git a/webclient/integration/src/app/game-board.spec.tsx b/webclient/integration/src/app/game-board.spec.tsx index b062fb2ba..940f459da 100644 --- a/webclient/integration/src/app/game-board.spec.tsx +++ b/webclient/integration/src/app/game-board.spec.tsx @@ -270,7 +270,7 @@ describe('Game board integration', () => { const labels = Array.from(zones.querySelectorAll('.zone-stack__label')).map( (n) => n.textContent, ); - expect(labels).toEqual(['Deck', 'Graveyard', 'Exile']); + expect(labels).toEqual(['Deck', 'Hand', 'Graveyard', 'Exile']); // Stack is now its own column, not a zone inside the rail. expect(within(zones as HTMLElement).queryByText('Stack')).not.toBeInTheDocument(); expect(within(localBoard).getByTestId('stack-column-1')).toBeInTheDocument(); diff --git a/webclient/integration/src/websocket/authentication.spec.ts b/webclient/integration/src/websocket/authentication.spec.ts index c2fc04428..f8a35ad87 100644 --- a/webclient/integration/src/websocket/authentication.spec.ts +++ b/webclient/integration/src/websocket/authentication.spec.ts @@ -2,7 +2,7 @@ // and the hashed-password (salt) login path. import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Data } from '@app/types'; import { store } from '@app/store'; @@ -137,7 +137,7 @@ describe('authentication', () => { }); describe('hashed-password login (salt path)', () => { - it('requests salt then sends login with hashedPassword instead of plaintext', () => { + it('requests salt then sends login with hashedPassword instead of plaintext', async () => { connectAndHandshakeWithSalt({ userName: 'alice', password: 'secret' }); // First command should be RequestPasswordSalt, not Login @@ -153,6 +153,11 @@ describe('authentication', () => { value: create(Data.Response_PasswordSaltSchema, { passwordSalt: 'test-salt-value' }), }))); + // Sockatrice's serverIdentification handler awaits hashPassword (1000 SHA-512 rounds) + // before dispatching Command_Login. With fake timers active, advance real-time enough + // for crypto.subtle.digest + the chained microtasks to complete. + await vi.waitFor(() => findLastSessionCommand(Data.Command_Login_ext)); + // Now login should have been sent with hashedPassword const login = findLastSessionCommand(Data.Command_Login_ext); expect(login.value.userName).toBe('alice'); diff --git a/webclient/src/components/Game/Battlefield/gridMath.spec.ts b/webclient/src/components/Game/Battlefield/gridMath.spec.ts index 00098c782..8e13c4bd0 100644 --- a/webclient/src/components/Game/Battlefield/gridMath.spec.ts +++ b/webclient/src/components/Game/Battlefield/gridMath.spec.ts @@ -1,7 +1,6 @@ import { makeCard } from '../../../store/game/__mocks__/fixtures'; import { CARD_WIDTH_PX, - MAX_SUBPOS, PADDING_X_PX, STACKED_CARD_OFFSET_X_PX, applyInvertY, diff --git a/webclient/src/components/Game/Battlefield/gridMath.ts b/webclient/src/components/Game/Battlefield/gridMath.ts index a03e5d8e0..435914abe 100644 --- a/webclient/src/components/Game/Battlefield/gridMath.ts +++ b/webclient/src/components/Game/Battlefield/gridMath.ts @@ -24,8 +24,12 @@ export const ROW_COUNT = 3; export const MAX_SUBPOS = 3; // cards per stack column before overflow export function clampRow(y: number): number { - if (y < 0) return 0; - if (y >= ROW_COUNT) return ROW_COUNT - 1; + if (y < 0) { + return 0; + } + if (y >= ROW_COUNT) { + return ROW_COUNT - 1; + } return y; } @@ -39,7 +43,9 @@ export function stackColumnWidth( cardWidth: number = CARD_WIDTH_PX, offsetX: number = STACKED_CARD_OFFSET_X_PX, ): number { - if (cardCount <= 1) return cardWidth; + if (cardCount <= 1) { + return cardWidth; + } const extras = Math.min(cardCount - 1, MAX_SUBPOS - 1); return cardWidth + extras * offsetX; } @@ -98,7 +104,9 @@ export function closestGridPoint( ): number | null { const base = Math.floor(gridX / MAX_SUBPOS) * MAX_SUBPOS; for (let i = 0; i < MAX_SUBPOS; i++) { - if (!occupiedXs.has(base + i)) return base + i; + if (!occupiedXs.has(base + i)) { + return base + i; + } } return null; } diff --git a/webclient/src/components/Game/Battlefield/useBattlefield.ts b/webclient/src/components/Game/Battlefield/useBattlefield.ts index 192be5ce0..9ae1d5d4e 100644 --- a/webclient/src/components/Game/Battlefield/useBattlefield.ts +++ b/webclient/src/components/Game/Battlefield/useBattlefield.ts @@ -51,7 +51,9 @@ export function useBattlefield({ gameId, playerId, mirrored }: UseBattlefieldArg for (const card of cards) { // Children render nested under their parent via AttachmentStack, not as // their own lane slot. - if (isAttachedChild(card)) continue; + if (isAttachedChild(card)) { + continue; + } bucketed[rowIndexFor(card)].push(card); } for (const row of bucketed) { @@ -74,9 +76,13 @@ export function useBattlefield({ gameId, playerId, mirrored }: UseBattlefieldArg let maxCol = -1; for (const card of rowCards) { const col = Math.floor((card.x ?? 0) / MAX_SUBPOS); - if (!sparse[col]) sparse[col] = []; + if (!sparse[col]) { + sparse[col] = []; + } (sparse[col] as Data.ServerInfo_Card[]).push(card); - if (col > maxCol) maxCol = col; + if (col > maxCol) { + maxCol = col; + } } const filled: (Data.ServerInfo_Card[] | null)[] = []; for (let i = 0; i <= maxCol; i++) { diff --git a/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.spec.tsx b/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.spec.tsx index 7e501e515..09b367228 100644 --- a/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.spec.tsx +++ b/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.spec.tsx @@ -304,8 +304,9 @@ describe('PlayerInfoPanel', () => { const second = screen.getByTestId('counter-3').getAttribute('style') ?? ''; expect(first).toBe(second); - expect(first).toMatch(/rgba\(\d+, \d+, \d+, 1\)/); - expect(first).not.toMatch(/rgba\(0, 0, 0/); + // jsdom normalizes rgba(r,g,b,1) → rgb(r,g,b) on style attributes; accept either. + expect(first).toMatch(/rgba?\(\d+, \d+, \d+(?:, 1)?\)/); + expect(first).not.toMatch(/rgba?\(0, 0, 0/); }); }); diff --git a/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.tsx b/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.tsx index d1b022c9d..90cb8a922 100644 --- a/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.tsx +++ b/webclient/src/components/Game/PlayerInfoPanel/PlayerInfoPanel.tsx @@ -53,19 +53,19 @@ function PlayerInfoPanel({ const counterHandlers = (c: Data.ServerInfo_Counter) => canEdit ? { - role: 'button' as const, - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - handleIncrement(c.id, +1); - }, - // stopPropagation prevents the panel's onContextMenu (player menu) - // from firing when the user right-clicks a counter to decrement. - onContextMenu: (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - handleIncrement(c.id, -1); - }, - } + role: 'button' as const, + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + handleIncrement(c.id, +1); + }, + // stopPropagation prevents the panel's onContextMenu (player menu) + // from firing when the user right-clicks a counter to decrement. + onContextMenu: (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + handleIncrement(c.id, -1); + }, + } : {}; const renderCounterCircle = (c: Data.ServerInfo_Counter, modifier?: string) => ( diff --git a/webclient/src/components/Game/PlayerInfoPanel/usePlayerInfoPanel.ts b/webclient/src/components/Game/PlayerInfoPanel/usePlayerInfoPanel.ts index 20de074cd..c43401109 100644 --- a/webclient/src/components/Game/PlayerInfoPanel/usePlayerInfoPanel.ts +++ b/webclient/src/components/Game/PlayerInfoPanel/usePlayerInfoPanel.ts @@ -17,8 +17,12 @@ const NAME_COLOR_MAP: Record = { }; function isBlankColor(c: ServerColor): boolean { - if (!c) return true; - if ((c.a ?? 255) === 0) return true; + if (!c) { + return true; + } + if ((c.a ?? 255) === 0) { + return true; + } return c.r === 0 && c.g === 0 && c.b === 0; } diff --git a/webclient/src/containers/Game/useGameDnd.ts b/webclient/src/containers/Game/useGameDnd.ts index d10886d18..538b83b18 100644 --- a/webclient/src/containers/Game/useGameDnd.ts +++ b/webclient/src/containers/Game/useGameDnd.ts @@ -34,7 +34,9 @@ export interface UseGameDndArgs { function computePointerXInRow(event: DragEndEvent): number { const overRect = event.over?.rect; const activeRect = event.active.rect.current.translated; - if (!overRect || !activeRect) return 0; + if (!overRect || !activeRect) { + return 0; + } const cardCenterX = activeRect.left + activeRect.width / 2; return cardCenterX - overRect.left - MARGIN_LEFT_PX; } @@ -152,7 +154,9 @@ export function useGameDnd({ gameId, onDragStart }: UseGameDndArgs): GameDnd { const resolved = closestGridPoint(rawGridX, occupied); // Fully-occupied 3-card stack → desktop silently rejects (see // card_drag_item.cpp:115); skip dispatch to match. - if (resolved == null) return; + if (resolved == null) { + return; + } gridX = resolved; } diff --git a/webclient/src/containers/Room/GameSelector/GameSelector.tsx b/webclient/src/containers/Room/GameSelector/GameSelector.tsx index 2000aec48..009d53b93 100644 --- a/webclient/src/containers/Room/GameSelector/GameSelector.tsx +++ b/webclient/src/containers/Room/GameSelector/GameSelector.tsx @@ -53,7 +53,9 @@ const GameSelector = ({ room }: GameSelectorProps) => { // identifies which joined game to display. useReduxEffect<{ data: Data.Event_GameJoined }>((action) => { const gameId = action.payload.data.gameInfo?.gameId; - if (gameId == null) return; + if (gameId == null) { + return; + } navigate(generatePath(App.RouteEnum.GAME, { gameId: gameId.toString() })); }, GameTypes.GAME_JOINED, [navigate]); diff --git a/webclient/src/store/game/game.reducer.spec.ts b/webclient/src/store/game/game.reducer.spec.ts index ddd78fceb..f7423f332 100644 --- a/webclient/src/store/game/game.reducer.spec.ts +++ b/webclient/src/store/game/game.reducer.spec.ts @@ -662,7 +662,7 @@ describe('2C: CARD_MOVED', () => { playerId: 1, data: { cardId: 15, cardName: '', startPlayerId: 1, startZone: 'table', - position: -1, targetPlayerId: 1, targetZone: '', // ← stripped on the wire + position: -1, targetPlayerId: 1, targetZone: '', // ← stripped on the wire x: 18, y: 1, newCardId: 15, faceDown: false, newCardProviderId: '', }, })); diff --git a/webclient/src/store/game/game.selectors.ts b/webclient/src/store/game/game.selectors.ts index f36d713ce..6db0688ad 100644 --- a/webclient/src/store/game/game.selectors.ts +++ b/webclient/src/store/game/game.selectors.ts @@ -52,9 +52,15 @@ function materializeAttachmentsByParent( // desktop maintains a child list, but here every child points up via // attachCardId. Skip unattached cards and any pointer to a different // player / zone (the protocol allows cross-player attach). - if (card.attachCardId == null || card.attachCardId === -1) continue; - if (card.attachPlayerId !== playerId) continue; - if (card.attachZone !== App.ZoneName.TABLE) continue; + if (card.attachCardId == null || card.attachCardId === -1) { + continue; + } + if (card.attachPlayerId !== playerId) { + continue; + } + if (card.attachZone !== App.ZoneName.TABLE) { + continue; + } let bucket = map.get(card.attachCardId); if (!bucket) { bucket = [];