From b10734f382619de7828153c4232ec03da691e5ca Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 24 Sep 2026 16:02:45 +0200 Subject: [PATCH] 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) --- ...est-tenant-selector-zeitueberschreitung.md | 14 +++++++ .../marketplace/marketplace-filters.test.tsx | 32 ++++++++-------- .../(portal)/marketplace/marketplace.test.tsx | 13 ++----- .../marketplace/tenant-selector.test.tsx | 38 ++++++++----------- .../dashboard/widgets/proxmox-widget.test.tsx | 35 ++++++++++++----- 5 files changed, 73 insertions(+), 59 deletions(-) rename .planning/todos/{pending => completed}/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md (72%) diff --git a/.planning/todos/pending/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md b/.planning/todos/completed/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md similarity index 72% rename from .planning/todos/pending/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md rename to .planning/todos/completed/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md index 76c26b5..78c53b2 100644 --- a/.planning/todos/pending/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md +++ b/.planning/todos/completed/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md @@ -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. diff --git a/apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx b/apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx index 19c2f19..fc2505b 100644 --- a/apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx +++ b/apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx @@ -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) => { @@ -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; + 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(); 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(); 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(); 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(); 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(); 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); diff --git a/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx b/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx index ff06629..978737d 100644 --- a/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx +++ b/apps/web/src/app/(portal)/marketplace/marketplace.test.tsx @@ -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(); await waitFor(() => { @@ -126,7 +122,6 @@ describe('MarketplacePage', () => { }), ); - const Page = await importPage(); render(); await waitFor(() => { @@ -156,7 +151,6 @@ describe('MarketplacePage', () => { }), ); - const Page = await importPage(); render(); await waitFor(() => { @@ -176,7 +170,6 @@ describe('MarketplacePage', () => { }), ); - const Page = await importPage(); render(); await waitFor(() => { diff --git a/apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx b/apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx index d20ffd1..96fd626 100644 --- a/apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx +++ b/apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx @@ -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) => { @@ -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(); + render(); - 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(); 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(); await waitFor(() => { @@ -190,7 +186,6 @@ describe('ActivationDialog', () => { }), ); - const Page = await importPage(); render(); await waitFor(() => { @@ -224,7 +219,6 @@ describe('ActivationDialog', () => { }), ); - const Page = await importPage(); render(); await waitFor(() => { diff --git a/apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx b/apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx index 9afbfb3..64d6f6f 100644 --- a/apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx +++ b/apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx @@ -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; isEditMode?: boolean } = {}, ) { - const { ProxmoxWidget } = await import('./proxmox-widget'); - return render( - , - ); + let result!: ReturnType; + await act(async () => { + result = render( + , + ); + }); + 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) => ( ); - const { rerender } = rtlRender(ui(true)); + let rerender!: ReturnType['rerender']; + await act(async () => { + ({ rerender } = rtlRender(ui(true))); + }); await screen.findByTestId('proxmox-summary'); fireEvent.click(screen.getByRole('button', { name: 'Server auswählen' }));