From 7bee3cbaa9d238ef38db0fa9fae08c4ef342595f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 19 Apr 2026 16:58:04 -0600 Subject: [PATCH] feat(ui): hide modifier tooltip on pieces with no active modifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hover tooltip previously rendered on every piece regardless of whether it had any modifier facts, showing just a piece-type header and 'No active modifiers' — noise with zero information the user can't already see on the board. Now returns null when there are no modifier rows. The pinned panel (click-to-pin) keeps its empty-state copy because an explicit pin is a deliberate inspect action where confirming 'nothing here' is valid. Tests: - Inverted the two T24 hover tests to assert the tooltip does NOT render on unmodified pieces (b1 knight, e2 pawn on a vanilla solo game). - Added a positive test: hover a modified pawn (HP +1 from a seeded profile) and assert the tooltip + at least one row are visible. --- packages/chess/e2e/modifier-profiles.spec.ts | 84 ++++++++++++++++---- packages/chess/src/ui/ModifierTooltip.tsx | 17 ++-- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/packages/chess/e2e/modifier-profiles.spec.ts b/packages/chess/e2e/modifier-profiles.spec.ts index 1643115..8a1bdfd 100644 --- a/packages/chess/e2e/modifier-profiles.spec.ts +++ b/packages/chess/e2e/modifier-profiles.spec.ts @@ -165,29 +165,85 @@ test.describe('Modifier Profiles — hover tooltip (T24)', () => { await page.waitForURL('**/game'); }); - test('hover piece shows modifier tooltip', async ({ page }) => { - // Hover white knight at b1 (standard opening position). + test('hover unmodified piece does NOT show tooltip', async ({ page }) => { + // Standard solo game — no modifier profile active, so b1 knight + // has no modifier facts. The tooltip only renders when the piece + // actually has modifiers; showing an empty card on every hover was + // noisy and unhelpful. 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"]'), + ).toHaveCount(0); + }); + + test('hover unmodified pawn does NOT show tooltip', async ({ page }) => { + // Same invariant, second fixture piece. Guards the "no noise on + // unmodified pieces" behaviour in case a future change accidentally + // re-enables the always-render path. + await page.hover('[data-square="e2"]'); // white pawn + await page.waitForTimeout(200); + + await expect( + page.locator('[data-testid="modifier-tooltip-row"]'), + ).toHaveCount(0); + await expect( + page.locator('[data-testid="modifier-tooltip"]'), + ).toHaveCount(0); + }); + + test('hover modified piece DOES show tooltip with rows', async ({ page }) => { + // Seed a profile that puts HP +1 on every white pawn, apply it to + // a fresh solo game via the lobby picker, then hover e2. The + // tooltip should render and surface the HP Bonus row. + await page.goto('/'); + const LIBRARY_KEY = 'houserules:modifier-profiles:v1'; + const entry = { + id: 't24-hover-positive', + name: 'Hover Positive', + profile: { + id: 't24-hover-positive', + name: 'Hover Positive', + description: '', + layoutId: 'classic', + perType: [ + { kind: 'hp-bonus', pieceType: 'pawn', color: 'white', value: 1 }, + ], + perInstance: [], + version: 1, + source: 'custom', + }, + starred: false, + updatedAt: Date.now(), + }; + await page.evaluate( + ({ key, e }) => localStorage.setItem(key, JSON.stringify([e])), + { key: LIBRARY_KEY, e: entry }, + ); + await page.reload(); + const picker = page.getByTestId('profile-picker'); + if (!(await picker.isVisible())) { + await page.locator('[data-action="open-rules-drawer"]').click(); + } + await expect(picker).toBeVisible(); + await picker.selectOption('t24-hover-positive'); + await page.locator('[data-action="play-solo"]').click(); + await page.waitForURL('**/game'); + + // Hover e2 — white pawn with HpBonus +1 from the profile. + await page.hover('[data-square="e2"]'); + await page.waitForTimeout(200); + 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. + ).toContainText(/pawn/i); await expect( - page.locator('[data-testid="modifier-tooltip-row"]'), - ).toHaveCount(0); + page.locator('[data-testid="modifier-tooltip-row"]').first(), + ).toBeVisible(); }); }); diff --git a/packages/chess/src/ui/ModifierTooltip.tsx b/packages/chess/src/ui/ModifierTooltip.tsx index 5268b8b..e8f96e5 100644 --- a/packages/chess/src/ui/ModifierTooltip.tsx +++ b/packages/chess/src/ui/ModifierTooltip.tsx @@ -5,9 +5,10 @@ * the engine session. Renders as an absolute-positioned card; the parent is * responsible for placement (positioned within a `relative` container). * - * Source attribution is simplified for T24: any fact that matches a - * MODIFIER_REGISTRY attribute is shown without distinguishing per-type / - * per-instance / preset origin (T26 can refine this). + * Renders nothing when the piece has no modifier facts — the tooltip exists + * to surface modifier state, and showing an empty card on every unmodified + * piece is noisy. The pinned panel (click-to-pin) carries the "no modifiers" + * empty state instead, since an explicit pin is a deliberate inspect action. */ import { asEntityId } from '@paratype/rete'; import { MODIFIER_REGISTRY } from '../modifiers/index.js'; @@ -40,6 +41,11 @@ export function ModifierTooltip({ pieceId, engine }: Props) { return [{ id: descriptor.id, label: descriptor.label, description }]; }); + // No modifiers on this piece → don't render the tooltip at all. A card + // that just says "White Knight" with an empty-state line adds noise + // without carrying information the user can't already see on the board. + if (modifierRows.length === 0) return null; + return (
))} - - {/* Empty state */} - {modifierRows.length === 0 && ( -

No active modifiers

- )} ); }