diff --git a/document-parser/data/docling_studio.db b/document-parser/data/docling_studio.db index 7807faf..3b36468 100644 Binary files a/document-parser/data/docling_studio.db and b/document-parser/data/docling_studio.db differ diff --git a/frontend/src/features/history/navigation.test.js b/frontend/src/features/history/navigation.test.js new file mode 100644 index 0000000..18bbca1 --- /dev/null +++ b/frontend/src/features/history/navigation.test.js @@ -0,0 +1,195 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' + +// Mock vue-router +const mockPush = vi.fn() +vi.mock('vue-router', () => ({ + useRouter: () => ({ push: mockPush }), +})) + +vi.mock('./api.js', () => ({ + fetchAnalyses: vi.fn(), + deleteAnalysis: vi.fn(), +})) + +vi.mock('../analysis/api.js', () => ({ + fetchAnalyses: vi.fn(), + fetchAnalysis: vi.fn(), + createAnalysis: vi.fn(), + deleteAnalysis: vi.fn(), +})) + +vi.mock('../document/api.js', () => ({ + fetchDocuments: vi.fn(), + uploadDocument: vi.fn(), + deleteDocument: vi.fn(), + getPreviewUrl: vi.fn(), +})) + +import { useHistoryStore } from './store.js' +import { useAnalysisStore } from '../analysis/store.js' +import { useDocumentStore } from '../document/store.js' + +describe('History → Studio navigation', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + describe('History store provides data for navigation', () => { + it('analyses contain documentId for document selection', async () => { + const { fetchAnalyses } = await import('./api.js') + fetchAnalyses.mockResolvedValue([ + { id: 'a1', documentId: 'd1', documentFilename: 'test.pdf', status: 'COMPLETED' }, + { id: 'a2', documentId: 'd2', documentFilename: 'other.pdf', status: 'FAILED' }, + ]) + + const store = useHistoryStore() + await store.load() + + expect(store.analyses[0].documentId).toBe('d1') + expect(store.analyses[1].documentId).toBe('d2') + }) + }) + + describe('Analysis store select() restores analysis state', () => { + it('select() sets currentAnalysis from fetched data', async () => { + const { fetchAnalysis } = await import('../analysis/api.js') + const analysis = { + id: 'a1', + documentId: 'd1', + status: 'COMPLETED', + contentMarkdown: '# Hello', + pagesJson: '[{"page_number":1}]', + } + fetchAnalysis.mockResolvedValue(analysis) + + const store = useAnalysisStore() + await store.select('a1') + + expect(store.currentAnalysis).toEqual(analysis) + expect(store.currentAnalysis.documentId).toBe('d1') + }) + + it('select() allows document store to select the associated document', async () => { + const { fetchAnalysis } = await import('../analysis/api.js') + fetchAnalysis.mockResolvedValue({ + id: 'a1', + documentId: 'd1', + status: 'COMPLETED', + }) + + const analysisStore = useAnalysisStore() + const docStore = useDocumentStore() + docStore.documents = [ + { id: 'd1', filename: 'test.pdf' }, + { id: 'd2', filename: 'other.pdf' }, + ] + + await analysisStore.select('a1') + docStore.select(analysisStore.currentAnalysis.documentId) + + expect(docStore.selectedId).toBe('d1') + }) + }) + + describe('Navigation intent from history items', () => { + it('completed analysis navigates to studio with analysisId query', () => { + // Simulates what HistoryList.openAnalysis does + const analysis = { id: 'a1', documentId: 'd1', status: 'COMPLETED' } + + mockPush({ name: 'studio', query: { analysisId: analysis.id } }) + + expect(mockPush).toHaveBeenCalledWith({ + name: 'studio', + query: { analysisId: 'a1' }, + }) + }) + + it('pending analysis navigates with same pattern', () => { + const analysis = { id: 'a2', documentId: 'd2', status: 'PENDING' } + + mockPush({ name: 'studio', query: { analysisId: analysis.id } }) + + expect(mockPush).toHaveBeenCalledWith({ + name: 'studio', + query: { analysisId: 'a2' }, + }) + }) + + it('failed analysis navigates with same pattern', () => { + const analysis = { id: 'a3', documentId: 'd3', status: 'FAILED' } + + mockPush({ name: 'studio', query: { analysisId: analysis.id } }) + + expect(mockPush).toHaveBeenCalledWith({ + name: 'studio', + query: { analysisId: 'a3' }, + }) + }) + }) + + describe('Full restore flow (store-level integration)', () => { + it('restores completed analysis: selects analysis + document + verifier mode', async () => { + const { fetchAnalysis } = await import('../analysis/api.js') + fetchAnalysis.mockResolvedValue({ + id: 'a1', + documentId: 'd1', + status: 'COMPLETED', + contentMarkdown: '# Result', + pagesJson: '[{"page_number":1,"width":612,"height":792}]', + }) + + const analysisStore = useAnalysisStore() + const docStore = useDocumentStore() + docStore.documents = [{ id: 'd1', filename: 'test.pdf' }] + + // Simulate what StudioPage.onMounted does with ?analysisId=a1 + await analysisStore.select('a1') + const analysis = analysisStore.currentAnalysis + expect(analysis).not.toBeNull() + + docStore.select(analysis.documentId) + expect(docStore.selectedId).toBe('d1') + + // Mode would be set to 'verifier' since status is COMPLETED + const mode = analysis.status === 'COMPLETED' ? 'verifier' : 'configurer' + expect(mode).toBe('verifier') + }) + + it('restores failed analysis: selects analysis + document, stays in configurer mode', async () => { + const { fetchAnalysis } = await import('../analysis/api.js') + fetchAnalysis.mockResolvedValue({ + id: 'a2', + documentId: 'd2', + status: 'FAILED', + errorMessage: 'parse error', + }) + + const analysisStore = useAnalysisStore() + const docStore = useDocumentStore() + docStore.documents = [{ id: 'd2', filename: 'broken.pdf' }] + + await analysisStore.select('a2') + const analysis = analysisStore.currentAnalysis + + docStore.select(analysis.documentId) + expect(docStore.selectedId).toBe('d2') + + const mode = analysis.status === 'COMPLETED' ? 'verifier' : 'configurer' + expect(mode).toBe('configurer') + }) + + it('handles missing analysis gracefully', async () => { + const { fetchAnalysis } = await import('../analysis/api.js') + fetchAnalysis.mockRejectedValue(new Error('Not found')) + vi.spyOn(console, 'error').mockImplementation(() => {}) + + const analysisStore = useAnalysisStore() + await analysisStore.select('nonexistent') + + // currentAnalysis stays null on error — StudioPage would not proceed + expect(analysisStore.currentAnalysis).toBeNull() + }) + }) +}) diff --git a/frontend/src/shared/i18n.test.js b/frontend/src/shared/i18n.test.js index 97c1426..dcf24b2 100644 --- a/frontend/src/shared/i18n.test.js +++ b/frontend/src/shared/i18n.test.js @@ -59,4 +59,51 @@ describe('useI18n', () => { const { t } = useI18n() expect(t('results.pageOf', { current: 1, total: 5 })).toBe('Page 1 sur 5') }) + + it('has history tab keys in French', () => { + useSettingsStore.mockReturnValue({ locale: 'fr' }) + + const { t } = useI18n() + expect(t('history.tabAnalyses')).toBe('Analyses') + expect(t('history.tabDocuments')).toBe('Documents') + expect(t('history.emptyDocs')).toBe('Aucun document. Importez un document depuis le Studio.') + }) + + it('has history tab keys in English', () => { + useSettingsStore.mockReturnValue({ locale: 'en' }) + + const { t } = useI18n() + expect(t('history.tabAnalyses')).toBe('Analyses') + expect(t('history.tabDocuments')).toBe('Documents') + expect(t('history.emptyDocs')).toBe('No documents yet. Upload a document from the Studio.') + }) + + it('has detailed pipeline option hints in French', () => { + useSettingsStore.mockReturnValue({ locale: 'fr' }) + + const { t } = useI18n() + // Hints should be longer than simple labels + expect(t('config.ocrHint').length).toBeGreaterThan(40) + expect(t('config.tableStructureHint').length).toBeGreaterThan(40) + expect(t('config.codeEnrichmentHint').length).toBeGreaterThan(40) + expect(t('config.formulaEnrichmentHint').length).toBeGreaterThan(40) + expect(t('config.pictureClassificationHint').length).toBeGreaterThan(40) + expect(t('config.pictureDescriptionHint').length).toBeGreaterThan(40) + expect(t('config.generatePictureImagesHint').length).toBeGreaterThan(40) + expect(t('config.generatePageImagesHint').length).toBeGreaterThan(40) + }) + + it('has detailed pipeline option hints in English', () => { + useSettingsStore.mockReturnValue({ locale: 'en' }) + + const { t } = useI18n() + expect(t('config.ocrHint').length).toBeGreaterThan(40) + expect(t('config.tableStructureHint').length).toBeGreaterThan(40) + expect(t('config.codeEnrichmentHint').length).toBeGreaterThan(40) + expect(t('config.formulaEnrichmentHint').length).toBeGreaterThan(40) + expect(t('config.pictureClassificationHint').length).toBeGreaterThan(40) + expect(t('config.pictureDescriptionHint').length).toBeGreaterThan(40) + expect(t('config.generatePictureImagesHint').length).toBeGreaterThan(40) + expect(t('config.generatePageImagesHint').length).toBeGreaterThan(40) + }) })