226 lines
7.4 KiB
JavaScript
226 lines
7.4 KiB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
import { createAnalysis } from './api.js'
|
|
import { useAnalysisStore } from './store.js'
|
|
|
|
vi.mock('../../shared/api/http.js', () => ({
|
|
apiFetch: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('./api.js', () => ({
|
|
fetchAnalyses: vi.fn(),
|
|
fetchAnalysis: vi.fn(),
|
|
createAnalysis: vi.fn(),
|
|
deleteAnalysis: vi.fn(),
|
|
}))
|
|
|
|
import { apiFetch } from '../../shared/api/http.js'
|
|
import * as api from './api.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// API layer — body construction with pipeline options
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('createAnalysis — pipeline options body construction', () => {
|
|
// For these tests we need the REAL createAnalysis, not the mock.
|
|
// So we re-import the actual module.
|
|
let realCreateAnalysis
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
// Reset the real module to get the unmocked version
|
|
vi.doUnmock('./api.js')
|
|
})
|
|
|
|
// 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.js')
|
|
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
|
|
|
|
const { createAnalysis: real } = await import('./api.js')
|
|
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.js')
|
|
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
|
|
|
|
const { createAnalysis: real } = await import('./api.js')
|
|
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.js')
|
|
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.js')
|
|
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.js')
|
|
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
|
|
|
|
const opts = { do_ocr: false }
|
|
|
|
const { createAnalysis: real } = await import('./api.js')
|
|
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.js')
|
|
realApiFetch.mockResolvedValue({ id: '1', status: 'PENDING' })
|
|
|
|
// Empty object is truthy in JS, so it SHOULD be included
|
|
const { createAnalysis: real } = await import('./api.js')
|
|
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)
|
|
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)
|
|
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 })
|
|
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()
|
|
})
|
|
})
|