diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 1406ba1..f24d144 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -1,12 +1,10 @@ -import { test, expect } from '@playwright/test'; -import { TEST_USERS, TIMEOUTS } from './utils/test-data'; +import { test, expect, AuthHelper, TEST_CREDENTIALS, TIMEOUTS } from './fixtures/auth'; import { TestHelpers } from './utils/test-helpers'; test.describe('Authentication', () => { - let helpers: TestHelpers; - test.beforeEach(async ({ page }) => { - helpers = new TestHelpers(page); + const authHelper = new AuthHelper(page); + await authHelper.ensureLoggedOut(); }); test('should display login form on initial visit', async ({ page }) => { @@ -19,22 +17,12 @@ test.describe('Authentication', () => { }); test('should login with valid credentials', async ({ page }) => { - await page.goto('/'); + const authHelper = new AuthHelper(page); - // Fill login form with demo credentials - await page.fill('input[name="username"]', 'admin'); - await page.fill('input[name="password"]', 'readur2024'); - - // Wait for login API call - const loginResponse = helpers.waitForApiCall('/auth/login'); - - await page.click('button[type="submit"]'); - - // Verify login was successful - await loginResponse; + await authHelper.loginAs(TEST_CREDENTIALS.admin); // Should redirect to dashboard or main page - await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium }); + await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation }); // Verify we're no longer on login page await expect(page.locator('input[name="username"]')).not.toBeVisible(); @@ -49,20 +37,19 @@ test.describe('Authentication', () => { await page.click('button[type="submit"]'); // Should show error message (Material-UI Alert) - await expect(page.locator('.MuiAlert-root, [role="alert"]')).toBeVisible({ timeout: TIMEOUTS.short }); + await expect(page.locator('.MuiAlert-root, [role="alert"]')).toBeVisible({ timeout: TIMEOUTS.api }); // Should remain on login page await expect(page.locator('input[name="username"]')).toBeVisible(); }); test.skip('should logout successfully', async ({ page }) => { - // First login - await page.goto('/'); - await page.fill('input[name="username"]', 'admin'); - await page.fill('input[name="password"]', 'readur2024'); - await page.click('button[type="submit"]'); + const authHelper = new AuthHelper(page); - await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium }); + // First login + await authHelper.loginAs(TEST_CREDENTIALS.admin); + + await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation }); // Find and click profile/account button in the top app bar (has AccountIcon) const profileButton = page.locator('button:has([data-testid="AccountCircleIcon"])'); @@ -73,18 +60,17 @@ test.describe('Authentication', () => { await logoutMenuItem.click(); // Should redirect back to login - await page.waitForURL(/\/login|\//, { timeout: TIMEOUTS.medium }); + await page.waitForURL(/\/login|\//, { timeout: TIMEOUTS.navigation }); await expect(page.locator('input[name="username"]')).toBeVisible(); }); test.skip('should persist session on page reload', async ({ page }) => { - // Login first - await page.goto('/'); - await page.fill('input[name="username"]', 'admin'); - await page.fill('input[name="password"]', 'readur2024'); - await page.click('button[type="submit"]'); + const authHelper = new AuthHelper(page); - await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium }); + // Login first + await authHelper.loginAs(TEST_CREDENTIALS.admin); + + await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation }); // Reload the page await page.reload(); @@ -93,7 +79,7 @@ test.describe('Authentication', () => { await page.waitForLoadState('networkidle'); // Should still be logged in (either on dashboard or main page, but not login) - await page.waitForURL(/\/dashboard|\/(?!login)/, { timeout: TIMEOUTS.medium }); + await page.waitForURL(/\/dashboard|\/(?!login)/, { timeout: TIMEOUTS.navigation }); await expect(page.locator('input[name="username"]')).not.toBeVisible(); }); diff --git a/frontend/e2e/sources.spec.ts b/frontend/e2e/sources.spec.ts index e8f924e..1f27f42 100644 --- a/frontend/e2e/sources.spec.ts +++ b/frontend/e2e/sources.spec.ts @@ -5,18 +5,18 @@ import { TestHelpers } from './utils/test-helpers'; test.describe('Source Management', () => { let helpers: TestHelpers; - test.beforeEach(async ({ authenticatedPage }) => { - helpers = new TestHelpers(authenticatedPage); + test.beforeEach(async ({ adminPage }) => { + helpers = new TestHelpers(adminPage); await helpers.navigateToPage('/sources'); }); - test.skip('should display sources interface', async ({ authenticatedPage: page }) => { + test.skip('should display sources interface', async ({ adminPage: page }) => { // Check for sources page components await expect(page.locator('[data-testid="sources-list"], .sources-list, .sources-container')).toBeVisible(); await expect(page.locator('button:has-text("Add Source"), [data-testid="add-source"]')).toBeVisible(); }); - test.skip('should create a new local folder source', async ({ authenticatedPage: page }) => { + test.skip('should create a new local folder source', async ({ adminPage: page }) => { // Click add source button await page.click('button:has-text("Add Source"), [data-testid="add-source"]'); @@ -51,7 +51,7 @@ test.describe('Source Management', () => { await expect(page.locator(':has-text("Test Local Folder")')).toBeVisible({ timeout: TIMEOUTS.medium }); }); - test.skip('should create a new WebDAV source', async ({ authenticatedPage: page }) => { + test.skip('should create a new WebDAV source', async ({ adminPage: page }) => { await page.click('button:has-text("Add Source"), [data-testid="add-source"]'); await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible(); @@ -79,7 +79,7 @@ test.describe('Source Management', () => { await expect(page.locator(':has-text("Test WebDAV")')).toBeVisible({ timeout: TIMEOUTS.medium }); }); - test.skip('should create a new S3 source', async ({ authenticatedPage: page }) => { + test.skip('should create a new S3 source', async ({ adminPage: page }) => { await page.click('button:has-text("Add Source"), [data-testid="add-source"]'); await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible(); @@ -108,7 +108,7 @@ test.describe('Source Management', () => { await expect(page.locator(':has-text("Test S3 Bucket")')).toBeVisible({ timeout: TIMEOUTS.medium }); }); - test('should edit existing source', async ({ authenticatedPage: page }) => { + test('should edit existing source', async ({ adminPage: page }) => { // Look for existing source to edit const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); @@ -138,7 +138,7 @@ test.describe('Source Management', () => { } }); - test('should delete source', async ({ authenticatedPage: page }) => { + test('should delete source', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { @@ -168,7 +168,7 @@ test.describe('Source Management', () => { } }); - test.skip('should start source sync', async ({ authenticatedPage: page }) => { + test.skip('should start source sync', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { @@ -189,7 +189,7 @@ test.describe('Source Management', () => { } }); - test('should stop source sync', async ({ authenticatedPage: page }) => { + test('should stop source sync', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { @@ -217,7 +217,7 @@ test.describe('Source Management', () => { } }); - test('should display source status and statistics', async ({ authenticatedPage: page }) => { + test('should display source status and statistics', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { @@ -235,7 +235,7 @@ test.describe('Source Management', () => { } }); - test.skip('should test source connection', async ({ authenticatedPage: page }) => { + test.skip('should test source connection', async ({ adminPage: page }) => { await page.click('button:has-text("Add Source"), [data-testid="add-source"]'); await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible(); @@ -266,7 +266,7 @@ test.describe('Source Management', () => { } }); - test('should filter sources by type', async ({ authenticatedPage: page }) => { + test('should filter sources by type', async ({ adminPage: page }) => { // Look for filter dropdown const filterDropdown = page.locator('[data-testid="source-filter"], select[name="filter"], .source-filter'); if (await filterDropdown.isVisible()) { @@ -282,7 +282,7 @@ test.describe('Source Management', () => { } }); - test('should display sync history', async ({ authenticatedPage: page }) => { + test('should display sync history', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { @@ -297,7 +297,7 @@ test.describe('Source Management', () => { } }); - test.skip('should validate required fields in source creation', async ({ authenticatedPage: page }) => { + test.skip('should validate required fields in source creation', async ({ adminPage: page }) => { await page.click('button:has-text("Add Source"), [data-testid="add-source"]'); await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible(); @@ -316,7 +316,7 @@ test.describe('Source Management', () => { } }); - test('should schedule automatic sync', async ({ authenticatedPage: page }) => { + test('should schedule automatic sync', async ({ adminPage: page }) => { const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first(); if (await firstSource.isVisible()) { diff --git a/frontend/e2e/utils/test-data.ts b/frontend/e2e/utils/test-data.ts index df71c00..bb836f8 100644 --- a/frontend/e2e/utils/test-data.ts +++ b/frontend/e2e/utils/test-data.ts @@ -1,8 +1,7 @@ +import { TEST_CREDENTIALS } from '../fixtures/auth'; + export const TEST_USERS = { - valid: { - username: 'admin', - password: 'readur2024' - }, + valid: TEST_CREDENTIALS.admin, invalid: { username: 'invaliduser', password: 'wrongpassword' diff --git a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx index 9232e0b..8e48bd5 100644 --- a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx +++ b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx @@ -1,9 +1,7 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { BrowserRouter } from 'react-router-dom'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { vi } from 'vitest'; import Login from '../Login'; -import { AuthProvider } from '../../../contexts/AuthContext'; -import { ThemeProvider } from '../../../contexts/ThemeContext'; +import { renderWithProviders, setupTestEnvironment } from '../../../test/test-utils'; // Mock the API vi.mock('../../../services/api', () => ({ @@ -27,15 +25,6 @@ vi.mock('react-router-dom', async () => { }; }); -// Mock localStorage -Object.defineProperty(window, 'localStorage', { - value: { - setItem: vi.fn(), - getItem: vi.fn(() => null), - removeItem: vi.fn() - } -}); - // Mock window.location Object.defineProperty(window, 'location', { value: { @@ -44,58 +33,14 @@ Object.defineProperty(window, 'location', { writable: true }); - -// Mock AuthContext -const mockAuthContextValue = { - user: null, - loading: false, - login: vi.fn(), - register: vi.fn(), - logout: vi.fn() -}; - -const MockAuthProvider = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - -const MockThemeProvider = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - describe('Login - OIDC Features', () => { beforeEach(() => { vi.clearAllMocks(); - - // Mock window.matchMedia - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), - removeListener: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); + setupTestEnvironment(); }); const renderLogin = () => { - return render( - - - - - - - - ); + return renderWithProviders(); }; it('renders OIDC login button', () => { diff --git a/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx b/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx index a2cf8e9..f54375a 100644 --- a/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx +++ b/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx @@ -1,8 +1,8 @@ -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { screen, waitFor, fireEvent } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { vi } from 'vitest'; import OidcCallback from '../OidcCallback'; -import { AuthProvider } from '../../../contexts/AuthContext'; +import { renderWithProviders, setupTestEnvironment } from '../../../test/test-utils'; import { api } from '../../../services/api'; // Mock the API @@ -28,15 +28,6 @@ vi.mock('react-router-dom', async () => { }; }); -// Mock localStorage -Object.defineProperty(window, 'localStorage', { - value: { - setItem: vi.fn(), - getItem: vi.fn(), - removeItem: vi.fn() - } -}); - // Mock window.location Object.defineProperty(window, 'location', { value: { @@ -45,56 +36,24 @@ Object.defineProperty(window, 'location', { writable: true }); - -// Mock AuthContext -const mockAuthContextValue = { - user: null, - loading: false, - login: vi.fn(), - register: vi.fn(), - logout: vi.fn() -}; - -const MockAuthProvider = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); - describe('OidcCallback', () => { beforeEach(() => { vi.clearAllMocks(); + setupTestEnvironment(); window.location.href = ''; // Clear API mocks (api.get as any).mockClear(); // Reset API mocks to default implementation (api.get as any).mockResolvedValue({ data: { token: 'default-token' } }); - - // Mock window.matchMedia - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), - removeListener: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); }); const renderOidcCallback = (search = '') => { - return render( + return renderWithProviders( - - - } /> - Login Page} /> - - + + } /> + Login Page} /> + ); }; diff --git a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx index 470ecd9..16d9e9e 100644 --- a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx +++ b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx @@ -3,10 +3,12 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; import GlobalSearchBar from '../GlobalSearchBar'; -import { renderWithProviders, createMockApiServices } from '../../../test/test-utils'; +import { renderWithProviders, createMockApiServices, setupTestEnvironment, createMockLocalStorage } from '../../../test/test-utils'; // Use centralized API mocking const mockServices = createMockApiServices(); +const mockDocumentService = mockServices.documentService; +const localStorageMock = createMockLocalStorage(); // Mock useNavigate const mockNavigate = vi.fn(); @@ -43,6 +45,7 @@ const mockSearchResponse = { describe('GlobalSearchBar', () => { beforeEach(() => { vi.clearAllMocks(); + setupTestEnvironment(); localStorageMock.getItem.mockReturnValue(null); mockDocumentService.enhancedSearch.mockResolvedValue(mockSearchResponse); }); diff --git a/frontend/src/components/Labels/__tests__/Label.test.tsx b/frontend/src/components/Labels/__tests__/Label.test.tsx index 2213a60..ab82d83 100644 --- a/frontend/src/components/Labels/__tests__/Label.test.tsx +++ b/frontend/src/components/Labels/__tests__/Label.test.tsx @@ -1,32 +1,24 @@ import { describe, test, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; +import { screen, fireEvent } from '@testing-library/react'; import Label, { type LabelData } from '../Label'; +import { renderWithProviders } from '../../../test/test-utils'; +import { + createMockLabel, + createMockSystemLabel, + setupTestEnvironment +} from '../../../test/label-test-utils'; -const theme = createTheme(); - -const mockLabel: LabelData = { - id: 'test-label-1', +const mockLabel = createMockLabel({ name: 'Test Label', - description: 'A test label', color: '#ff0000', - background_color: undefined, - icon: 'star', - is_system: false, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', document_count: 5, source_count: 2, -}; +}); -const systemLabel: LabelData = { - ...mockLabel, - id: 'system-label-1', +const systemLabel = createMockSystemLabel({ name: 'Important', color: '#d73a49', - icon: 'star', - is_system: true, -}; +}); const renderLabel = (props: Partial> = {}) => { const defaultProps = { @@ -34,14 +26,14 @@ const renderLabel = (props: Partial> = {}) => ...props, }; - return render( - - - ); + return renderWithProviders(