From 6e4579fd02184ee87d81a5a511ee8c8f102978a2 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Mon, 14 Jul 2025 20:46:51 +0000 Subject: [PATCH] feat(lang): implement tests for this updated language selection --- frontend/e2e/ocr-multiple-languages.spec.ts | 234 ++++++++++++-- .../LanguageSelector.tsx | 0 .../__tests__/LanguageSelector.test.tsx | 293 ++++++++++++++++++ .../src/components/LanguageSelector/index.ts | 1 + tests/integration_webdav_integration_tests.rs | 6 + 5 files changed, 506 insertions(+), 28 deletions(-) rename frontend/src/components/{ => LanguageSelector}/LanguageSelector.tsx (100%) create mode 100644 frontend/src/components/LanguageSelector/__tests__/LanguageSelector.test.tsx create mode 100644 frontend/src/components/LanguageSelector/index.ts diff --git a/frontend/e2e/ocr-multiple-languages.spec.ts b/frontend/e2e/ocr-multiple-languages.spec.ts index 86e7bc8..b1b6084 100644 --- a/frontend/e2e/ocr-multiple-languages.spec.ts +++ b/frontend/e2e/ocr-multiple-languages.spec.ts @@ -45,21 +45,25 @@ test.describe('OCR Multiple Languages', () => { await page.goto('/settings'); await helpers.waitForLoadingToComplete(); - // Look for OCR language selector component - const languageSelector = page.locator('[data-testid="ocr-language-selector"], #ocr-language-label').first(); + // Look for the new LanguageSelector component + const languageSelector = page.locator('label:has-text("OCR Languages")').first(); await expect(languageSelector).toBeVisible({ timeout: TIMEOUTS.medium }); - // Check if the selector shows available languages - const selectInput = page.locator('div[role="combobox"], select[id*="ocr"], input[id*="language"]').first(); - if (await selectInput.isVisible()) { - await selectInput.click(); + // Check for the language selector button + const selectButton = page.locator('button:has-text("Select OCR languages")').first(); + if (await selectButton.isVisible()) { + await selectButton.click(); - // Wait for language options to appear + // Wait for dropdown panel to appear await page.waitForTimeout(1000); + // Check for dropdown panel with languages + const dropdownPanel = page.locator('text="Available Languages"').first(); + await expect(dropdownPanel).toBeVisible({ timeout: 3000 }); + // Check for Spanish and English options - const spanishOption = page.locator('[data-value="spa"], option[value="spa"], :has-text("Spanish")').first(); - const englishOption = page.locator('[data-value="eng"], option[value="eng"], :has-text("English")').first(); + const spanishOption = page.locator('text="Spanish"').first(); + const englishOption = page.locator('text="English"').first(); if (await spanishOption.isVisible({ timeout: 3000 })) { console.log('✅ Spanish language option found'); @@ -70,35 +74,53 @@ test.describe('OCR Multiple Languages', () => { } }); - test('should change OCR language preference to Spanish', async ({ adminPage: page }) => { + test('should select multiple OCR languages', async ({ adminPage: page }) => { await page.goto('/settings'); await helpers.waitForLoadingToComplete(); - // Find and interact with language selector - const languageSelector = page.locator('[data-testid="ocr-language-selector"], div:has(label:text("OCR Language"))').first(); + // Find the multi-language selector button + const selectButton = page.locator('button:has-text("Select OCR languages")').first(); - if (await languageSelector.isVisible()) { - // Click on the selector to open dropdown - await languageSelector.click(); + if (await selectButton.isVisible()) { + await selectButton.click(); await page.waitForTimeout(500); // Select Spanish option - const spanishOption = page.locator('[data-value="spa"], option[value="spa"], li:has-text("Spanish")').first(); + const spanishOption = page.locator('button:has-text("Spanish")').first(); if (await spanishOption.isVisible({ timeout: 5000 })) { await spanishOption.click(); + await page.waitForTimeout(500); - // Look for save button or auto-save indication - const saveButton = page.locator('button:has-text("Save"), button[type="submit"]').first(); - if (await saveButton.isVisible({ timeout: 3000 })) { - // Wait for settings update API call - const updatePromise = helpers.waitForApiCall('/api/settings', TIMEOUTS.medium); - await saveButton.click(); - await updatePromise; + // Select English option + const englishOption = page.locator('button:has-text("English")').first(); + if (await englishOption.isVisible({ timeout: 5000 })) { + await englishOption.click(); + await page.waitForTimeout(500); + + // Close the dropdown + const closeButton = page.locator('button:has-text("Close")').first(); + if (await closeButton.isVisible()) { + await closeButton.click(); + } + + // Verify both languages are selected and displayed as tags + await expect(page.locator('text="Spanish"')).toBeVisible({ timeout: 3000 }); + await expect(page.locator('text="English"')).toBeVisible({ timeout: 3000 }); + await expect(page.locator('text="(Primary)"')).toBeVisible({ timeout: 3000 }); + + // Look for save button + const saveButton = page.locator('button:has-text("Save"), button[type="submit"]').first(); + if (await saveButton.isVisible({ timeout: 3000 })) { + // Wait for settings update API call + const updatePromise = helpers.waitForApiCall('/api/settings', TIMEOUTS.medium); + await saveButton.click(); + await updatePromise; + + // Check for success indication + await helpers.waitForToast(); + console.log('✅ Multiple OCR languages selected and saved'); + } } - - // Check for success indication - await helpers.waitForToast(); - console.log('✅ OCR language changed to Spanish'); } } }); @@ -475,7 +497,7 @@ test.describe('OCR Multiple Languages', () => { await helpers.waitForLoadingToComplete(); // Look for language selector component - const languageSelector = page.locator('[data-testid="ocr-language-selector"]').first(); + const languageSelector = page.locator('label:has-text("OCR Languages")').first(); // Check for error handling in language selector const errorAlert = page.locator('[role="alert"], .error, .alert-warning').first(); @@ -498,4 +520,160 @@ test.describe('OCR Multiple Languages', () => { console.log('✅ Fallback language option available'); } }); + + test('should upload document with multiple languages selected', async ({ adminPage: page }) => { + // First set multiple languages in settings + await page.goto('/settings'); + await helpers.waitForLoadingToComplete(); + + const selectButton = page.locator('button:has-text("Select OCR languages")').first(); + if (await selectButton.isVisible()) { + await selectButton.click(); + + // Select English and Spanish + const englishOption = page.locator('button:has-text("English")').first(); + if (await englishOption.isVisible()) { + await englishOption.click(); + await page.waitForTimeout(500); + } + + const spanishOption = page.locator('button:has-text("Spanish")').first(); + if (await spanishOption.isVisible()) { + await spanishOption.click(); + await page.waitForTimeout(500); + } + + // Close dropdown and save + const closeButton = page.locator('button:has-text("Close")').first(); + if (await closeButton.isVisible()) { + await closeButton.click(); + } + + const saveButton = page.locator('button:has-text("Save")').first(); + if (await saveButton.isVisible()) { + await saveButton.click(); + await helpers.waitForToast(); + } + } + + // Navigate to upload page + await page.goto('/upload'); + await helpers.waitForLoadingToComplete(); + + // Check if the upload form includes multi-language selector + const uploadLanguageSelector = page.locator('label:has-text("OCR Languages")').first(); + if (await uploadLanguageSelector.isVisible()) { + console.log('✅ Multi-language selector available in upload form'); + + // Click to view language options + const uploadSelectButton = page.locator('button:has-text("Select OCR languages"), button:has-text("Add more languages")').first(); + if (await uploadSelectButton.isVisible()) { + await uploadSelectButton.click(); + await page.waitForTimeout(500); + + // Verify languages are selectable for upload + const uploadDropdown = page.locator('text="Available Languages"').first(); + if (await uploadDropdown.isVisible()) { + console.log('✅ Language options available for upload'); + } + + // Close the dropdown + const uploadCloseButton = page.locator('button:has-text("Close")').first(); + if (await uploadCloseButton.isVisible()) { + await uploadCloseButton.click(); + } + } + } + + // Upload a test file + const fileInput = page.locator('input[type="file"]').first(); + if (await fileInput.isAttached({ timeout: 10000 })) { + try { + await fileInput.setInputFiles(MULTILINGUAL_TEST_FILES.mixed); + + // Verify file appears in upload list + await expect(page.getByText('mixed_language_test.pdf')).toBeVisible({ timeout: 5000 }); + + // Click upload button + const uploadButton = page.locator('button:has-text("Upload")').first(); + if (await uploadButton.isVisible()) { + const uploadPromise = helpers.waitForApiCall('/api/documents', TIMEOUTS.upload); + await uploadButton.click(); + await uploadPromise; + + console.log('✅ Multi-language document uploaded successfully'); + } + } catch (error) { + console.log('ℹ️ Mixed language test file not found, skipping upload test'); + } + } + }); + + test('should retry failed OCR with multiple languages', async ({ adminPage: page }) => { + await page.goto('/documents'); + await helpers.waitForLoadingToComplete(); + + // Look for retry button on any document + const retryButton = page.locator('button:has-text("Retry"), [data-testid="retry-ocr"]').first(); + + if (await retryButton.isVisible()) { + await retryButton.click(); + + // Check if retry dialog opens with multi-language options + const retryDialog = page.locator('[role="dialog"], .modal').first(); + if (await retryDialog.isVisible({ timeout: 5000 })) { + + // Look for multi-language toggle buttons + const multiLanguageButton = page.locator('button:has-text("Multiple Languages")').first(); + if (await multiLanguageButton.isVisible()) { + await multiLanguageButton.click(); + console.log('✅ Multi-language mode activated in retry dialog'); + + // Look for language selector in retry dialog + const retryLanguageSelector = page.locator('label:has-text("OCR Languages")').first(); + if (await retryLanguageSelector.isVisible()) { + const retrySelectButton = page.locator('button:has-text("Select OCR languages")').first(); + if (await retrySelectButton.isVisible()) { + await retrySelectButton.click(); + + // Select multiple languages for retry + const retryEnglishOption = page.locator('button:has-text("English")').first(); + if (await retryEnglishOption.isVisible()) { + await retryEnglishOption.click(); + await page.waitForTimeout(500); + } + + const retrySpanishOption = page.locator('button:has-text("Spanish")').first(); + if (await retrySpanishOption.isVisible()) { + await retrySpanishOption.click(); + await page.waitForTimeout(500); + } + + // Close language selector + const retryCloseButton = page.locator('button:has-text("Close")').first(); + if (await retryCloseButton.isVisible()) { + await retryCloseButton.click(); + } + } + } + + // Confirm retry with multiple languages + const confirmRetryButton = page.locator('button:has-text("Retry OCR")').first(); + if (await confirmRetryButton.isVisible()) { + const retryPromise = helpers.waitForApiCall('/retry', TIMEOUTS.ocr); + await confirmRetryButton.click(); + + try { + await retryPromise; + console.log('✅ OCR retry with multiple languages initiated'); + } catch (error) { + console.log('ℹ️ Multi-language retry may have failed or timed out'); + } + } + } + } + } else { + console.log('ℹ️ No retry buttons found for multi-language retry testing'); + } + }); }); \ No newline at end of file diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector/LanguageSelector.tsx similarity index 100% rename from frontend/src/components/LanguageSelector.tsx rename to frontend/src/components/LanguageSelector/LanguageSelector.tsx diff --git a/frontend/src/components/LanguageSelector/__tests__/LanguageSelector.test.tsx b/frontend/src/components/LanguageSelector/__tests__/LanguageSelector.test.tsx new file mode 100644 index 0000000..cea7a84 --- /dev/null +++ b/frontend/src/components/LanguageSelector/__tests__/LanguageSelector.test.tsx @@ -0,0 +1,293 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import LanguageSelector from '../LanguageSelector'; +import { renderWithProviders } from '../../../test/test-utils'; + +const renderLanguageSelector = (props: Partial> = {}) => { + const defaultProps = { + selectedLanguages: [], + primaryLanguage: '', + onLanguagesChange: vi.fn(), + ...props, + }; + + return renderWithProviders(); +}; + +describe('LanguageSelector Component', () => { + let user: ReturnType; + + beforeEach(() => { + user = userEvent.setup(); + }); + + describe('Basic Rendering', () => { + test('should render the language selector container', () => { + renderLanguageSelector(); + expect(screen.getByText('OCR Languages')).toBeInTheDocument(); + }); + + test('should show default state text when no languages selected', () => { + renderLanguageSelector(); + expect(screen.getByText('No languages selected. Documents will use default OCR language.')).toBeInTheDocument(); + }); + + test('should show selection button', () => { + renderLanguageSelector(); + expect(screen.getByText('Select OCR languages...')).toBeInTheDocument(); + }); + + test('should show language count when languages are selected', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng' + }); + expect(screen.getByText('OCR Languages (2/4)')).toBeInTheDocument(); + }); + + test('should open dropdown when button is clicked', async () => { + renderLanguageSelector(); + + await user.click(screen.getByText('Select OCR languages...')); + + expect(screen.getByText('Available Languages')).toBeInTheDocument(); + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('Spanish')).toBeInTheDocument(); + }); + + test('should apply custom className', () => { + const { container } = renderLanguageSelector({ className: 'custom-class' }); + expect(container.firstChild).toHaveClass('custom-class'); + }); + }); + + describe('Language Selection', () => { + test('should show selected languages as tags', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng' + }); + + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('Spanish')).toBeInTheDocument(); + expect(screen.getByText('(Primary)')).toBeInTheDocument(); + }); + + test('should call onLanguagesChange when language is selected from dropdown', async () => { + const mockOnChange = vi.fn(); + renderLanguageSelector({ onLanguagesChange: mockOnChange }); + + // Open dropdown + await user.click(screen.getByText('Select OCR languages...')); + + // Select English from the dropdown + const englishButton = screen.getByText('English').closest('button'); + if (englishButton) { + await user.click(englishButton); + } + + expect(mockOnChange).toHaveBeenCalledWith(['eng'], 'eng'); + }); + + test('should show "Add more languages" when languages are selected', () => { + renderLanguageSelector({ + selectedLanguages: ['eng'], + primaryLanguage: 'eng' + }); + + expect(screen.getByText('Add more languages (3 remaining)')).toBeInTheDocument(); + }); + + test('should handle maximum language limit', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa', 'fra', 'deu'], + primaryLanguage: 'eng', + maxLanguages: 4 + }); + + expect(screen.getByText('Add more languages (0 remaining)')).toBeInTheDocument(); + }); + }); + + describe('Primary Language', () => { + test('should show primary language indicator', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng' + }); + + expect(screen.getByText('(Primary)')).toBeInTheDocument(); + }); + + test('should handle primary language changes', async () => { + const mockOnChange = vi.fn(); + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng', + onLanguagesChange: mockOnChange + }); + + // Open dropdown and click on a primary language option + await user.click(screen.getByText('Add more languages (2 remaining)')); + + // The implementation should show primary selection when languages are selected + // This is more of an integration test + }); + }); + + describe('Disabled State', () => { + test('should not show button when disabled', () => { + renderLanguageSelector({ disabled: true }); + + expect(screen.queryByText('Select OCR languages...')).not.toBeInTheDocument(); + }); + + test('should not show remove buttons when disabled', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng', + disabled: true + }); + + // Should show languages but no interactive elements + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('Spanish')).toBeInTheDocument(); + }); + }); + + describe('Custom Configuration', () => { + test('should respect custom maxLanguages prop', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng', + maxLanguages: 3 + }); + + expect(screen.getByText('OCR Languages (2/3)')).toBeInTheDocument(); + expect(screen.getByText('Add more languages (1 remaining)')).toBeInTheDocument(); + }); + + test('should handle edge case of maxLanguages = 1', () => { + renderLanguageSelector({ + selectedLanguages: ['eng'], + primaryLanguage: 'eng', + maxLanguages: 1 + }); + + expect(screen.getByText('OCR Languages (1/1)')).toBeInTheDocument(); + expect(screen.getByText('Add more languages (0 remaining)')).toBeInTheDocument(); + }); + }); + + describe('Language Display', () => { + test('should show available languages in dropdown', async () => { + renderLanguageSelector(); + + await user.click(screen.getByText('Select OCR languages...')); + + // Check for common languages + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('Spanish')).toBeInTheDocument(); + expect(screen.getByText('French')).toBeInTheDocument(); + expect(screen.getByText('German')).toBeInTheDocument(); + expect(screen.getByText('Chinese (Simplified)')).toBeInTheDocument(); + }); + + test('should handle less common languages', async () => { + renderLanguageSelector(); + + await user.click(screen.getByText('Select OCR languages...')); + + // Check for some less common languages + expect(screen.getByText('Japanese')).toBeInTheDocument(); + expect(screen.getByText('Arabic')).toBeInTheDocument(); + expect(screen.getByText('Thai')).toBeInTheDocument(); + }); + }); + + describe('Integration Scenarios', () => { + test('should handle typical workflow: select language', async () => { + const mockOnChange = vi.fn(); + renderLanguageSelector({ onLanguagesChange: mockOnChange }); + + // Start with no languages + expect(screen.getByText('No languages selected. Documents will use default OCR language.')).toBeInTheDocument(); + + // Open dropdown and select English + await user.click(screen.getByText('Select OCR languages...')); + const englishButton = screen.getByText('English').closest('button'); + if (englishButton) { + await user.click(englishButton); + } + + expect(mockOnChange).toHaveBeenCalledWith(['eng'], 'eng'); + }); + + test('should handle selecting multiple languages', async () => { + const mockOnChange = vi.fn(); + + // Start with one language selected + renderLanguageSelector({ + selectedLanguages: ['eng'], + primaryLanguage: 'eng', + onLanguagesChange: mockOnChange + }); + + // Should show the selected language + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('(Primary)')).toBeInTheDocument(); + + // Should show "Add more languages" button + expect(screen.getByText('Add more languages (3 remaining)')).toBeInTheDocument(); + }); + + test('should handle deselecting all languages', () => { + const mockOnChange = vi.fn(); + renderLanguageSelector({ + selectedLanguages: [], + primaryLanguage: '', + onLanguagesChange: mockOnChange + }); + + expect(screen.getByText('No languages selected. Documents will use default OCR language.')).toBeInTheDocument(); + }); + }); + + describe('Accessibility', () => { + test('should be keyboard navigable', async () => { + renderLanguageSelector(); + + const button = screen.getByText('Select OCR languages...').closest('button'); + + // Tab to button and press Enter to open + button?.focus(); + expect(button).toHaveFocus(); + + await user.keyboard('{Enter}'); + expect(screen.getByText('Available Languages')).toBeInTheDocument(); + }); + + test('should have proper button roles', () => { + renderLanguageSelector(); + + const button = screen.getByText('Select OCR languages...').closest('button'); + expect(button).toHaveAttribute('type', 'button'); + }); + + test('should have proper structure when languages are selected', () => { + renderLanguageSelector({ + selectedLanguages: ['eng', 'spa'], + primaryLanguage: 'eng' + }); + + // Should have language tags + expect(screen.getByText('English')).toBeInTheDocument(); + expect(screen.getByText('Spanish')).toBeInTheDocument(); + + // Should have proper button for adding more + const addButton = screen.getByText('Add more languages (2 remaining)'); + expect(addButton.closest('button')).toHaveAttribute('type', 'button'); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/LanguageSelector/index.ts b/frontend/src/components/LanguageSelector/index.ts new file mode 100644 index 0000000..48b0dfd --- /dev/null +++ b/frontend/src/components/LanguageSelector/index.ts @@ -0,0 +1 @@ +export { default } from './LanguageSelector'; \ No newline at end of file diff --git a/tests/integration_webdav_integration_tests.rs b/tests/integration_webdav_integration_tests.rs index 6d1f85d..d60038c 100644 --- a/tests/integration_webdav_integration_tests.rs +++ b/tests/integration_webdav_integration_tests.rs @@ -21,6 +21,9 @@ use readur::{ fn create_empty_update_settings() -> UpdateSettings { UpdateSettings { ocr_language: None, + preferred_languages: None, + primary_language: None, + auto_detect_language_combination: None, concurrent_ocr_jobs: None, ocr_timeout_seconds: None, max_file_size_mb: None, @@ -154,6 +157,9 @@ async fn setup_webdav_settings(state: &AppState, user_id: Uuid) { webdav_auto_sync: Some(true), webdav_sync_interval_minutes: Some(60), ocr_language: None, + preferred_languages: None, + primary_language: None, + auto_detect_language_combination: None, concurrent_ocr_jobs: None, ocr_timeout_seconds: None, max_file_size_mb: None,