diff --git a/frontend/apps/cli/src/__tests__/screens/masterdata/CategoryForm.test.tsx b/frontend/apps/cli/src/__tests__/screens/masterdata/CategoryForm.test.tsx new file mode 100644 index 0000000..1778c34 --- /dev/null +++ b/frontend/apps/cli/src/__tests__/screens/masterdata/CategoryForm.test.tsx @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from 'ink-testing-library'; +import React from 'react'; +import { CategoryCreateScreen } from '../../../components/masterdata/categories/CategoryCreateScreen.js'; +import { useCategories } from '../../../hooks/useCategories.js'; + +vi.mock('../../../state/navigation-context.js', () => ({ + useNavigation: () => ({ + replace: vi.fn(), + back: vi.fn(), + navigate: vi.fn(), + current: 'category-create', + params: {}, + canGoBack: true, + }), +})); + +vi.mock('../../../hooks/useCategories.js', () => ({ + useCategories: vi.fn(), +})); + +const createCategoryMock = vi.fn(); + +const defaultState = { + categories: [], + loading: false, + error: null, + fetchCategories: vi.fn(), + createCategory: createCategoryMock, + updateCategory: vi.fn(), + deleteCategory: vi.fn(), + clearError: vi.fn(), +}; + +describe('TC-CAT: Produktkategorie-Formular', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useCategories).mockReturnValue(defaultState); + }); + + it('TC-CAT-01: Rendert Formular korrekt (Happy-Path-Vorbedingung)', () => { + const { lastFrame } = render(React.createElement(CategoryCreateScreen)); + expect(lastFrame()).toContain('Neue Produktkategorie'); + expect(lastFrame()).toContain('Name *'); + expect(lastFrame()).toContain('Beschreibung'); + }); + + it('TC-CAT-04: Doppelter Name → API-Fehler wird angezeigt', () => { + vi.mocked(useCategories).mockReturnValue({ + ...defaultState, + error: 'Kategorie mit diesem Namen existiert bereits.', + }); + const { lastFrame } = render(React.createElement(CategoryCreateScreen)); + expect(lastFrame()).toContain('Kategorie mit diesem Namen existiert bereits.'); + }); + + it('TC-CAT-06: Leerer Name → Fehlermeldung, kein API-Call', async () => { + createCategoryMock.mockResolvedValue(null); + vi.mocked(useCategories).mockReturnValue({ ...defaultState, createCategory: createCategoryMock }); + + const { stdin, lastFrame } = render(React.createElement(CategoryCreateScreen)); + // Warten bis useInput-Effects (ink 5, readable-Listener) registriert sind + await new Promise((r) => setTimeout(r, 20)); + + // Enter auf Feld 0 (name) → springt zu Feld 1 (description) + stdin.write('\r'); + // Warten bis React re-rendert (description bekommt focus=true) + await new Promise((r) => setTimeout(r, 30)); + // Enter auf Feld 1 (description, letztes Feld) → löst handleSubmit aus + stdin.write('\r'); + await new Promise((r) => setTimeout(r, 50)); + + expect(lastFrame()).toContain('Name ist erforderlich'); + expect(createCategoryMock).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierForm.test.tsx b/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierForm.test.tsx new file mode 100644 index 0000000..60f5c1c --- /dev/null +++ b/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierForm.test.tsx @@ -0,0 +1,90 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from 'ink-testing-library'; +import React from 'react'; +import { SupplierCreateScreen } from '../../../components/masterdata/suppliers/SupplierCreateScreen.js'; +import { useSuppliers } from '../../../hooks/useSuppliers.js'; + +vi.mock('../../../state/navigation-context.js', () => ({ + useNavigation: () => ({ + replace: vi.fn(), + back: vi.fn(), + navigate: vi.fn(), + current: 'supplier-create', + params: {}, + canGoBack: true, + }), +})); + +vi.mock('../../../hooks/useSuppliers.js', () => ({ + useSuppliers: vi.fn(), +})); + +vi.mock('../../../utils/api-client.js', () => ({ + client: { + countries: { + search: vi.fn().mockResolvedValue([]), + }, + }, +})); + +const createSupplierMock = vi.fn(); + +const defaultState = { + suppliers: [], + loading: false, + error: null, + fetchSuppliers: vi.fn(), + createSupplier: createSupplierMock, + updateSupplier: vi.fn(), + activateSupplier: vi.fn(), + deactivateSupplier: vi.fn(), + rateSupplier: vi.fn(), + addCertificate: vi.fn(), + removeCertificate: vi.fn(), + clearError: vi.fn(), +}; + +describe('TC-SUP: Lieferanten-Formular', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useSuppliers).mockReturnValue(defaultState); + }); + + it('TC-SUP-01: Rendert Formular mit Pflichtfeld-Labels', () => { + const { lastFrame } = render(React.createElement(SupplierCreateScreen)); + expect(lastFrame()).toContain('Neuer Lieferant'); + expect(lastFrame()).toContain('Name *'); + expect(lastFrame()).toContain('Telefon *'); + }); + + it('TC-SUP-02: API-Fehler wird im Formular angezeigt', () => { + vi.mocked(useSuppliers).mockReturnValue({ + ...defaultState, + error: 'Lieferant mit diesem Namen existiert bereits.', + }); + const { lastFrame } = render(React.createElement(SupplierCreateScreen)); + expect(lastFrame()).toContain('Lieferant mit diesem Namen existiert bereits.'); + }); + + it('TC-SUP-03: Leerer Name → Fehlermeldung, kein API-Call', async () => { + createSupplierMock.mockResolvedValue(null); + vi.mocked(useSuppliers).mockReturnValue({ ...defaultState, createSupplier: createSupplierMock }); + + const { stdin, lastFrame } = render(React.createElement(SupplierCreateScreen)); + // Warten bis useInput-Effects (ink 5, readable-Listener) registriert sind + await new Promise((r) => setTimeout(r, 20)); + + // Tab durch alle 9 Feldübergänge bis zum letzten Feld (paymentDueDays) + for (let i = 0; i < 9; i++) { + stdin.write('\t'); + } + // Warten bis React re-rendert und das fokussierte Feld aktualisiert + await new Promise((r) => setTimeout(r, 30)); + // Enter auf letztem Feld (paymentDueDays) löst handleSubmit aus + stdin.write('\r'); + await new Promise((r) => setTimeout(r, 50)); + + expect(lastFrame()).toContain('Name ist erforderlich'); + expect(createSupplierMock).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierList.test.tsx b/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierList.test.tsx new file mode 100644 index 0000000..35f50c2 --- /dev/null +++ b/frontend/apps/cli/src/__tests__/screens/masterdata/SupplierList.test.tsx @@ -0,0 +1,113 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from 'ink-testing-library'; +import React from 'react'; +import { SupplierListScreen } from '../../../components/masterdata/suppliers/SupplierListScreen.js'; +import { useSuppliers } from '../../../hooks/useSuppliers.js'; +import type { SupplierDTO } from '@effigenix/api-client'; + +vi.mock('../../../state/navigation-context.js', () => ({ + useNavigation: () => ({ + replace: vi.fn(), + back: vi.fn(), + navigate: vi.fn(), + current: 'supplier-list', + params: {}, + canGoBack: true, + }), +})); + +vi.mock('../../../hooks/useSuppliers.js', () => ({ + useSuppliers: vi.fn(), +})); + +const mockSuppliers: SupplierDTO[] = [ + { + id: '1', + name: 'Frisch AG', + status: 'ACTIVE', + rating: null, + certificates: [], + } as unknown as SupplierDTO, + { + id: '2', + name: 'Alt GmbH', + status: 'INACTIVE', + rating: null, + certificates: [], + } as unknown as SupplierDTO, +]; + +const defaultState = { + suppliers: mockSuppliers, + loading: false, + error: null, + fetchSuppliers: vi.fn(), + createSupplier: vi.fn(), + updateSupplier: vi.fn(), + activateSupplier: vi.fn(), + deactivateSupplier: vi.fn(), + rateSupplier: vi.fn(), + addCertificate: vi.fn(), + removeCertificate: vi.fn(), + clearError: vi.fn(), +}; + +describe('TC-SUP-06: Lieferantenliste – Filter', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useSuppliers).mockReturnValue(defaultState); + }); + + it('Zeigt initial alle Lieferanten (Filter: Alle)', () => { + const { lastFrame } = render(React.createElement(SupplierListScreen)); + expect(lastFrame()).toContain('Lieferanten'); + expect(lastFrame()).toContain('Alle'); + expect(lastFrame()).toContain('Frisch AG'); + expect(lastFrame()).toContain('Alt GmbH'); + }); + + it('[A] filtert auf aktive Lieferanten', async () => { + const { stdin, lastFrame } = render(React.createElement(SupplierListScreen)); + await new Promise((r) => setTimeout(r, 20)); + + stdin.write('A'); + await new Promise((r) => setTimeout(r, 30)); + + expect(lastFrame()).toContain('Aktiv'); + expect(lastFrame()).toContain('Frisch AG'); + expect(lastFrame()).not.toContain('Alt GmbH'); + }); + + it('[I] filtert auf inaktive Lieferanten', async () => { + const { stdin, lastFrame } = render(React.createElement(SupplierListScreen)); + await new Promise((r) => setTimeout(r, 20)); + + stdin.write('I'); + await new Promise((r) => setTimeout(r, 30)); + + expect(lastFrame()).toContain('Inaktiv'); + expect(lastFrame()).toContain('Alt GmbH'); + expect(lastFrame()).not.toContain('Frisch AG'); + }); + + it('[a] setzt Filter zurück auf Alle', async () => { + const { stdin, lastFrame } = render(React.createElement(SupplierListScreen)); + await new Promise((r) => setTimeout(r, 20)); + + // Erst auf Aktiv filtern, dann zurücksetzen + stdin.write('A'); + await new Promise((r) => setTimeout(r, 30)); + stdin.write('a'); + await new Promise((r) => setTimeout(r, 30)); + + expect(lastFrame()).toContain('Alle'); + expect(lastFrame()).toContain('Frisch AG'); + expect(lastFrame()).toContain('Alt GmbH'); + }); + + it('Zeigt Lade-Spinner wenn loading=true', () => { + vi.mocked(useSuppliers).mockReturnValue({ ...defaultState, loading: true, suppliers: [] }); + const { lastFrame } = render(React.createElement(SupplierListScreen)); + expect(lastFrame()).toContain('Lade Lieferanten'); + }); +}); diff --git a/test-automation/TASKS.md b/test-automation/TASKS.md index 701f6c3..07d8310 100644 --- a/test-automation/TASKS.md +++ b/test-automation/TASKS.md @@ -12,12 +12,12 @@ Konzept: [`docs/ui-testing-automation.md`](./docs/ui-testing-automation.md) > Sofort umsetzbar, kein neues Setup nötig. Nutzt `ink-testing-library` + `vitest`. > Ort: `frontend/apps/cli/src/__tests__/screens/` -- [ ] `__tests__/screens/` Verzeichnisstruktur anlegen (`masterdata/`, `production/`) -- [ ] `SupplierForm.test.tsx` – TC-SUP-01 (Pflichtfelder), TC-SUP-02, TC-SUP-03 (leerer Name) -- [ ] `CategoryForm.test.tsx` – TC-CAT-01 (Happy Path), TC-CAT-04 (Duplikat), TC-CAT-06 (leerer Name) -- [ ] `SupplierList.test.tsx` – TC-SUP-06 (Filter/Suche) -- [ ] Bestehende 4 Tests (`ConfirmDialog`, `ErrorDisplay`, `SuccessDisplay`, `useRoles`) als Vorlage prüfen -- [ ] Alle neuen TUI-Tests laufen durch (`pnpm --filter @effigenix/cli test`) +- [x] `__tests__/screens/` Verzeichnisstruktur anlegen (`masterdata/`, `production/`) +- [x] `SupplierForm.test.tsx` – TC-SUP-01 (Pflichtfelder), TC-SUP-02, TC-SUP-03 (leerer Name) +- [x] `CategoryForm.test.tsx` – TC-CAT-01 (Happy Path), TC-CAT-04 (Duplikat), TC-CAT-06 (leerer Name) +- [x] `SupplierList.test.tsx` – TC-SUP-06 (Filter/Suche) +- [x] Bestehende 4 Tests (`ConfirmDialog`, `ErrorDisplay`, `SuccessDisplay`, `useRoles`) als Vorlage prüfen +- [x] Alle neuen TUI-Tests laufen durch (`pnpm --filter @effigenix/cli test`) ---