feat(09-03): InspectTab UI + inspectCertAction + render tests

- Added inspectCertAction to actions.ts (JSON path for pemText, multipart path for file)
- Added CertDetails interface to actions.ts (mirrors API response shape)
- Implemented InspectTab: Analysieren button, loading state, grid-cols-2 result grid
- InspectTab handles wrong-password error (t('error.wrongPassword')) and generic error
- Added 2 new InspectTab tests: success grid (subject CN + SHA-256) and error (text-destructive)
- Fixed setup.ts: explicit expect.extend(matchers) for vitest@4.x compatibility
  (Rule 1: @testing-library/jest-dom/vitest not extending global expect in vitest 4)
- Fixed existing test: getByText -> getAllByText for 'Analysieren' (now appears in tab nav + button)
- 9/9 cert-manager tests pass
This commit is contained in:
2026-07-01 23:55:41 +02:00
parent ba994635e8
commit 64a8e725e7
4 changed files with 285 additions and 7 deletions
@@ -1,4 +1,4 @@
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
import { cleanup, render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
// Mock next-intl — provide certManager namespace keys
@@ -42,9 +42,12 @@ vi.mock('next-intl', () => ({
}));
import CertManagerPage from './page';
import { InspectTab } from './components/InspectTab';
import * as actions from './actions';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('CertManagerPage shell', () => {
@@ -56,7 +59,9 @@ describe('CertManagerPage shell', () => {
it('renders all four tab labels', () => {
render(<CertManagerPage />);
expect(screen.getByText('Analysieren')).toBeInTheDocument();
// "Analysieren" appears as both tab label (nav) and InspectTab action button
// getAllByText avoids the "multiple elements" error
expect(screen.getAllByText('Analysieren').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Aufteilen')).toBeInTheDocument();
expect(screen.getByText('Zusammenfuehren')).toBeInTheDocument();
expect(screen.getByText('Konvertieren')).toBeInTheDocument();
@@ -99,3 +104,86 @@ describe('CertManagerPage shell', () => {
expect(screen.getByText('Lade eine Datei hoch und waehle ein Ausgabeformat.')).toBeInTheDocument();
});
});
// ---------------------------------------------------------------------------
// InspectTab — unit tests for the Analysieren result grid and error state
// ---------------------------------------------------------------------------
const MOCK_CERT_DETAILS: actions.CertDetails = {
subject: { cn: 'example.com', o: 'Acme Corp', ou: 'IT', c: 'DE' },
issuer: { cn: 'Example CA', o: 'CA Corp', c: 'US' },
validity: {
notBefore: '2025-01-01T00:00:00.000Z',
notAfter: '2026-01-01T00:00:00.000Z',
isExpired: false,
daysLeft: 180,
},
san: ['example.com', 'www.example.com'],
keyType: 'RSA',
keyBits: 2048,
serialNumber: '01',
signatureAlgorithm: 'sha256WithRSAEncryption',
fingerprint: {
sha1: 'AA:BB:CC:DD:EE',
sha256: 'FF:00:11:22:33:44:55:66:77:88',
},
pemPreview: '-----BEGIN CERTIFICATE-----\nXXX\n-----END CERTIFICATE-----',
};
describe('InspectTab', () => {
it('renders CertDetails grid with subject CN and SHA-256 after successful inspect', async () => {
vi.spyOn(actions, 'inspectCertAction').mockResolvedValue(MOCK_CERT_DETAILS);
render(
<InspectTab
file={null}
pemText="-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"
password=""
/>,
);
// Click the Analysieren button
fireEvent.click(screen.getByText('Analysieren'));
// Wait for the async action to complete and result grid to render
await waitFor(() => {
expect(screen.getByText('example.com')).toBeInTheDocument();
});
// SHA-256 fingerprint is rendered
expect(screen.getByText('FF:00:11:22:33:44:55:66:77:88')).toBeInTheDocument();
// Empty state is no longer shown
expect(screen.queryByText('Kein Zertifikat geladen.')).not.toBeInTheDocument();
});
it('shows text-destructive error when inspectCertAction rejects', async () => {
vi.spyOn(actions, 'inspectCertAction').mockRejectedValue(
new Error('wrong password'),
);
render(
<InspectTab
file={null}
pemText="test pem"
password="wrong"
/>,
);
fireEvent.click(screen.getByText('Analysieren'));
// Wait for the error message to appear
await waitFor(() => {
// The wrong-password error message should be rendered
expect(
screen.getByText('Falsches Passwort. PFX/P12-Datei konnte nicht entschluesselt werden.'),
).toBeInTheDocument();
});
// Error element should have the destructive class
const errorEl = screen.getByText(
'Falsches Passwort. PFX/P12-Datei konnte nicht entschluesselt werden.',
);
expect(errorEl.className).toContain('text-destructive');
});
});