feat(09-06): MergeTab multi-file UI + PFX convert option + render tests

- actions.ts: mergeCertsAction(files, outputFormat, password?) builds FormData with
  multiple file fields; delegates to postForm('merge', ...) (T-09-02/T-09-04)
- MergeTab.tsx: multi-file state (local), file input (multiple), output selector
  (pem|pfx), Zusammenfuehren button disabled when < 2 files (data-testid for tests),
  onOutputFormatChange callback to page.tsx for shared PasswordField visibility
- ConvertTab.tsx: gains pfx option + onTargetFormatChange callback (same pattern)
- page.tsx: lifts mergeOutputFormat + convertOutputFormat state; showPassword now
  also true when active tab's output format is 'pfx'; passes callbacks to tabs
- cert-manager.test.tsx: 5 new tests — MergeTab disabled/enabled by file count,
  shared PasswordField appears on pfx output, downloadBase64 called on success;
  ConvertTab pfx option present; all 19/19 web tests green
- All production cert-manager files type-clean (pre-existing test type issues unchanged)
This commit is contained in:
2026-07-02 07:52:52 +02:00
parent 6326064ad3
commit 8b15a0099f
5 changed files with 299 additions and 15 deletions
@@ -44,6 +44,7 @@ vi.mock('next-intl', () => ({
import CertManagerPage from './page';
import { InspectTab } from './components/InspectTab';
import { SplitTab } from './components/SplitTab';
import { MergeTab } from './components/MergeTab';
import { ConvertTab } from './components/ConvertTab';
import * as actions from './actions';
@@ -336,4 +337,94 @@ describe('ConvertTab', () => {
);
expect(errorEl.className).toContain('text-destructive');
});
it('renders pem, der, p7b, and pfx options in the format selector', () => {
render(<ConvertTab file={null} pemText="" password="" />);
const select = screen.getByRole('combobox');
const optionValues = Array.from(select.querySelectorAll('option')).map(
(o) => (o as HTMLOptionElement).value,
);
expect(optionValues).toContain('pem');
expect(optionValues).toContain('der');
expect(optionValues).toContain('p7b');
expect(optionValues).toContain('pfx');
});
});
// ---------------------------------------------------------------------------
// MergeTab — unit tests (CERT-03, CERT-05)
// ---------------------------------------------------------------------------
const MOCK_MERGE_PEM_RESPONSE: actions.FileResponse = {
filename: 'chain.pem',
content: btoa('-----BEGIN CERTIFICATE-----\nXXX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nYYY\n-----END CERTIFICATE-----'),
mimeType: 'application/x-pem-file',
};
describe('MergeTab', () => {
it('disables Zusammenfuehren action button when fewer than 2 files are selected', () => {
render(<MergeTab password="" />);
// The action button should be disabled (no files yet)
const actionBtn = screen.getByTestId('merge-action-btn');
expect(actionBtn).toBeDisabled();
});
it('enables Zusammenfuehren button when 2 or more files are selected', async () => {
render(<MergeTab password="" />);
const fileInput = screen.getByTestId('merge-file-input');
const file1 = new File(['cert1'], 'cert1.pem', { type: 'application/x-pem-file' });
const file2 = new File(['cert2'], 'cert2.pem', { type: 'application/x-pem-file' });
fireEvent.change(fileInput, { target: { files: [file1, file2] } });
await waitFor(() => {
expect(screen.getByTestId('merge-action-btn')).not.toBeDisabled();
});
});
it('shows password field in page.tsx when PFX output is selected in MergeTab', async () => {
// Render the full page so the shared PasswordField behaviour can be tested
render(<CertManagerPage />);
// Navigate to merge tab — only one "Zusammenfuehren" visible before tab switch
fireEvent.click(screen.getByText('Zusammenfuehren'));
// Change format selector to pfx
const formatSelect = screen.getByDisplayValue('PEM-Kette');
fireEvent.change(formatSelect, { target: { value: 'pfx' } });
// The shared PasswordField should now be visible
await waitFor(() => {
expect(screen.getByText('Passwort (PFX/P12)')).toBeInTheDocument();
});
});
it('calls downloadBase64 after successful merge when >= 2 files selected', async () => {
vi.spyOn(actions, 'mergeCertsAction').mockResolvedValue(MOCK_MERGE_PEM_RESPONSE);
const downloadSpy = vi.spyOn(actions, 'downloadBase64').mockImplementation(() => {});
render(<MergeTab password="" />);
const fileInput = screen.getByTestId('merge-file-input');
const file1 = new File(['cert1'], 'cert1.pem', { type: 'application/x-pem-file' });
const file2 = new File(['cert2'], 'cert2.pem', { type: 'application/x-pem-file' });
fireEvent.change(fileInput, { target: { files: [file1, file2] } });
await waitFor(() => {
expect(screen.getByTestId('merge-action-btn')).not.toBeDisabled();
});
fireEvent.click(screen.getByTestId('merge-action-btn'));
await waitFor(() => {
expect(downloadSpy).toHaveBeenCalledWith(
MOCK_MERGE_PEM_RESPONSE.filename,
MOCK_MERGE_PEM_RESPONSE.content,
MOCK_MERGE_PEM_RESPONSE.mimeType,
);
});
});
});