test: Add comprehensive test coverage for untested modules

Add tests for previously untested components and services:
- apiKeyService.ts: localStorage operations (14 tests)
- ApiKeyModal.tsx: form validation, submission (22 tests)
- App.tsx: state transitions, error handling (23 tests)
- latexService.ts: API calls, template detection (35 tests)
- latex_service.py: LaTeX escaping, compilation (59 tests)
- server.py: Flask routes, field mapping (38 tests)

Also fix geminiService tests by adding proper apiKeyService mock.

Total new test coverage: 173 TypeScript tests, 97 Python tests

https://claude.ai/code/session_01D4k6b4nUjwfcHMvectsri2
This commit is contained in:
Claude 2026-01-31 10:33:40 +00:00
parent 1eb00f3d64
commit 04fe925891
No known key found for this signature in database
7 changed files with 2158 additions and 8 deletions

View file

@ -0,0 +1,125 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { getApiKey, setApiKey, clearApiKey, hasApiKey } from '../../services/apiKeyService';
describe('apiKeyService', () => {
const STORAGE_KEY = 'gemini_api_key';
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
});
describe('getApiKey', () => {
it('should return null when no key is stored', () => {
const result = getApiKey();
expect(result).toBeNull();
});
it('should return the stored API key', () => {
localStorage.setItem(STORAGE_KEY, 'AItest123');
const result = getApiKey();
expect(result).toBe('AItest123');
});
it('should return empty string if empty string was stored', () => {
localStorage.setItem(STORAGE_KEY, '');
const result = getApiKey();
expect(result).toBe('');
});
});
describe('setApiKey', () => {
it('should store the API key in localStorage', () => {
setApiKey('AItest456');
expect(localStorage.getItem(STORAGE_KEY)).toBe('AItest456');
});
it('should overwrite existing key', () => {
localStorage.setItem(STORAGE_KEY, 'AIold');
setApiKey('AInew');
expect(localStorage.getItem(STORAGE_KEY)).toBe('AInew');
});
it('should allow storing empty string', () => {
setApiKey('');
expect(localStorage.getItem(STORAGE_KEY)).toBe('');
});
});
describe('clearApiKey', () => {
it('should remove the API key from localStorage', () => {
localStorage.setItem(STORAGE_KEY, 'AItest');
clearApiKey();
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
});
it('should not throw when no key exists', () => {
expect(() => clearApiKey()).not.toThrow();
});
});
describe('hasApiKey', () => {
it('should return false when no key is stored', () => {
const result = hasApiKey();
expect(result).toBe(false);
});
it('should return false when empty string is stored', () => {
localStorage.setItem(STORAGE_KEY, '');
const result = hasApiKey();
expect(result).toBe(false);
});
it('should return true when a non-empty key is stored', () => {
localStorage.setItem(STORAGE_KEY, 'AItest789');
const result = hasApiKey();
expect(result).toBe(true);
});
it('should return true for single character key', () => {
localStorage.setItem(STORAGE_KEY, 'A');
const result = hasApiKey();
expect(result).toBe(true);
});
});
describe('integration', () => {
it('should work correctly with set and get', () => {
setApiKey('AIintegration');
expect(getApiKey()).toBe('AIintegration');
expect(hasApiKey()).toBe(true);
});
it('should work correctly with set, clear, and get', () => {
setApiKey('AItest');
expect(hasApiKey()).toBe(true);
clearApiKey();
expect(getApiKey()).toBeNull();
expect(hasApiKey()).toBe(false);
});
});
});

View file

@ -2,17 +2,25 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { FileData, FormResponse } from '../../types';
import { PdfFieldInfo } from '../../services/pdfService';
// Create a mock function that can be referenced in the mock
// Mock the apiKeyService module - MUST be before geminiService import
vi.mock('../../services/apiKeyService', () => ({
getApiKey: vi.fn(() => 'AItest123'),
setApiKey: vi.fn(),
hasApiKey: vi.fn(() => true),
clearApiKey: vi.fn(),
}));
// Create mock function in module scope that will be hoisted properly
const mockGenerateContent = vi.fn();
// Mock the @google/genai module
vi.mock('@google/genai', async () => {
// Mock the @google/genai module using factory that references mockGenerateContent
vi.mock('@google/genai', () => {
return {
GoogleGenAI: vi.fn().mockImplementation(() => ({
models: {
GoogleGenAI: class MockGoogleGenAI {
models = {
generateContent: mockGenerateContent
}
})),
};
},
Type: {
OBJECT: 'OBJECT',
STRING: 'STRING',
@ -24,7 +32,7 @@ vi.mock('@google/genai', async () => {
});
// Import after mocking
const { processDocuments } = await import('../../services/geminiService');
import { processDocuments } from '../../services/geminiService';
describe('geminiService', () => {
const mockBlankForm: FileData = {

View file

@ -0,0 +1,393 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
isLatexServiceAvailable,
getAvailableTemplates,
getTemplateFieldMapping,
generateLatexPdf,
previewLatexSource,
base64ToBlob,
detectTemplate,
getExpectedFields,
} from '../../services/latexService';
import { ExtractedField } from '../../types';
// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('latexService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('isLatexServiceAvailable', () => {
it('should return true when health endpoint responds with ok', async () => {
mockFetch.mockResolvedValue({ ok: true });
const result = await isLatexServiceAvailable();
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/health'),
expect.objectContaining({ method: 'GET' })
);
});
it('should return false when health endpoint returns error', async () => {
mockFetch.mockResolvedValue({ ok: false });
const result = await isLatexServiceAvailable();
expect(result).toBe(false);
});
it('should return false when fetch throws error', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
const result = await isLatexServiceAvailable();
expect(result).toBe(false);
});
it('should use timeout signal', async () => {
mockFetch.mockResolvedValue({ ok: true });
await isLatexServiceAvailable();
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
signal: expect.any(AbortSignal),
})
);
});
});
describe('getAvailableTemplates', () => {
it('should return templates array on success', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ templates: ['G2210-11', 'S0051'] }),
});
const result = await getAvailableTemplates();
expect(result).toEqual(['G2210-11', 'S0051']);
});
it('should return empty array when response has no templates', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({}),
});
const result = await getAvailableTemplates();
expect(result).toEqual([]);
});
it('should return empty array on fetch error', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
const result = await getAvailableTemplates();
expect(result).toEqual([]);
});
it('should return empty array on non-ok response', async () => {
mockFetch.mockResolvedValue({ ok: false });
const result = await getAvailableTemplates();
expect(result).toEqual([]);
});
});
describe('getTemplateFieldMapping', () => {
it('should return mapping on success', async () => {
const mockMapping = { field1: ['alias1', 'alias2'] };
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ mapping: mockMapping }),
});
const result = await getTemplateFieldMapping('G2210-11');
expect(result).toEqual(mockMapping);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/field-mapping/G2210-11')
);
});
it('should return null when response has no mapping', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({}),
});
const result = await getTemplateFieldMapping('G2210-11');
expect(result).toBeNull();
});
it('should return null on non-ok response', async () => {
mockFetch.mockResolvedValue({ ok: false });
const result = await getTemplateFieldMapping('unknown');
expect(result).toBeNull();
});
it('should return null on fetch error', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
const result = await getTemplateFieldMapping('G2210-11');
expect(result).toBeNull();
});
});
describe('generateLatexPdf', () => {
const mockFields: ExtractedField[] = [
{ label: 'Name', value: 'John Doe', key: 'name' },
{ label: 'Date', value: '2025-01-28', key: 'date' },
];
it('should return success result with PDF on success', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
success: true,
pdf: 'base64pdfcontent',
mapped_fields: { name: 'John Doe' },
}),
});
const result = await generateLatexPdf('G2210-11', mockFields);
expect(result.success).toBe(true);
expect(result.pdf).toBe('base64pdfcontent');
expect(result.mappedFields).toEqual({ name: 'John Doe' });
});
it('should send correct request body', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
await generateLatexPdf('G2210-11', mockFields);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/generate'),
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: expect.stringContaining('"template":"G2210-11"'),
})
);
});
it('should map fields correctly in request', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true }),
});
await generateLatexPdf('G2210-11', mockFields);
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(callBody.fields).toEqual([
{ label: 'Name', value: 'John Doe', key: 'name' },
{ label: 'Date', value: '2025-01-28', key: 'date' },
]);
expect(callBody.format).toBe('base64');
});
it('should return error result on non-ok response', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.resolve({ error: 'Template not found' }),
});
const result = await generateLatexPdf('unknown', mockFields);
expect(result.success).toBe(false);
expect(result.error).toBe('Template not found');
});
it('should return error result with HTTP status when no error message', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 404,
json: () => Promise.reject(new Error('Invalid JSON')),
});
const result = await generateLatexPdf('unknown', mockFields);
expect(result.success).toBe(false);
expect(result.error).toBe('HTTP 404');
});
it('should return error result on fetch error', async () => {
mockFetch.mockRejectedValue(new Error('Network timeout'));
const result = await generateLatexPdf('G2210-11', mockFields);
expect(result.success).toBe(false);
expect(result.error).toBe('Network timeout');
});
});
describe('previewLatexSource', () => {
const mockFields: ExtractedField[] = [
{ label: 'Name', value: 'Test' },
];
it('should return latex source on success', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ latex: '\\documentclass{article}' }),
});
const result = await previewLatexSource('G2210-11', mockFields);
expect(result.latex).toBe('\\documentclass{article}');
expect(result.error).toBeUndefined();
});
it('should return error on non-ok response', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 404,
json: () => Promise.resolve({ error: 'Template not found' }),
});
const result = await previewLatexSource('unknown', mockFields);
expect(result.error).toBe('Template not found');
expect(result.latex).toBeUndefined();
});
it('should return error on fetch failure', async () => {
mockFetch.mockRejectedValue(new Error('Connection refused'));
const result = await previewLatexSource('G2210-11', mockFields);
expect(result.error).toBe('Connection refused');
});
});
describe('base64ToBlob', () => {
it('should convert base64 to Blob with correct mime type', () => {
const base64 = btoa('test content');
const result = base64ToBlob(base64, 'application/pdf');
expect(result).toBeInstanceOf(Blob);
expect(result.type).toBe('application/pdf');
});
it('should use application/pdf as default mime type', () => {
const base64 = btoa('test');
const result = base64ToBlob(base64);
expect(result.type).toBe('application/pdf');
});
it('should correctly decode base64 content', () => {
const originalContent = 'Hello, World!';
const base64 = btoa(originalContent);
const result = base64ToBlob(base64, 'text/plain');
// Verify the blob has correct size (original content length)
expect(result.size).toBe(originalContent.length);
});
it('should handle binary content', () => {
// PDF magic bytes in base64
const pdfHeader = btoa('%PDF-1.4');
const result = base64ToBlob(pdfHeader, 'application/pdf');
expect(result.size).toBeGreaterThan(0);
});
});
describe('detectTemplate', () => {
it('should detect G2210-11 from filename with g2210', () => {
expect(detectTemplate('G2210-11.pdf')).toBe('G2210-11');
expect(detectTemplate('g2210_form.pdf')).toBe('G2210-11');
expect(detectTemplate('G2210.pdf')).toBe('G2210-11');
});
it('should detect G2210-11 from filename with befundbericht', () => {
expect(detectTemplate('befundbericht.pdf')).toBe('G2210-11');
expect(detectTemplate('Aerztlicher_Befundbericht.pdf')).toBe('G2210-11');
});
it('should detect G2210-11 from filename with aerztlicher', () => {
expect(detectTemplate('aerztlicher_bericht.pdf')).toBe('G2210-11');
expect(detectTemplate('Aerztlicher_form.pdf')).toBe('G2210-11');
});
it('should detect G2210-11 from filename with ärztlicher (umlaut)', () => {
expect(detectTemplate('ärztlicher_bericht.pdf')).toBe('G2210-11');
});
it('should be case insensitive', () => {
expect(detectTemplate('G2210-11.PDF')).toBe('G2210-11');
expect(detectTemplate('BEFUNDBERICHT.pdf')).toBe('G2210-11');
expect(detectTemplate('AERZTLICHER.pdf')).toBe('G2210-11');
});
it('should return null for unrecognized filenames', () => {
expect(detectTemplate('random_form.pdf')).toBeNull();
expect(detectTemplate('document.pdf')).toBeNull();
expect(detectTemplate('scan.jpg')).toBeNull();
});
});
describe('getExpectedFields', () => {
it('should return expected fields for G2210-11 template', () => {
const fields = getExpectedFields('G2210-11');
expect(fields).toBeInstanceOf(Array);
expect(fields.length).toBeGreaterThan(0);
expect(fields).toContain('Versicherungsnummer');
expect(fields).toContain('Name, Vorname');
expect(fields).toContain('Geburtsdatum');
expect(fields).toContain('Diagnose 1');
});
it('should return empty array for unknown template', () => {
const fields = getExpectedFields('unknown');
expect(fields).toEqual([]);
});
it('should include medical fields for G2210-11', () => {
const fields = getExpectedFields('G2210-11');
expect(fields).toContain('Diagnose 1 ICD');
expect(fields).toContain('Anamnese/Beschwerden');
expect(fields).toContain('Körperlicher Befund');
});
it('should include doctor fields for G2210-11', () => {
const fields = getExpectedFields('G2210-11');
expect(fields).toContain('Arzt Name');
expect(fields).toContain('Facharztbezeichnung');
expect(fields).toContain('BSNR');
expect(fields).toContain('LANR');
});
});
});