fix(quick-260921-jt4): MarketplaceCard-Klick wird echte deckende Schaltflaeche
- Karten-onClick entfernt, deckende <button> als Geschwister traegt handleCardClick nur wenn isActive (D-01, verschachtelte Buttons vermieden wie bei DropZone.tsx aus 260921-bi2) - Aktivieren/Deaktivieren-Schaltflaeche bleibt eigener Tab-Stopp, Fussbereich bekommt relative fuer korrekten Stapelkontext - Neuer Katalogschluessel marketplace.openDetail in de.json/en.json - Tests: echte Tastaturbetaetigung (Enter auf fokussierter Schaltflaeche) statt Klick-Behauptung; kein Overlay im nicht-aktivierten Zustand Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -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<string, string | number>) => {
|
||||
const translations: Record<string, string> = {
|
||||
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(<MarketplaceCard {...baseProps} isActive={false} />);
|
||||
|
||||
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(
|
||||
<MarketplaceCard
|
||||
{...baseProps}
|
||||
isActive={true}
|
||||
hasAccess={true}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MarketplaceCard
|
||||
{...baseProps}
|
||||
isActive={true}
|
||||
hasAccess={false}
|
||||
onLockedClick={onLockedClick}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MarketplaceCard
|
||||
{...baseProps}
|
||||
isActive={true}
|
||||
hasAccess={true}
|
||||
onToggle={onToggle}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
@@ -114,16 +114,22 @@ export function MarketplaceCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border border-border bg-card p-5 shadow-sm transition-all ${
|
||||
locked
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: isActive
|
||||
? 'hover:shadow-md hover:border-primary/30 cursor-pointer'
|
||||
: 'hover:shadow-md hover:border-primary/30'
|
||||
className={`relative rounded-lg border border-border bg-card p-5 shadow-sm transition-all ${
|
||||
locked ? 'opacity-60' : 'hover:shadow-md hover:border-primary/30'
|
||||
}`}
|
||||
style={{ minHeight: '180px' }}
|
||||
onClick={isActive ? handleCardClick : undefined}
|
||||
>
|
||||
{/* Deckende Schaltflaeche: traegt den Kartenklick, damit die verschachtelte
|
||||
Aktivieren-Schaltflaeche im Fuss kein <button> in einem <button> wird. */}
|
||||
{isActive && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCardClick}
|
||||
aria-label={t('openDetail', { name })}
|
||||
className={`absolute inset-0 rounded-lg ${locked ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Header: icon + name + badges */}
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="rounded-md bg-muted p-2.5 shrink-0">
|
||||
@@ -167,7 +173,7 @@ export function MarketplaceCard({
|
||||
|
||||
{/* Footer: action button — only rendered for users who may activate/deactivate */}
|
||||
{canManage && (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="relative flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user