diff --git a/packages/chess/e2e/modifier-profiles.spec.ts b/packages/chess/e2e/modifier-profiles.spec.ts index 683850e..fc2e1f5 100644 --- a/packages/chess/e2e/modifier-profiles.spec.ts +++ b/packages/chess/e2e/modifier-profiles.spec.ts @@ -142,6 +142,55 @@ test.describe('Modifier Profiles', () => { }); }); +// ── T24: Hover modifier tooltip ────────────────────────────────────────── + +test.describe('Modifier Profiles — hover tooltip (T24)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + + // Clear autosave so Play Solo starts a fresh game. + await page.evaluate(() => { + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key !== null && key.startsWith('paratype-chess:v2:autosave:')) { + localStorage.removeItem(key); + } + } + localStorage.removeItem('paratype-chess:v1:autosave'); + }); + + // Navigate into the game view WITHOUT opening the rules drawer, + // so the board is fully interactive (no backdrop overlay). + await page.locator('[data-action="play-solo"]').click(); + await page.waitForURL('**/game'); + }); + + test('hover piece shows modifier tooltip', async ({ page }) => { + // Hover white knight at b1 (standard opening position). + await page.hover('[data-square="b1"]'); + await page.waitForTimeout(200); + + // Tooltip must appear and identify the piece. + await expect( + page.locator('[data-testid="modifier-tooltip"]'), + ).toBeVisible({ timeout: 1000 }); + await expect( + page.locator('[data-testid="modifier-tooltip"]'), + ).toContainText(/knight/i); + }); + + test('hover unmodified piece shows no modifier rows', async ({ page }) => { + // Standard solo game — no modifier profile active. + await page.hover('[data-square="e2"]'); // white pawn + await page.waitForTimeout(200); + + // Tooltip appears (header always shows) but no modifier rows. + await expect( + page.locator('[data-testid="modifier-tooltip-row"]'), + ).toHaveCount(0); + }); +}); + // ── T26: Lobby profile picker + GameView badge ───────────────────────── test.describe('Modifier Profiles — Lobby integration (T26)', () => { diff --git a/packages/chess/src/ui/Board.tsx b/packages/chess/src/ui/Board.tsx index c893cee..07eaeb9 100644 --- a/packages/chess/src/ui/Board.tsx +++ b/packages/chess/src/ui/Board.tsx @@ -34,6 +34,11 @@ interface BoardProps { /** Live engine reference for the visual effect stream. Optional — * omit in tests or headless renders; no effects will render. */ engine?: ChessEngine; + /** + * Called when the mouse enters a piece's square (pieceId) or leaves + * (null). Used by GameView to show/hide the modifier tooltip. + */ + onPieceHover?: (pieceId: number | null) => void; } interface PieceState { @@ -42,7 +47,7 @@ interface PieceState { color: PieceColor; } -export function Board({ facts, legalMoves, onMove, turn, myColor, lastMove, checkedKingSquare, activePresetIds, engine }: BoardProps) { +export function Board({ facts, legalMoves, onMove, turn, myColor, lastMove, checkedKingSquare, activePresetIds, engine, onPieceHover }: BoardProps) { // Pre-compute overlay components once per render — lookup is cheap // but doing it once in a useMemo keeps the Piece render path clean. const overlays: PieceOverlayComponent[] = useMemo( @@ -317,7 +322,12 @@ export function Board({ facts, legalMoves, onMove, turn, myColor, lastMove, chec * FLIP clean. */} {piece && ( -
+ {color} {pieceType} +
+ + {/* Modifier rows */} + {modifierRows.map((row) => ( +No active modifiers
+ )} +