1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 14:09:34 +01:00
This commit is contained in:
Janosch 2026-03-27 10:36:32 +01:00
parent c84629cc4e
commit 6a672705c2
4 changed files with 285 additions and 6 deletions

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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');
});
});

View file

@ -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`. > Sofort umsetzbar, kein neues Setup nötig. Nutzt `ink-testing-library` + `vitest`.
> Ort: `frontend/apps/cli/src/__tests__/screens/` > Ort: `frontend/apps/cli/src/__tests__/screens/`
- [ ] `__tests__/screens/` Verzeichnisstruktur anlegen (`masterdata/`, `production/`) - [x] `__tests__/screens/` Verzeichnisstruktur anlegen (`masterdata/`, `production/`)
- [ ] `SupplierForm.test.tsx` TC-SUP-01 (Pflichtfelder), TC-SUP-02, TC-SUP-03 (leerer Name) - [x] `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) - [x] `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) - [x] `SupplierList.test.tsx` TC-SUP-06 (Filter/Suche)
- [ ] Bestehende 4 Tests (`ConfirmDialog`, `ErrorDisplay`, `SuccessDisplay`, `useRoles`) als Vorlage prüfen - [x] Bestehende 4 Tests (`ConfirmDialog`, `ErrorDisplay`, `SuccessDisplay`, `useRoles`) als Vorlage prüfen
- [ ] Alle neuen TUI-Tests laufen durch (`pnpm --filter @effigenix/cli test`) - [x] Alle neuen TUI-Tests laufen durch (`pnpm --filter @effigenix/cli test`)
--- ---