feat(ui): hide modifier tooltip on pieces with no active modifiers

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.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-19 16:58:04 -06:00
commit 7bee3cbaa9
No known key found for this signature in database
2 changed files with 79 additions and 22 deletions

View file

@ -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();
});
});

View file

@ -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 (
<div
data-testid="modifier-tooltip"
@ -63,11 +69,6 @@ export function ModifierTooltip({ pieceId, engine }: Props) {
</span>
</div>
))}
{/* Empty state */}
{modifierRows.length === 0 && (
<p className="text-[11px] text-neutral-400 italic">No active modifiers</p>
)}
</div>
);
}