test(260924-m4n): flackernden Marktplatz-Test entschaerfen, act-Warnungen der Proxmox-Kachel weg

- tenant-selector: Komponenten statisch statt im Test dynamisch importiert
  (Laden zaehlte in die 5-s-Frist des ersten Tests), SUPER_ADMIN/ADMIN in
  zwei it aufgetrennt
- marketplace/marketplace-filters: gleiche Umstellung; userEvent an die
  falsche Uhr gekoppelt statt auf shouldAdvanceTime zu warten
- proxmox-widget: Rendern wartet das erste Laden in act() ab (81 Warnungen weg)
- Todo 2026-09-23 nach completed/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 16:02:45 +02:00
parent dd09c08311
commit b10734f382
5 changed files with 73 additions and 59 deletions
@@ -49,3 +49,17 @@ auslöst. Der Fall wiederholt sich also, nicht zufällig.
Die drei parallelen Pipelines abschalten: der Lauf auf `main` und der auf dem
Tag prüfen unterschiedliche Dinge, und der `live`-Lauf ist die Absicherung,
dass der Zweig für sich genommen grün ist.
## Erledigt in quick-260924-m4n (24.09.2026)
- Ursache: `TenantContextSelector` und die Marktplatzseite wurden per
`await import(...)` INNERHALB der Tests geladen. Das Laden und Umwandeln der
Module zählte damit in die 5-s-Frist des ersten Tests — unter Läuferlast
(drei Pipelines je Freigabe) reicht das, um die Frist zu reißen.
- Behoben: statische Importe (vi.mock wird darüber gehoben), der Doppelfall
SUPER_ADMIN/ADMIN in zwei `it` aufgetrennt (der ADMIN-Fall prüft zusätzlich,
dass kein Abruf passiert). Dieselbe Umstellung in `marketplace.test.tsx` und
`marketplace-filters.test.tsx`; dort ist userEvent zusätzlich an die falsche
Uhr gekoppelt (`advanceTimers`). Kein globales `testTimeout`.
- Messung (ganze Web-Suite, lokal und auf 2 Kerne gedrosselt): kein Test über
2 s; der langsamste lag gedrosselt bei rund 1,3 s.
@@ -1,6 +1,9 @@
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Statisch statt je Test dynamisch importiert: Laden/Umwandeln der Seite faellt
// in die Sammelphase, nicht in die 5-s-Frist eines Tests (quick-260924-m4n).
import Page from './page';
vi.mock('next-intl', () => ({
useTranslations: () => (key: string, params?: Record<string, string>) => {
@@ -110,14 +113,14 @@ afterEach(() => {
vi.restoreAllMocks();
});
async function importPage() {
const mod = await import('./page');
return mod.default;
}
describe('Marketplace Filters', () => {
// userEvent an die falsche Uhr koppeln: sonst wartet jede Eingabe auf das
// langsame Echtzeit-Nachschieben von shouldAdvanceTime (quick-260924-m4n).
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
mockAuthStore.mockImplementation(
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
selector({
@@ -132,7 +135,6 @@ describe('Marketplace Filters', () => {
});
it('typing in search filters the grid to only matching modules after debounce', async () => {
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -142,7 +144,7 @@ describe('Marketplace Filters', () => {
expect(screen.getByText('Email Tool')).toBeInTheDocument();
const searchInput = screen.getByPlaceholderText('Module suchen...');
await userEvent.type(searchInput, 'Domain');
await user.type(searchInput, 'Domain');
act(() => {
vi.advanceTimersByTime(350);
@@ -155,7 +157,6 @@ describe('Marketplace Filters', () => {
});
it('selecting the Aktiviert status tab shows only activated modules', async () => {
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -163,7 +164,7 @@ describe('Marketplace Filters', () => {
});
const activeTab = screen.getByRole('tab', { name: /Aktiviert/i });
await userEvent.click(activeTab);
await user.click(activeTab);
await waitFor(() => {
expect(screen.getByText('Domaincheck')).toBeInTheDocument();
@@ -173,7 +174,6 @@ describe('Marketplace Filters', () => {
});
it('selecting a category chip shows only modules of that category; Alle shows all', async () => {
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -181,7 +181,7 @@ describe('Marketplace Filters', () => {
});
const utilitiesChip = screen.getByRole('button', { name: 'Utilities' });
await userEvent.click(utilitiesChip);
await user.click(utilitiesChip);
await waitFor(() => {
expect(screen.getByText('Converter')).toBeInTheDocument();
@@ -189,7 +189,7 @@ describe('Marketplace Filters', () => {
});
const allChip = screen.getByRole('button', { name: 'Alle' });
await userEvent.click(allChip);
await user.click(allChip);
await waitFor(() => {
expect(screen.getByText('Domaincheck')).toBeInTheDocument();
@@ -198,7 +198,6 @@ describe('Marketplace Filters', () => {
});
it('search + status + category filters compose together (AND)', async () => {
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -207,11 +206,11 @@ describe('Marketplace Filters', () => {
// Filter to Domain-Tools category
const domainChip = screen.getByRole('button', { name: 'Domain-Tools' });
await userEvent.click(domainChip);
await user.click(domainChip);
// Filter to available only
const availableTab = screen.getByRole('tab', { name: /Verfuegbar/i });
await userEvent.click(availableTab);
await user.click(availableTab);
// Domain-Tools + Available = only Email Tool (Domaincheck is active)
await waitFor(() => {
@@ -222,7 +221,6 @@ describe('Marketplace Filters', () => {
});
it('renders filtered-empty heading when filters produce zero results', async () => {
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -230,7 +228,7 @@ describe('Marketplace Filters', () => {
});
const searchInput = screen.getByPlaceholderText('Module suchen...');
await userEvent.type(searchInput, 'zzz-nonexistent');
await user.type(searchInput, 'zzz-nonexistent');
act(() => {
vi.advanceTimersByTime(350);
@@ -1,5 +1,8 @@
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Statisch statt je Test dynamisch importiert: Laden/Umwandeln der Seite faellt
// in die Sammelphase, nicht in die 5-s-Frist eines Tests (quick-260924-m4n).
import Page from './page';
// Mock next-intl
vi.mock('next-intl', () => ({
@@ -79,12 +82,6 @@ afterEach(() => {
vi.restoreAllMocks();
});
// Lazy import after mocks
async function importPage() {
const mod = await import('./page');
return mod.default;
}
describe('MarketplacePage', () => {
beforeEach(() => {
// Default: admin user
@@ -106,7 +103,6 @@ describe('MarketplacePage', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -126,7 +122,6 @@ describe('MarketplacePage', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -156,7 +151,6 @@ describe('MarketplacePage', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -176,7 +170,6 @@ describe('MarketplacePage', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -1,6 +1,13 @@
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';
// Komponenten statisch importiert (vi.mock wird ueber die Importe gehoben): das
// Laden und Umwandeln faellt so in die Sammelphase der Datei und nicht in die
// 5-s-Frist des ersten Tests. Frueher lag der dynamische Import im Test und hat
// unter Laeuferlast (drei parallele Pipelines bei jeder Freigabe) die Frist
// gerissen (Todo 2026-09-23, quick-260924-m4n).
import { TenantContextSelector } from './components/TenantContextSelector';
import Page from './page';
vi.mock('next-intl', () => ({
useTranslations: () => (key: string, params?: Record<string, string>) => {
@@ -86,19 +93,8 @@ afterEach(() => {
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
it('renders tenant options for SUPER_ADMIN', async () => {
mockAuthStore.mockImplementation(
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
selector({
@@ -116,16 +112,13 @@ describe('TenantContextSelector', () => {
}),
);
const TenantContextSelector = await importTenantSelector();
const { unmount } = render(<TenantContextSelector />);
render(<TenantContextSelector />);
await waitFor(() => {
expect(screen.getByText('Tenant Alpha')).toBeInTheDocument();
});
expect(await screen.findByText('Tenant Alpha')).toBeInTheDocument();
expect(screen.getByText('Tenant Beta')).toBeInTheDocument();
unmount();
});
// ADMIN case
it('renders nothing for ADMIN and does not load the tenant list', () => {
mockAuthStore.mockImplementation(
(selector: (state: { user: { id: string; username: string; displayName: string; role: string; tenantId: string } }) => unknown) =>
selector({
@@ -133,8 +126,12 @@ describe('TenantContextSelector', () => {
}),
);
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const { container } = render(<TenantContextSelector />);
expect(container.innerHTML).toBe('');
expect(fetchMock).not.toHaveBeenCalled();
});
it('calls setSelectedTenantId when selector value changes', async () => {
@@ -155,7 +152,6 @@ describe('TenantContextSelector', () => {
}),
);
const TenantContextSelector = await importTenantSelector();
render(<TenantContextSelector />);
await waitFor(() => {
@@ -190,7 +186,6 @@ describe('ActivationDialog', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -224,7 +219,6 @@ describe('ActivationDialog', () => {
}),
);
const Page = await importPage();
render(<Page />);
await waitFor(() => {
@@ -12,6 +12,9 @@ import type { ReactElement } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ProxmoxMetrics, ProxmoxServer } from '@/lib/proxmox-api';
import de from '@/messages/de.json';
// Statisch importiert (vi.mock wird darueber gehoben) — das Laden faellt in die
// Sammelphase, nicht in die Frist des ersten Tests (quick-260924-m4n).
import { ProxmoxWidget } from './proxmox-widget';
// Echter next-intl-Provider mit den deutschen Texten (Muster
// proxmox-page-roles.test.tsx) — die Kachel braucht ICU-Plural und useLocale.
@@ -119,17 +122,27 @@ function makeServer(
};
}
/**
* Rendert die Kachel und wartet INNERHALB von act() das Ende des ersten
* Ladens ab (aufgeloestes listServers-Versprechen -> setServers/setLoadFailed/
* setNow). Ohne das liefen diese Zustandsaenderungen nach dem Rendern ins Leere
* und React meldete je Test drei „not wrapped in act(...)“-Warnungen
* (quick-260924-m4n).
*/
async function renderWidget(
props: { config?: Record<string, unknown>; isEditMode?: boolean } = {},
) {
const { ProxmoxWidget } = await import('./proxmox-widget');
return render(
let result!: ReturnType<typeof render>;
await act(async () => {
result = render(
<ProxmoxWidget
instanceId="w-1"
config={props.config ?? {}}
isEditMode={props.isEditMode ?? false}
/>,
);
});
return result;
}
beforeEach(() => {
@@ -617,13 +630,15 @@ describe('ProxmoxWidget: Bearbeitungsmodus mit Titel und Serverauswahl (quick-26
it('Verlassen des Bearbeitungsmodus schliesst die Auswahl', async () => {
mockListServers.mockResolvedValue([makeServer('ok')]);
const { ProxmoxWidget } = await import('./proxmox-widget');
const ui = (isEditMode: boolean) => (
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
<ProxmoxWidget instanceId="w-1" config={{}} isEditMode={isEditMode} />
</NextIntlClientProvider>
);
const { rerender } = rtlRender(ui(true));
let rerender!: ReturnType<typeof rtlRender>['rerender'];
await act(async () => {
({ rerender } = rtlRender(ui(true)));
});
await screen.findByTestId('proxmox-summary');
fireEvent.click(screen.getByRole('button', { name: 'Server auswählen' }));