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:
parent
1eb00f3d64
commit
04fe925891
7 changed files with 2158 additions and 8 deletions
125
tests/services/apiKeyService.test.ts
Normal file
125
tests/services/apiKeyService.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
393
tests/services/latexService.test.ts
Normal file
393
tests/services/latexService.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue