docling-studio / frontend /src /features /analysis /pipelineOptions.test.ts
Pier-Jean's picture
Initial deploy: Docling Studio (local mode, port 7860)
5539271
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAnalysisStore } from './store'
vi.mock('../../shared/api/http', () => ({
apiFetch: vi.fn(),
}))
vi.mock('./api', () => ({
fetchAnalyses: vi.fn(),
fetchAnalysis: vi.fn(),
createAnalysis: vi.fn(),
deleteAnalysis: vi.fn(),
}))
import * as api from './api'
// ---------------------------------------------------------------------------
// API layer — body construction with pipeline options
// ---------------------------------------------------------------------------
describe('createAnalysis — pipeline options body construction', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset the real module to get the unmocked version
vi.doUnmock('./api')
})
// We test the mock integration (store → api) separately below.
// Here we verify the raw apiFetch call shape.
it('sends body without pipelineOptions when null', async () => {
const { apiFetch: realApiFetch } = await import('../../shared/api/http')
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
const { createAnalysis: real } = await import('./api')
await real('doc-1', null)
expect(realApiFetch).toHaveBeenCalledWith('/api/analyses', {
method: 'POST',
body: JSON.stringify({ documentId: 'doc-1' }),
})
})
it('sends body without pipelineOptions when undefined', async () => {
const { apiFetch: realApiFetch } = await import('../../shared/api/http')
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
const { createAnalysis: real } = await import('./api')
await real('doc-1')
expect(realApiFetch).toHaveBeenCalledWith('/api/analyses', {
method: 'POST',
body: JSON.stringify({ documentId: 'doc-1' }),
})
})
it('includes pipelineOptions in body when provided', async () => {
const { apiFetch: realApiFetch } = await import('../../shared/api/http')
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
const opts = {
do_ocr: false,
do_table_structure: true,
table_mode: 'fast',
do_code_enrichment: true,
do_formula_enrichment: false,
do_picture_classification: false,
do_picture_description: false,
generate_picture_images: false,
generate_page_images: false,
images_scale: 1.0,
}
const { createAnalysis: real } = await import('./api')
await real('doc-1', opts)
const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body)
expect(sentBody.documentId).toBe('doc-1')
expect(sentBody.pipelineOptions).toEqual(opts)
})
it('includes only specified fields in partial options', async () => {
const { apiFetch: realApiFetch } = await import('../../shared/api/http')
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
const opts = { do_ocr: false }
const { createAnalysis: real } = await import('./api')
await real('doc-1', opts)
const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body)
expect(sentBody.pipelineOptions).toEqual({ do_ocr: false })
})
it('does not include pipelineOptions key when options is empty object treated as falsy', async () => {
const { apiFetch: realApiFetch } = await import('../../shared/api/http')
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
// Empty object is truthy in JS, so it SHOULD be included
const { createAnalysis: real } = await import('./api')
await real('doc-1', {})
const sentBody = JSON.parse(realApiFetch.mock.calls[0][1].body)
expect(sentBody.pipelineOptions).toEqual({})
})
})
// ---------------------------------------------------------------------------
// Store → API integration — pipeline options forwarding
// ---------------------------------------------------------------------------
describe('useAnalysisStore — pipeline options forwarding', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('run() passes null when no options provided', async () => {
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
api.createAnalysis.mockResolvedValue(job)
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' })
const store = useAnalysisStore()
await store.run('d1')
expect(api.createAnalysis).toHaveBeenCalledWith('d1', null, null)
store.stopPolling()
})
it('run() forwards full pipeline options object', async () => {
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
api.createAnalysis.mockResolvedValue(job)
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' })
const store = useAnalysisStore()
const opts = {
do_ocr: false,
do_table_structure: true,
table_mode: 'fast',
do_code_enrichment: true,
do_formula_enrichment: false,
do_picture_classification: false,
do_picture_description: true,
generate_picture_images: true,
generate_page_images: false,
images_scale: 2.0,
}
await store.run('d1', opts)
expect(api.createAnalysis).toHaveBeenCalledWith('d1', opts, null)
store.stopPolling()
})
it('run() forwards partial pipeline options', async () => {
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
api.createAnalysis.mockResolvedValue(job)
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' })
const store = useAnalysisStore()
const opts = { do_ocr: false }
await store.run('d1', opts)
expect(api.createAnalysis).toHaveBeenCalledWith('d1', { do_ocr: false }, null)
store.stopPolling()
})
it('run() sets running state even with pipeline options', async () => {
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
api.createAnalysis.mockResolvedValue(job)
const store = useAnalysisStore()
const opts = { do_ocr: false, table_mode: 'fast' }
await store.run('d1', opts)
expect(store.running).toBe(true)
expect(store.currentAnalysis).toEqual(job)
store.stopPolling()
})
it('run() with options still triggers polling', async () => {
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
api.createAnalysis.mockResolvedValue(job)
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'RUNNING' })
const store = useAnalysisStore()
await store.run('d1', { do_ocr: false })
// After 2s polling should fire
await vi.advanceTimersByTimeAsync(2000)
expect(api.fetchAnalysis).toHaveBeenCalledWith('j1')
// Still running
expect(store.running).toBe(true)
// Now complete
api.fetchAnalysis.mockResolvedValue({ ...job, status: 'COMPLETED' })
await vi.advanceTimersByTimeAsync(2000)
expect(store.running).toBe(false)
store.stopPolling()
})
it('run() with options handles API error gracefully', async () => {
api.createAnalysis.mockRejectedValue(new Error('Server error'))
vi.spyOn(console, 'error').mockImplementation(() => {})
const store = useAnalysisStore()
await expect(store.run('d1', { do_ocr: false })).rejects.toThrow('Server error')
expect(store.running).toBe(false)
expect(store.currentAnalysis).toBeNull()
})
})