Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
467 lines
12 KiB
TypeScript
467 lines
12 KiB
TypeScript
/**
|
|
* Tests for OCR Integration Utility
|
|
*
|
|
* Tests cover:
|
|
* - mm to pixel conversion
|
|
* - OCR data export format
|
|
* - LocalStorage operations
|
|
* - Canvas integration
|
|
*/
|
|
|
|
import {
|
|
MM_TO_PX,
|
|
A4_WIDTH_MM,
|
|
A4_HEIGHT_MM,
|
|
A4_WIDTH_PX,
|
|
A4_HEIGHT_PX,
|
|
mmToPixel,
|
|
pixelToMm,
|
|
getColumnColor,
|
|
createTextProps,
|
|
exportOCRData,
|
|
saveOCRExportToStorage,
|
|
loadLatestOCRExport,
|
|
loadOCRExport,
|
|
clearOCRExports,
|
|
type OCRWord,
|
|
type OCRExportData,
|
|
type ColumnType,
|
|
} from './ocr-integration'
|
|
|
|
// Mock localStorage
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {}
|
|
return {
|
|
getItem: jest.fn((key: string) => store[key] || null),
|
|
setItem: jest.fn((key: string, value: string) => {
|
|
store[key] = value
|
|
}),
|
|
removeItem: jest.fn((key: string) => {
|
|
delete store[key]
|
|
}),
|
|
clear: jest.fn(() => {
|
|
store = {}
|
|
}),
|
|
keys: () => Object.keys(store),
|
|
}
|
|
})()
|
|
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
|
|
|
describe('Constants', () => {
|
|
test('MM_TO_PX is correct for 96 DPI', () => {
|
|
// 1 inch = 25.4mm, 96 DPI = 96 pixels per inch
|
|
// 96 / 25.4 = 3.7795275591
|
|
expect(MM_TO_PX).toBeCloseTo(3.7795275591, 8)
|
|
})
|
|
|
|
test('A4 dimensions in mm are correct', () => {
|
|
expect(A4_WIDTH_MM).toBe(210)
|
|
expect(A4_HEIGHT_MM).toBe(297)
|
|
})
|
|
|
|
test('A4 dimensions in pixels are calculated correctly', () => {
|
|
expect(A4_WIDTH_PX).toBe(Math.round(210 * MM_TO_PX)) // ~794
|
|
expect(A4_HEIGHT_PX).toBe(Math.round(297 * MM_TO_PX)) // ~1123
|
|
})
|
|
})
|
|
|
|
describe('mmToPixel', () => {
|
|
test('converts 0mm to 0px', () => {
|
|
expect(mmToPixel(0)).toBe(0)
|
|
})
|
|
|
|
test('converts 1mm correctly', () => {
|
|
expect(mmToPixel(1)).toBeCloseTo(3.7795275591, 8)
|
|
})
|
|
|
|
test('converts 100mm correctly', () => {
|
|
expect(mmToPixel(100)).toBeCloseTo(377.95275591, 6)
|
|
})
|
|
|
|
test('converts A4 width correctly', () => {
|
|
expect(mmToPixel(210)).toBeCloseTo(793.7, 1)
|
|
})
|
|
})
|
|
|
|
describe('pixelToMm', () => {
|
|
test('converts 0px to 0mm', () => {
|
|
expect(pixelToMm(0)).toBe(0)
|
|
})
|
|
|
|
test('converts 100px correctly', () => {
|
|
expect(pixelToMm(100)).toBeCloseTo(26.458, 2)
|
|
})
|
|
|
|
test('round-trip conversion is accurate', () => {
|
|
const original = 50
|
|
const pixels = mmToPixel(original)
|
|
const backToMm = pixelToMm(pixels)
|
|
expect(backToMm).toBeCloseTo(original, 8)
|
|
})
|
|
})
|
|
|
|
describe('getColumnColor', () => {
|
|
test('returns blue for english column', () => {
|
|
expect(getColumnColor('english')).toBe('#1e40af')
|
|
})
|
|
|
|
test('returns green for german column', () => {
|
|
expect(getColumnColor('german')).toBe('#166534')
|
|
})
|
|
|
|
test('returns purple for example column', () => {
|
|
expect(getColumnColor('example')).toBe('#6b21a8')
|
|
})
|
|
|
|
test('returns gray for unknown column', () => {
|
|
expect(getColumnColor('unknown')).toBe('#374151')
|
|
})
|
|
|
|
test('uses custom colors from options', () => {
|
|
const options = { englishColor: '#ff0000' }
|
|
expect(getColumnColor('english', options)).toBe('#ff0000')
|
|
})
|
|
})
|
|
|
|
describe('createTextProps', () => {
|
|
const mockWord: OCRWord = {
|
|
text: 'house',
|
|
x_mm: 21.0,
|
|
y_mm: 44.55,
|
|
width_mm: 52.5,
|
|
height_mm: 8.91,
|
|
column_type: 'english',
|
|
logical_row: 0,
|
|
}
|
|
|
|
test('creates correct type', () => {
|
|
const props = createTextProps(mockWord)
|
|
expect(props.type).toBe('i-text')
|
|
})
|
|
|
|
test('converts mm to pixels for left position', () => {
|
|
const props = createTextProps(mockWord)
|
|
expect(props.left).toBeCloseTo(21.0 * MM_TO_PX, 2)
|
|
})
|
|
|
|
test('converts mm to pixels for top position', () => {
|
|
const props = createTextProps(mockWord)
|
|
expect(props.top).toBeCloseTo(44.55 * MM_TO_PX, 2)
|
|
})
|
|
|
|
test('applies offset correctly', () => {
|
|
const props = createTextProps(mockWord, { offsetX: 5, offsetY: 10 })
|
|
expect(props.left).toBeCloseTo((21.0 + 5) * MM_TO_PX, 2)
|
|
expect(props.top).toBeCloseTo((44.55 + 10) * MM_TO_PX, 2)
|
|
})
|
|
|
|
test('sets fill color based on column type', () => {
|
|
const props = createTextProps(mockWord)
|
|
expect(props.fill).toBe('#1e40af') // English blue
|
|
})
|
|
|
|
test('includes OCR metadata', () => {
|
|
const props = createTextProps(mockWord)
|
|
expect(props.ocrMetadata).toBeDefined()
|
|
expect((props.ocrMetadata as any).x_mm).toBe(21.0)
|
|
expect((props.ocrMetadata as any).column_type).toBe('english')
|
|
expect((props.ocrMetadata as any).logical_row).toBe(0)
|
|
})
|
|
|
|
test('uses custom font family', () => {
|
|
const props = createTextProps(mockWord, { fontFamily: 'Times New Roman' })
|
|
expect(props.fontFamily).toBe('Times New Roman')
|
|
})
|
|
|
|
test('uses custom font size', () => {
|
|
const props = createTextProps(mockWord, { fontSize: 16 })
|
|
expect(props.fontSize).toBe(16)
|
|
})
|
|
})
|
|
|
|
describe('exportOCRData', () => {
|
|
const mockGridData = {
|
|
cells: [
|
|
[
|
|
{
|
|
text: 'house',
|
|
x_mm: 21.0,
|
|
y_mm: 44.55,
|
|
width_mm: 52.5,
|
|
height_mm: 8.91,
|
|
column_type: 'english' as ColumnType,
|
|
logical_row: 0,
|
|
status: 'recognized',
|
|
},
|
|
{
|
|
text: 'Haus',
|
|
x_mm: 80.0,
|
|
y_mm: 44.55,
|
|
width_mm: 40.0,
|
|
height_mm: 8.91,
|
|
column_type: 'german' as ColumnType,
|
|
logical_row: 0,
|
|
status: 'recognized',
|
|
},
|
|
],
|
|
],
|
|
detected_columns: [
|
|
{ column_type: 'english', x_start_mm: 20.0, x_end_mm: 73.5 },
|
|
{ column_type: 'german', x_start_mm: 74.0, x_end_mm: 140.0 },
|
|
],
|
|
page_dimensions: {
|
|
width_mm: 210,
|
|
height_mm: 297,
|
|
format: 'A4',
|
|
},
|
|
}
|
|
|
|
test('creates correct version', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.version).toBe('1.0')
|
|
})
|
|
|
|
test('sets correct source', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.source).toBe('ocr-compare')
|
|
})
|
|
|
|
test('includes session ID and page number', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.session_id).toBe('session-123')
|
|
expect(result.page_number).toBe(1)
|
|
})
|
|
|
|
test('includes page dimensions', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.page_dimensions.width_mm).toBe(210)
|
|
expect(result.page_dimensions.height_mm).toBe(297)
|
|
expect(result.page_dimensions.format).toBe('A4')
|
|
})
|
|
|
|
test('converts cells to words', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.words).toHaveLength(2)
|
|
expect(result.words[0].text).toBe('house')
|
|
expect(result.words[0].column_type).toBe('english')
|
|
})
|
|
|
|
test('filters empty cells', () => {
|
|
const dataWithEmpty = {
|
|
...mockGridData,
|
|
cells: [
|
|
[
|
|
...mockGridData.cells[0],
|
|
{ text: '', status: 'empty' }, // Empty cell
|
|
],
|
|
],
|
|
}
|
|
const result = exportOCRData(dataWithEmpty, 'session-123', 1)
|
|
expect(result.words).toHaveLength(2) // Empty cell excluded
|
|
})
|
|
|
|
test('includes detected columns', () => {
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
expect(result.detected_columns).toHaveLength(2)
|
|
expect(result.detected_columns[0].column_type).toBe('english')
|
|
})
|
|
|
|
test('sets exported_at timestamp', () => {
|
|
const before = new Date().toISOString()
|
|
const result = exportOCRData(mockGridData, 'session-123', 1)
|
|
const after = new Date().toISOString()
|
|
|
|
expect(result.exported_at >= before).toBe(true)
|
|
expect(result.exported_at <= after).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('localStorage operations', () => {
|
|
beforeEach(() => {
|
|
localStorageMock.clear()
|
|
})
|
|
|
|
const mockExportData: OCRExportData = {
|
|
version: '1.0',
|
|
source: 'ocr-compare',
|
|
exported_at: '2026-02-08T12:00:00Z',
|
|
session_id: 'session-123',
|
|
page_number: 1,
|
|
page_dimensions: {
|
|
width_mm: 210,
|
|
height_mm: 297,
|
|
format: 'A4',
|
|
},
|
|
words: [
|
|
{
|
|
text: 'house',
|
|
x_mm: 21.0,
|
|
y_mm: 44.55,
|
|
width_mm: 52.5,
|
|
height_mm: 8.91,
|
|
column_type: 'english',
|
|
logical_row: 0,
|
|
},
|
|
],
|
|
detected_columns: [],
|
|
}
|
|
|
|
describe('saveOCRExportToStorage', () => {
|
|
test('saves data to localStorage', () => {
|
|
saveOCRExportToStorage(mockExportData)
|
|
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
|
'ocr_export_session-123_1',
|
|
expect.any(String)
|
|
)
|
|
})
|
|
|
|
test('sets latest export key', () => {
|
|
saveOCRExportToStorage(mockExportData)
|
|
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
|
'ocr_export_latest',
|
|
'ocr_export_session-123_1'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('loadLatestOCRExport', () => {
|
|
test('returns null when no export exists', () => {
|
|
const result = loadLatestOCRExport()
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test('loads latest export data', () => {
|
|
// Manually set up the mock
|
|
localStorageMock.setItem(
|
|
'ocr_export_session-123_1',
|
|
JSON.stringify(mockExportData)
|
|
)
|
|
localStorageMock.setItem('ocr_export_latest', 'ocr_export_session-123_1')
|
|
|
|
// Reset the mock to return correct values
|
|
localStorageMock.getItem.mockImplementation((key: string) => {
|
|
if (key === 'ocr_export_latest') return 'ocr_export_session-123_1'
|
|
if (key === 'ocr_export_session-123_1')
|
|
return JSON.stringify(mockExportData)
|
|
return null
|
|
})
|
|
|
|
const result = loadLatestOCRExport()
|
|
expect(result).not.toBeNull()
|
|
expect(result?.session_id).toBe('session-123')
|
|
})
|
|
})
|
|
|
|
describe('loadOCRExport', () => {
|
|
test('returns null for non-existent session', () => {
|
|
const result = loadOCRExport('nonexistent', 1)
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test('loads specific export by session and page', () => {
|
|
localStorageMock.getItem.mockImplementation((key: string) => {
|
|
if (key === 'ocr_export_session-123_1')
|
|
return JSON.stringify(mockExportData)
|
|
return null
|
|
})
|
|
|
|
const result = loadOCRExport('session-123', 1)
|
|
expect(result).not.toBeNull()
|
|
expect(result?.page_number).toBe(1)
|
|
})
|
|
|
|
test('handles JSON parse errors gracefully', () => {
|
|
localStorageMock.getItem.mockImplementation((key: string) => {
|
|
if (key === 'ocr_export_session-123_1') return 'invalid json'
|
|
return null
|
|
})
|
|
|
|
const result = loadOCRExport('session-123', 1)
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('clearOCRExports', () => {
|
|
test('removes all OCR export keys', () => {
|
|
// Set up mock to return keys
|
|
Object.defineProperty(localStorageMock, 'keys', {
|
|
value: () => [
|
|
'ocr_export_session-1_1',
|
|
'ocr_export_session-2_1',
|
|
'ocr_export_latest',
|
|
'other_key',
|
|
],
|
|
})
|
|
|
|
// Mock Object.keys(localStorage)
|
|
const originalKeys = Object.keys
|
|
Object.keys = jest.fn((obj) => {
|
|
if (obj === localStorage) {
|
|
return [
|
|
'ocr_export_session-1_1',
|
|
'ocr_export_session-2_1',
|
|
'ocr_export_latest',
|
|
'other_key',
|
|
]
|
|
}
|
|
return originalKeys(obj)
|
|
})
|
|
|
|
clearOCRExports()
|
|
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'ocr_export_session-1_1'
|
|
)
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'ocr_export_session-2_1'
|
|
)
|
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith(
|
|
'ocr_export_latest'
|
|
)
|
|
|
|
// Restore Object.keys
|
|
Object.keys = originalKeys
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
test('handles negative mm values', () => {
|
|
const pixels = mmToPixel(-10)
|
|
expect(pixels).toBeCloseTo(-37.795, 2)
|
|
})
|
|
|
|
test('handles very large mm values', () => {
|
|
const pixels = mmToPixel(10000)
|
|
expect(pixels).toBeCloseTo(37795.275591, 2)
|
|
})
|
|
|
|
test('handles word with missing optional fields', () => {
|
|
const word: OCRWord = {
|
|
text: 'test',
|
|
x_mm: 0,
|
|
y_mm: 0,
|
|
width_mm: 10,
|
|
height_mm: 5,
|
|
column_type: 'unknown',
|
|
logical_row: 0,
|
|
}
|
|
const props = createTextProps(word)
|
|
expect(props).toBeDefined()
|
|
expect(props.text).toBe('test')
|
|
})
|
|
|
|
test('handles empty words array in export', () => {
|
|
const gridData = {
|
|
cells: [],
|
|
detected_columns: [],
|
|
page_dimensions: { width_mm: 210, height_mm: 297, format: 'A4' },
|
|
}
|
|
const result = exportOCRData(gridData, 'session', 1)
|
|
expect(result.words).toHaveLength(0)
|
|
})
|
|
})
|