diff --git a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx index 8483cf3..a43e902 100644 --- a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx @@ -4,15 +4,21 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; // Mock next-intl useTranslations vi.mock('next-intl', () => ({ - useTranslations: () => (key: string) => { + useTranslations: () => (key: string, values?: Record) => { const translations: Record = { activate: 'Modul aktivieren', deactivate: 'Deaktivieren', statusActive: 'Aktiviert', statusAvailable: 'Verfuegbar', statusNoAccess: 'Kein Zugriff', + openDetail: 'Details zu {name} öffnen', }; - return translations[key] ?? key; + const template = translations[key] ?? key; + if (!values) return template; + return Object.entries(values).reduce( + (acc, [placeholder, value]) => acc.replace(`{${placeholder}}`, String(value)), + template, + ); }, })); @@ -81,7 +87,15 @@ describe('MarketplaceCard', () => { expect(onToggle).toHaveBeenCalledWith('mod-1', false); }); - it('shows the "Kein Zugriff" badge for an active module without access and clicking the card triggers the toast instead of navigating (D-08)', async () => { + it('does not render a card overlay button for a not-yet-activated module (D-01)', () => { + render(); + + expect( + screen.queryByRole('button', { name: 'Details zu Domaincheck öffnen' }), + ).not.toBeInTheDocument(); + }); + + it('shows the "Kein Zugriff" badge for an active module without access and mouse-clicking the overlay button triggers the toast instead of navigating (D-08)', async () => { const onOpenDetail = vi.fn(); const onLockedClick = vi.fn(); const user = userEvent.setup(); @@ -98,13 +112,13 @@ describe('MarketplaceCard', () => { expect(screen.getByText('Kein Zugriff')).toBeInTheDocument(); - await user.click(screen.getByText('Domaincheck')); + await user.click(screen.getByRole('button', { name: 'Details zu Domaincheck öffnen' })); expect(onLockedClick).toHaveBeenCalledOnce(); expect(onOpenDetail).not.toHaveBeenCalled(); }); - it('does not show the "Kein Zugriff" badge for an active module with access and clicking the card navigates', async () => { + it('does not show the "Kein Zugriff" badge for an active module with access and mouse-clicking the overlay button navigates', async () => { const onOpenDetail = vi.fn(); const onLockedClick = vi.fn(); const user = userEvent.setup(); @@ -121,14 +135,80 @@ describe('MarketplaceCard', () => { expect(screen.queryByText('Kein Zugriff')).not.toBeInTheDocument(); - await user.click(screen.getByText('Domaincheck')); + await user.click(screen.getByRole('button', { name: 'Details zu Domaincheck öffnen' })); expect(onOpenDetail).toHaveBeenCalledOnce(); expect(onOpenDetail).toHaveBeenCalledWith('domaincheck'); expect(onLockedClick).not.toHaveBeenCalled(); }); - it('leaves the not-activated state unchanged: no access badge, and clicking the card does not fire navigate or locked callbacks', async () => { + it('opens the module detail via a real keyboard activation of the focused overlay button (D-01)', async () => { + const onOpenDetail = vi.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const overlay = screen.getByRole('button', { name: 'Details zu Domaincheck öffnen' }); + overlay.focus(); + await user.keyboard('{Enter}'); + + expect(onOpenDetail).toHaveBeenCalledOnce(); + expect(onOpenDetail).toHaveBeenCalledWith('domaincheck'); + }); + + it('triggers the locked hint via a real keyboard activation of the focused overlay button (D-01, D-08)', async () => { + const onLockedClick = vi.fn(); + const onOpenDetail = vi.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const overlay = screen.getByRole('button', { name: 'Details zu Domaincheck öffnen' }); + overlay.focus(); + await user.keyboard('{Enter}'); + + expect(onLockedClick).toHaveBeenCalledOnce(); + expect(onOpenDetail).not.toHaveBeenCalled(); + }); + + it('keeps the activate/deactivate footer button as its own reachable tab stop that does not additionally open the detail overlay', async () => { + const onToggle = vi.fn(); + const onOpenDetail = vi.fn(); + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByRole('button', { name: 'Deaktivieren' })); + + expect(onToggle).toHaveBeenCalledOnce(); + expect(onToggle).toHaveBeenCalledWith('mod-1', true); + expect(onOpenDetail).not.toHaveBeenCalled(); + }); + + it('leaves the not-activated state unchanged: no access badge, no overlay button, and clicking the visible text does not fire navigate or locked callbacks', async () => { const onOpenDetail = vi.fn(); const onLockedClick = vi.fn(); const user = userEvent.setup(); diff --git a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx index 37523a4..008bd68 100644 --- a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx @@ -114,16 +114,22 @@ export function MarketplaceCard({ return (
+ {/* Deckende Schaltflaeche: traegt den Kartenklick, damit die verschachtelte + Aktivieren-Schaltflaeche im Fuss kein