diff --git a/document-parser/data/docling_studio.db b/document-parser/data/docling_studio.db index bdb74be..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/features/history/ui/HistoryList.vue b/frontend/src/features/history/ui/HistoryList.vue index cd49c10..f468a41 100644 --- a/frontend/src/features/history/ui/HistoryList.vue +++ b/frontend/src/features/history/ui/HistoryList.vue @@ -8,6 +8,7 @@ v-for="analysis in store.analyses" :key="analysis.id" class="history-item" + @click="openAnalysis(analysis)" >
@@ -21,7 +22,7 @@ — {{ duration(analysis) }}
- @@ -30,12 +31,18 @@ diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue index cbcf370..729d6fa 100644 --- a/frontend/src/pages/StudioPage.vue +++ b/frontend/src/pages/StudioPage.vue @@ -269,6 +269,7 @@ diff --git a/frontend/src/shared/i18n.js b/frontend/src/shared/i18n.js index 3d96ce2..4f6350b 100644 --- a/frontend/src/shared/i18n.js +++ b/frontend/src/shared/i18n.js @@ -67,8 +67,12 @@ const messages = { 'upload.maxSize': 'Max 50Mo', // History - 'history.title': 'Historique des analyses', + 'history.title': 'Historique', + 'history.tabAnalyses': 'Analyses', + 'history.tabDocuments': 'Documents', 'history.empty': 'Aucune analyse. Allez dans Studio pour analyser votre premier document.', + 'history.emptyDocs': 'Aucun document. Importez un document depuis le Studio.', + 'history.open': 'Ouvrir', // Settings 'settings.title': 'Paramètres', @@ -138,8 +142,12 @@ const messages = { 'upload.uploading': 'Uploading...', 'upload.maxSize': 'Max 50MB', - 'history.title': 'Analysis History', + 'history.title': 'History', + 'history.tabAnalyses': 'Analyses', + 'history.tabDocuments': 'Documents', 'history.empty': 'No analyses yet. Go to Studio to analyze your first document.', + 'history.emptyDocs': 'No documents yet. Upload a document from the Studio.', + 'history.open': 'Open', 'settings.title': 'Settings', 'settings.apiUrl': 'API URL', 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) + }) })