feat(04-02): add TenantContextSelector, ActivationDialog, and asymmetric toggle UX
Super-Admin tenant dropdown fetches /tenants, sets selectedTenantId for per-tenant activation. Deactivation gated by confirmation dialog; activation immediate with toast. 18 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
interface ActivationDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
moduleName: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivationDialog({ open, moduleName, onConfirm, onCancel }: ActivationDialogProps) {
|
||||||
|
const t = useTranslations('marketplace');
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const confirmRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
cancelRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (document.activeElement === cancelRef.current) {
|
||||||
|
confirmRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
cancelRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handler);
|
||||||
|
return () => document.removeEventListener('keydown', handler);
|
||||||
|
}, [open, onCancel]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" role="dialog" aria-modal="true">
|
||||||
|
<div className="max-w-sm rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground mb-2">{t('deactivateDialogTitle')}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('deactivateDialogBody', { moduleName })}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
ref={cancelRef}
|
||||||
|
onClick={onCancel}
|
||||||
|
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted"
|
||||||
|
>
|
||||||
|
{t('deactivateDialogCancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ref={confirmRef}
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
{t('deactivateDialogConfirm')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||||
|
import { useMarketplaceStore } from '@/lib/stores/marketplace-store';
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
|
interface Tenant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
userCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TenantContextSelector() {
|
||||||
|
const t = useTranslations('marketplace');
|
||||||
|
const currentUser = useAuthStore((s) => s.user);
|
||||||
|
const { setSelectedTenantId } = useMarketplaceStore();
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const isSuperAdmin = currentUser?.role === 'SUPER_ADMIN';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSuperAdmin) return;
|
||||||
|
fetch(`${API_URL}/tenants`, { credentials: 'include' })
|
||||||
|
.then((res) => (res.ok ? res.json() : []))
|
||||||
|
.then((data: Tenant[]) => setTenants(data))
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [isSuperAdmin]);
|
||||||
|
|
||||||
|
if (!isSuperAdmin) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-muted/50 rounded-lg p-3 mb-4 border border-border">
|
||||||
|
<label className="block text-xs font-semibold text-muted-foreground mb-1.5">
|
||||||
|
{t('tenantContextLabel')}
|
||||||
|
</label>
|
||||||
|
{loading ? (
|
||||||
|
<span className="text-sm text-muted-foreground">Laden...</span>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
onChange={(e) => setSelectedTenantId(e.target.value || null)}
|
||||||
|
defaultValue=""
|
||||||
|
className="bg-card border border-border rounded-md px-3 py-1.5 text-sm min-w-[200px]"
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{tenants.map((tenant) => (
|
||||||
|
<option key={tenant.id} value={tenant.id}>
|
||||||
|
{tenant.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { MarketplaceSearch } from './components/MarketplaceSearch';
|
|||||||
import { StatusFilter, type StatusValue } from './components/StatusFilter';
|
import { StatusFilter, type StatusValue } from './components/StatusFilter';
|
||||||
import { CategoryFilter } from './components/CategoryFilter';
|
import { CategoryFilter } from './components/CategoryFilter';
|
||||||
import { ToastContainer, useToastStore } from './components/Toast';
|
import { ToastContainer, useToastStore } from './components/Toast';
|
||||||
|
import { TenantContextSelector } from './components/TenantContextSelector';
|
||||||
|
import { ActivationDialog } from './components/ActivationDialog';
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ export default function MarketplacePage() {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusValue>('all');
|
const [statusFilter, setStatusFilter] = useState<StatusValue>('all');
|
||||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||||
|
const [pendingDeactivation, setPendingDeactivation] = useState<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN';
|
const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN';
|
||||||
|
|
||||||
@@ -81,6 +84,7 @@ export default function MarketplacePage() {
|
|||||||
}
|
}
|
||||||
}, [isAdmin, fetchModules]);
|
}, [isAdmin, fetchModules]);
|
||||||
|
|
||||||
|
|
||||||
const locale = typeof window !== 'undefined' ? document.documentElement.lang || 'de' : 'de';
|
const locale = typeof window !== 'undefined' ? document.documentElement.lang || 'de' : 'de';
|
||||||
|
|
||||||
const filteredModules = useMemo(() => {
|
const filteredModules = useMemo(() => {
|
||||||
@@ -112,7 +116,16 @@ export default function MarketplacePage() {
|
|||||||
|
|
||||||
const hasFiltersApplied = searchQuery !== '' || statusFilter !== 'all' || categoryFilter !== 'all';
|
const hasFiltersApplied = searchQuery !== '' || statusFilter !== 'all' || categoryFilter !== 'all';
|
||||||
|
|
||||||
const toggleModule = async (moduleId: string, currentlyActive: boolean) => {
|
const handleToggle = (moduleId: string, currentlyActive: boolean) => {
|
||||||
|
if (currentlyActive) {
|
||||||
|
const mod = modules.find((m) => m.id === moduleId);
|
||||||
|
setPendingDeactivation({ id: moduleId, name: mod?.name ?? '' });
|
||||||
|
} else {
|
||||||
|
performToggle(moduleId, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performToggle = async (moduleId: string, currentlyActive: boolean) => {
|
||||||
setToggling(moduleId);
|
setToggling(moduleId);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -152,6 +165,13 @@ export default function MarketplacePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDeactivation = () => {
|
||||||
|
if (pendingDeactivation) {
|
||||||
|
performToggle(pendingDeactivation.id, true);
|
||||||
|
setPendingDeactivation(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
@@ -162,6 +182,8 @@ export default function MarketplacePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<TenantContextSelector />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{t('subtitle')}</p>
|
<p className="mt-1 text-sm text-muted-foreground">{t('subtitle')}</p>
|
||||||
@@ -224,7 +246,7 @@ export default function MarketplacePage() {
|
|||||||
category={mod.category}
|
category={mod.category}
|
||||||
isActive={activations.has(mod.id)}
|
isActive={activations.has(mod.id)}
|
||||||
isToggling={toggling === mod.id}
|
isToggling={toggling === mod.id}
|
||||||
onToggle={toggleModule}
|
onToggle={handleToggle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -233,6 +255,13 @@ export default function MarketplacePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
|
<ActivationDialog
|
||||||
|
open={pendingDeactivation !== null}
|
||||||
|
moduleName={pendingDeactivation?.name ?? ''}
|
||||||
|
onConfirm={confirmDeactivation}
|
||||||
|
onCancel={() => setPendingDeactivation(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('next-intl', () => ({
|
||||||
|
useTranslations: () => (key: string, params?: Record<string, string>) => {
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
title: 'Marktplatz',
|
||||||
|
subtitle: 'Entdecken und aktivieren Sie Module fuer Ihren Mandanten',
|
||||||
|
activate: 'Modul aktivieren',
|
||||||
|
deactivate: 'Deaktivieren',
|
||||||
|
searchPlaceholder: 'Module suchen...',
|
||||||
|
statusAll: 'Alle',
|
||||||
|
statusActive: 'Aktiviert',
|
||||||
|
statusAvailable: 'Verfuegbar',
|
||||||
|
categoryAll: 'Alle',
|
||||||
|
emptyTitle: 'Noch keine Module verfuegbar',
|
||||||
|
filteredEmptyTitle: 'Keine Ergebnisse',
|
||||||
|
errorLoad: 'Module konnten nicht geladen werden.',
|
||||||
|
toastActivated: 'Modul erfolgreich aktiviert',
|
||||||
|
toastDeactivated: 'Modul erfolgreich deaktiviert',
|
||||||
|
accessDenied: 'Zugriff verweigert',
|
||||||
|
tenantContextLabel: 'Mandanten-Kontext',
|
||||||
|
deactivateDialogTitle: 'Modul deaktivieren',
|
||||||
|
deactivateDialogBody: 'Moechten Sie {moduleName} fuer diesen Mandanten deaktivieren?',
|
||||||
|
deactivateDialogConfirm: 'Deaktivieren',
|
||||||
|
deactivateDialogCancel: 'Abbrechen',
|
||||||
|
};
|
||||||
|
let result = translations[key] ?? key;
|
||||||
|
if (params) {
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
result = result.replace(`{${k}}`, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockAuthStore = vi.fn();
|
||||||
|
vi.mock('@/lib/stores/auth-store', () => ({
|
||||||
|
useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockSetSelectedTenantId = vi.fn();
|
||||||
|
const mockBumpSidebarRefresh = vi.fn();
|
||||||
|
vi.mock('@/lib/stores/marketplace-store', () => ({
|
||||||
|
useMarketplaceStore: () => ({
|
||||||
|
sidebarRefreshKey: 0,
|
||||||
|
bumpSidebarRefresh: mockBumpSidebarRefresh,
|
||||||
|
selectedTenantId: null,
|
||||||
|
setSelectedTenantId: mockSetSelectedTenantId,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockTenants = [
|
||||||
|
{ id: 't1', name: 'Tenant Alpha', slug: 'alpha', isActive: true, createdAt: '2026-01-01', userCount: 5 },
|
||||||
|
{ id: 't2', name: 'Tenant Beta', slug: 'beta', isActive: true, createdAt: '2026-02-01', userCount: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockModules = [
|
||||||
|
{
|
||||||
|
id: 'mod-1',
|
||||||
|
slug: 'domaincheck',
|
||||||
|
name: 'Domaincheck',
|
||||||
|
version: '1.0.0',
|
||||||
|
category: 'Domain-Tools',
|
||||||
|
description: { de: 'Domain pruefen', en: 'Check domain' },
|
||||||
|
icon: 'Globe',
|
||||||
|
isSystem: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockActiveModules = [mockModules[0]];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
mockSetSelectedTenantId.mockClear();
|
||||||
|
mockBumpSidebarRefresh.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function importPage() {
|
||||||
|
const mod = await import('./page');
|
||||||
|
return mod.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importTenantSelector() {
|
||||||
|
const mod = await import('./components/TenantContextSelector');
|
||||||
|
return mod.TenantContextSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TenantContextSelector', () => {
|
||||||
|
it('renders tenant options for SUPER_ADMIN; renders nothing for ADMIN', async () => {
|
||||||
|
// SUPER_ADMIN case
|
||||||
|
mockAuthStore.mockImplementation(
|
||||||
|
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
|
||||||
|
selector({
|
||||||
|
user: { id: 'u1', username: 'superadmin', displayName: 'Super', role: 'SUPER_ADMIN', tenantId: 't1' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn((url: string) => {
|
||||||
|
if (url.includes('/tenants')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockTenants) });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const TenantContextSelector = await importTenantSelector();
|
||||||
|
const { unmount } = render(<TenantContextSelector />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Tenant Alpha')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Tenant Beta')).toBeInTheDocument();
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// ADMIN case
|
||||||
|
mockAuthStore.mockImplementation(
|
||||||
|
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
|
||||||
|
selector({
|
||||||
|
user: { id: 'u2', username: 'admin', displayName: 'Admin', role: 'ADMIN', tenantId: 't1' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<TenantContextSelector />);
|
||||||
|
expect(container.innerHTML).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setSelectedTenantId when selector value changes', async () => {
|
||||||
|
mockAuthStore.mockImplementation(
|
||||||
|
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
|
||||||
|
selector({
|
||||||
|
user: { id: 'u1', username: 'superadmin', displayName: 'Super', role: 'SUPER_ADMIN', tenantId: 't1' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn((url: string) => {
|
||||||
|
if (url.includes('/tenants')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockTenants) });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const TenantContextSelector = await importTenantSelector();
|
||||||
|
render(<TenantContextSelector />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = screen.getByRole('combobox');
|
||||||
|
await userEvent.selectOptions(select, 't2');
|
||||||
|
|
||||||
|
expect(mockSetSelectedTenantId).toHaveBeenCalledWith('t2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ActivationDialog', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAuthStore.mockImplementation(
|
||||||
|
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
|
||||||
|
selector({
|
||||||
|
user: { id: 'u1', username: 'admin', displayName: 'Admin', role: 'ADMIN', tenantId: 't1' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking deactivate opens dialog with module name, does NOT call API', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn((url: string) => {
|
||||||
|
if (url.includes('/modules/active')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockActiveModules) });
|
||||||
|
}
|
||||||
|
if (url.includes('/modules')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockModules) });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Page = await importPage();
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Domaincheck')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deactivateBtn = screen.getByText('Deaktivieren');
|
||||||
|
await userEvent.click(deactivateBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Moechten Sie Domaincheck/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// No deactivate API call yet
|
||||||
|
const fetchCalls = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
||||||
|
const deactivateCalls = fetchCalls.filter((c: string[]) => c[0]?.includes('/deactivate'));
|
||||||
|
expect(deactivateCalls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirming dialog calls deactivate; cancelling closes dialog without API call', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn((url: string) => {
|
||||||
|
if (url.includes('/deactivate')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||||
|
}
|
||||||
|
if (url.includes('/modules/active')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockActiveModules) });
|
||||||
|
}
|
||||||
|
if (url.includes('/modules')) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockModules) });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Page = await importPage();
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Domaincheck')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open dialog
|
||||||
|
const deactivateBtn = screen.getByText('Deaktivieren');
|
||||||
|
await userEvent.click(deactivateBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Moechten Sie Domaincheck/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel
|
||||||
|
const cancelBtn = screen.getByText('Abbrechen');
|
||||||
|
await userEvent.click(cancelBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText(/Moechten Sie Domaincheck/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-open and confirm
|
||||||
|
await userEvent.click(screen.getByText('Deaktivieren'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Moechten Sie Domaincheck/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogConfirmBtns = screen.getAllByText('Deaktivieren');
|
||||||
|
const dialogConfirm = dialogConfirmBtns.find(
|
||||||
|
(el) => el.closest('[role="dialog"]') !== null,
|
||||||
|
);
|
||||||
|
expect(dialogConfirm).toBeDefined();
|
||||||
|
await userEvent.click(dialogConfirm!);
|
||||||
|
|
||||||
|
// Verify deactivate API was called
|
||||||
|
await waitFor(() => {
|
||||||
|
const fetchCalls = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls;
|
||||||
|
const deactivateCalls = fetchCalls.filter((c: string[]) => c[0]?.includes('/deactivate'));
|
||||||
|
expect(deactivateCalls.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user