A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
391 lines
11 KiB
TypeScript
391 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
import { BlockReviewPanel, BlockReviewSummary, type BlockReviewData } from '../BlockReviewPanel'
|
|
import type { GridData, GridCell } from '../GridOverlay'
|
|
|
|
// Mock grid data
|
|
const createMockGrid = (rows: number = 3, cols: number = 3): GridData => {
|
|
const cells: GridCell[][] = []
|
|
for (let r = 0; r < rows; r++) {
|
|
const row: GridCell[] = []
|
|
for (let c = 0; c < cols; c++) {
|
|
row.push({
|
|
row: r,
|
|
col: c,
|
|
x: (c / cols) * 100,
|
|
y: (r / rows) * 100,
|
|
width: 100 / cols,
|
|
height: 100 / rows,
|
|
text: r === 0 || c === 0 ? '' : `cell-${r}-${c}`,
|
|
confidence: 0.85,
|
|
status: r === 0 || c === 0 ? 'empty' : 'recognized',
|
|
column_type: c === 1 ? 'english' : c === 2 ? 'german' : 'unknown',
|
|
})
|
|
}
|
|
cells.push(row)
|
|
}
|
|
|
|
return {
|
|
rows,
|
|
columns: cols,
|
|
cells,
|
|
column_types: ['unknown', 'english', 'german'],
|
|
column_boundaries: [0, 33.33, 66.66, 100],
|
|
row_boundaries: [0, 33.33, 66.66, 100],
|
|
deskew_angle: 0,
|
|
stats: {
|
|
recognized: 4,
|
|
problematic: 0,
|
|
empty: 5,
|
|
total: 9,
|
|
coverage: 0.44,
|
|
},
|
|
}
|
|
}
|
|
|
|
const createMockMethodResults = () => ({
|
|
vision_llm: {
|
|
vocabulary: [
|
|
{ english: 'word1', german: 'Wort1' },
|
|
{ english: 'word2', german: 'Wort2' },
|
|
],
|
|
},
|
|
tesseract: {
|
|
vocabulary: [
|
|
{ english: 'word1', german: 'Wort1' },
|
|
{ english: 'word2', german: 'Wort2' },
|
|
],
|
|
},
|
|
})
|
|
|
|
describe('BlockReviewPanel', () => {
|
|
const mockOnBlockChange = vi.fn()
|
|
const mockOnApprove = vi.fn()
|
|
const mockOnCorrect = vi.fn()
|
|
const mockOnSkip = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should render the current block number', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('Block 5')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display progress percentage', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
5: {
|
|
blockNumber: 5,
|
|
cell: createMockGrid().cells[1][1],
|
|
methodResults: [],
|
|
status: 'approved',
|
|
correctedText: 'approved text',
|
|
},
|
|
}
|
|
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={reviewData}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('25%')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show cell position information', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('Position:')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display method results', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('Erkannte Texte:')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onSkip when skip button is clicked', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('Überspringen'))
|
|
expect(mockOnSkip).toHaveBeenCalledWith(5)
|
|
})
|
|
|
|
it('should show manual correction button', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('+ Manuell korrigieren')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show correction input when correction button is clicked', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('+ Manuell korrigieren'))
|
|
expect(screen.getByPlaceholderText('Korrekten Text eingeben...')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onCorrect when correction is submitted', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('+ Manuell korrigieren'))
|
|
const input = screen.getByPlaceholderText('Korrekten Text eingeben...')
|
|
fireEvent.change(input, { target: { value: 'corrected text' } })
|
|
fireEvent.click(screen.getByText('Übernehmen'))
|
|
|
|
expect(mockOnCorrect).toHaveBeenCalledWith(5, 'corrected text')
|
|
})
|
|
|
|
it('should show approved status when block is approved', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
5: {
|
|
blockNumber: 5,
|
|
cell: createMockGrid().cells[1][1],
|
|
methodResults: [],
|
|
status: 'approved',
|
|
correctedText: 'approved text',
|
|
},
|
|
}
|
|
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={reviewData}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText(/Freigegeben:/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should disable previous button on first block', () => {
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={createMockGrid()}
|
|
methodResults={createMockMethodResults()}
|
|
currentBlockNumber={5}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
const prevButton = screen.getByText('Zurück').closest('button')
|
|
expect(prevButton).toBeDisabled()
|
|
})
|
|
|
|
it('should show empty message when no blocks available', () => {
|
|
const emptyGrid: GridData = {
|
|
...createMockGrid(),
|
|
cells: [[{
|
|
row: 0,
|
|
col: 0,
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
text: '',
|
|
confidence: 0,
|
|
status: 'empty'
|
|
}]],
|
|
}
|
|
|
|
render(
|
|
<BlockReviewPanel
|
|
grid={emptyGrid}
|
|
methodResults={{}}
|
|
currentBlockNumber={1}
|
|
onBlockChange={mockOnBlockChange}
|
|
onApprove={mockOnApprove}
|
|
onCorrect={mockOnCorrect}
|
|
onSkip={mockOnSkip}
|
|
reviewData={{}}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('Keine Blöcke zum Überprüfen')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('BlockReviewSummary', () => {
|
|
const mockOnBlockClick = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should display summary statistics', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
1: { blockNumber: 1, cell: {} as GridCell, methodResults: [], status: 'approved', correctedText: 'text1' },
|
|
2: { blockNumber: 2, cell: {} as GridCell, methodResults: [], status: 'corrected', correctedText: 'text2' },
|
|
3: { blockNumber: 3, cell: {} as GridCell, methodResults: [], status: 'skipped' },
|
|
}
|
|
|
|
render(
|
|
<BlockReviewSummary
|
|
reviewData={reviewData}
|
|
totalBlocks={5}
|
|
onBlockClick={mockOnBlockClick}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText('Überprüfungsübersicht')).toBeInTheDocument()
|
|
expect(screen.getByText('1')).toBeInTheDocument() // approved count
|
|
expect(screen.getByText('Freigegeben')).toBeInTheDocument()
|
|
expect(screen.getByText('Korrigiert')).toBeInTheDocument()
|
|
expect(screen.getByText('Übersprungen')).toBeInTheDocument()
|
|
expect(screen.getByText('Ausstehend')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onBlockClick when a block is clicked', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
5: { blockNumber: 5, cell: {} as GridCell, methodResults: [], status: 'approved', correctedText: 'text' },
|
|
}
|
|
|
|
render(
|
|
<BlockReviewSummary
|
|
reviewData={reviewData}
|
|
totalBlocks={10}
|
|
onBlockClick={mockOnBlockClick}
|
|
/>
|
|
)
|
|
|
|
fireEvent.click(screen.getByText('Block 5'))
|
|
expect(mockOnBlockClick).toHaveBeenCalledWith(5)
|
|
})
|
|
|
|
it('should show correct counts for each status', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
1: { blockNumber: 1, cell: {} as GridCell, methodResults: [], status: 'approved', correctedText: 't1' },
|
|
2: { blockNumber: 2, cell: {} as GridCell, methodResults: [], status: 'approved', correctedText: 't2' },
|
|
3: { blockNumber: 3, cell: {} as GridCell, methodResults: [], status: 'corrected', correctedText: 't3' },
|
|
}
|
|
|
|
render(
|
|
<BlockReviewSummary
|
|
reviewData={reviewData}
|
|
totalBlocks={5}
|
|
onBlockClick={mockOnBlockClick}
|
|
/>
|
|
)
|
|
|
|
// 2 approved, 1 corrected, 0 skipped, 2 pending
|
|
const approvedCount = screen.getAllByText('2')[0]
|
|
expect(approvedCount).toBeInTheDocument()
|
|
})
|
|
|
|
it('should truncate long corrected text', () => {
|
|
const reviewData: Record<number, BlockReviewData> = {
|
|
1: {
|
|
blockNumber: 1,
|
|
cell: {} as GridCell,
|
|
methodResults: [],
|
|
status: 'approved',
|
|
correctedText: 'This is a very long text that should be truncated'
|
|
},
|
|
}
|
|
|
|
render(
|
|
<BlockReviewSummary
|
|
reviewData={reviewData}
|
|
totalBlocks={1}
|
|
onBlockClick={mockOnBlockClick}
|
|
/>
|
|
)
|
|
|
|
expect(screen.getByText(/This is a very long t\.\.\./)).toBeInTheDocument()
|
|
})
|
|
})
|