diff --git a/app/src/components/auth/LoginPage.test.tsx b/app/src/components/auth/LoginPage.test.tsx
index 40002ad..f7c4d78 100644
--- a/app/src/components/auth/LoginPage.test.tsx
+++ b/app/src/components/auth/LoginPage.test.tsx
@@ -38,8 +38,16 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => (
);
// Custom render function
-const render = (ui: React.ReactElement) => {
- return rtlRender(ui, { wrapper: TestWrapper });
+const render = async (ui: React.ReactElement) => {
+ const result = rtlRender(ui, { wrapper: TestWrapper });
+
+ // Wait for AuthProvider initialization to complete
+ await waitFor(() => {
+ // The LoginPage should be rendered (indicates AuthProvider has initialized)
+ expect(screen.getByText('Welcome to Lemma')).toBeInTheDocument();
+ });
+
+ return result;
};
describe('LoginPage', () => {
@@ -64,8 +72,8 @@ describe('LoginPage', () => {
});
describe('Initial Render', () => {
- it('renders the login form with all required elements', () => {
- render();
+ it('renders the login form with all required elements', async () => {
+ await render();
// Check title and subtitle
expect(screen.getByText('Welcome to Lemma')).toBeInTheDocument();
@@ -95,8 +103,8 @@ describe('LoginPage', () => {
});
describe('Form Interaction', () => {
- it('updates input values when user types', () => {
- render();
+ it('updates input values when user types', async () => {
+ await render();
const emailInput = screen.getByTestId('email-input');
const passwordInput = screen.getByTestId('password-input');
@@ -108,8 +116,8 @@ describe('LoginPage', () => {
expect((passwordInput as HTMLInputElement).value).toBe('password123');
});
- it('prevents form submission with empty fields due to HTML5 validation', () => {
- render();
+ it('prevents form submission with empty fields due to HTML5 validation', async () => {
+ await render();
const submitButton = screen.getByTestId('login-button');
fireEvent.click(submitButton);
@@ -132,7 +140,7 @@ describe('LoginPage', () => {
};
it('calls login function with correct credentials on form submit', async () => {
- render();
+ await render();
fillAndSubmitForm('test@example.com', 'password123');
await waitFor(() => {
@@ -151,7 +159,7 @@ describe('LoginPage', () => {
});
mockApiLogin.mockReturnValue(loginPromise);
- render();
+ await render();
const { submitButton } = fillAndSubmitForm(
'test@example.com',
'password123'
@@ -170,7 +178,7 @@ describe('LoginPage', () => {
});
it('handles login success with notification', async () => {
- render();
+ await render();
fillAndSubmitForm('test@example.com', 'password123');
await waitFor(() => {
@@ -194,7 +202,7 @@ describe('LoginPage', () => {
const errorMessage = 'Invalid credentials';
mockApiLogin.mockRejectedValue(new Error(errorMessage));
- render();
+ await render();
const { submitButton } = fillAndSubmitForm(
'test@example.com',
'wrongpassword'
diff --git a/app/src/components/editor/MarkdownPreview.test.tsx b/app/src/components/editor/MarkdownPreview.test.tsx
index da3ee85..444ee3a 100644
--- a/app/src/components/editor/MarkdownPreview.test.tsx
+++ b/app/src/components/editor/MarkdownPreview.test.tsx
@@ -272,7 +272,7 @@ describe('MarkdownPreview', () => {
});
});
- it('handles markdown processing errors gracefully', () => {
+ it('handles markdown processing errors gracefully', async () => {
// Mock console.error to avoid noise in test output
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -286,9 +286,12 @@ describe('MarkdownPreview', () => {
/>
);
- // Should still render something even if processing has issues
- const markdownPreview = screen.getByTestId('markdown-preview');
- expect(markdownPreview).toBeInTheDocument();
+ // Wait for async content processing to complete
+ await waitFor(() => {
+ // Should still render something even if processing has issues
+ const markdownPreview = screen.getByTestId('markdown-preview');
+ expect(markdownPreview).toBeInTheDocument();
+ });
consoleSpy.mockRestore();
});
diff --git a/app/src/components/modals/account/DeleteAccountModal.test.tsx b/app/src/components/modals/account/DeleteAccountModal.test.tsx
index 104c5bb..92a4b0e 100644
--- a/app/src/components/modals/account/DeleteAccountModal.test.tsx
+++ b/app/src/components/modals/account/DeleteAccountModal.test.tsx
@@ -4,6 +4,7 @@ import {
screen,
fireEvent,
waitFor,
+ act,
} from '@testing-library/react';
import React from 'react';
import { MantineProvider } from '@mantine/core';
@@ -192,7 +193,7 @@ describe('DeleteAccountModal', () => {
expect(mockOnConfirm).not.toHaveBeenCalled();
});
- it('handles rapid multiple clicks gracefully', () => {
+ it('handles rapid multiple clicks gracefully', async () => {
render(
{
fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
// Multiple rapid clicks should not break the component
- fireEvent.click(deleteButton);
- fireEvent.click(deleteButton);
- fireEvent.click(deleteButton);
+ act(() => {
+ fireEvent.click(deleteButton);
+ fireEvent.click(deleteButton);
+ fireEvent.click(deleteButton);
+ });
expect(screen.getByText('Delete Account')).toBeInTheDocument();
- expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('testpassword');
+ });
});
});
diff --git a/app/src/components/modals/account/EmailPasswordModal.test.tsx b/app/src/components/modals/account/EmailPasswordModal.test.tsx
index e1a25fd..7fca059 100644
--- a/app/src/components/modals/account/EmailPasswordModal.test.tsx
+++ b/app/src/components/modals/account/EmailPasswordModal.test.tsx
@@ -4,6 +4,7 @@ import {
screen,
fireEvent,
waitFor,
+ act,
} from '@testing-library/react';
import React from 'react';
import { MantineProvider } from '@mantine/core';
@@ -218,7 +219,7 @@ describe('EmailPasswordModal', () => {
});
});
- it('handles rapid multiple clicks gracefully', () => {
+ it('handles rapid multiple clicks gracefully', async () => {
render(
{
fireEvent.change(passwordInput, { target: { value: 'rapidtest' } });
// Multiple rapid clicks should not break the component
- fireEvent.click(confirmButton);
- fireEvent.click(confirmButton);
- fireEvent.click(confirmButton);
+ act(() => {
+ fireEvent.click(confirmButton);
+ fireEvent.click(confirmButton);
+ fireEvent.click(confirmButton);
+ });
- expect(screen.getByText('Confirm Password')).toBeInTheDocument();
- expect(mockOnConfirm).toHaveBeenCalledWith('rapidtest');
+ await waitFor(() => {
+ expect(mockOnConfirm).toHaveBeenCalledWith('rapidtest');
+ });
});
});
diff --git a/app/src/hooks/useAdminData.test.ts b/app/src/hooks/useAdminData.test.ts
index 9dcb79b..1d2153c 100644
--- a/app/src/hooks/useAdminData.test.ts
+++ b/app/src/hooks/useAdminData.test.ts
@@ -83,13 +83,18 @@ describe('useAdminData', () => {
});
describe('stats data type', () => {
- it('initializes with empty stats and loading state', () => {
+ it('initializes with empty stats and loading state', async () => {
const { result } = renderHook(() => useAdminData('stats'));
expect(result.current.data).toEqual({});
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeNull();
expect(typeof result.current.reload).toBe('function');
+
+ // Wait for the hook to complete its async initialization
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
});
it('loads system stats successfully', async () => {
@@ -148,13 +153,18 @@ describe('useAdminData', () => {
});
describe('users data type', () => {
- it('initializes with empty users array and loading state', () => {
+ it('initializes with empty users array and loading state', async () => {
const { result } = renderHook(() => useAdminData('users'));
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeNull();
expect(typeof result.current.reload).toBe('function');
+
+ // Wait for the hook to complete its async initialization
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
});
it('loads users successfully', async () => {
@@ -227,13 +237,18 @@ describe('useAdminData', () => {
});
describe('workspaces data type', () => {
- it('initializes with empty workspaces array and loading state', () => {
+ it('initializes with empty workspaces array and loading state', async () => {
const { result } = renderHook(() => useAdminData('workspaces'));
expect(result.current.data).toEqual([]);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBeNull();
expect(typeof result.current.reload).toBe('function');
+
+ // Wait for the hook to complete its async initialization
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
});
it('loads workspaces successfully', async () => {
@@ -456,22 +471,69 @@ describe('useAdminData', () => {
expect(result.current.data).toEqual(mockUsers);
expect(mockGetUsers).toHaveBeenCalledTimes(1);
});
- it('handles data type changes correctly with different initial values', () => {
- const { result: statsResult } = renderHook(() => useAdminData('stats'));
- const { result: usersResult } = renderHook(() => useAdminData('users'));
- const { result: workspacesResult } = renderHook(() =>
- useAdminData('workspaces')
+ it('handles data type changes correctly with different initial values', async () => {
+ const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
+ const mockGetUsers = vi.mocked(adminApi.getUsers);
+ const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
+
+ mockGetSystemStats.mockResolvedValue(mockSystemStats);
+ mockGetUsers.mockResolvedValue(mockUsers);
+ mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats);
+
+ const { result, rerender } = renderHook(
+ ({ type }) => useAdminData(type),
+ {
+ initialProps: { type: 'stats' as const } as {
+ type: 'stats' | 'users' | 'workspaces';
+ },
+ }
);
- // Different data types should have different initial values
- expect(statsResult.current.data).toEqual({});
- expect(usersResult.current.data).toEqual([]);
- expect(workspacesResult.current.data).toEqual([]);
+ // Wait for stats to load
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
+ expect(result.current.data).toEqual(mockSystemStats);
+
+ // Change to users type - should reset to empty array and reload
+ act(() => {
+ rerender({ type: 'users' as const });
+ });
+
+ // Data should reset to empty array immediately when type changes
+ expect(result.current.data).toEqual([]);
+ expect(result.current.loading).toBe(true);
+ expect(result.current.error).toBeNull();
+
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
+ expect(result.current.data).toEqual(mockUsers);
+
+ // Change to workspaces type - should reset to empty array and reload
+ act(() => {
+ rerender({ type: 'workspaces' as const });
+ });
+
+ // Data should reset to empty array immediately when type changes
+ expect(result.current.data).toEqual([]);
+ expect(result.current.loading).toBe(true);
+ expect(result.current.error).toBeNull();
+
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
+ expect(result.current.data).toEqual(mockWorkspaceStats);
+
+ // Verify correct API calls were made
+ expect(mockGetSystemStats).toHaveBeenCalledTimes(1);
+ expect(mockGetUsers).toHaveBeenCalledTimes(1);
+ expect(mockGetWorkspaces).toHaveBeenCalledTimes(1);
});
});
describe('function stability', () => {
- it('maintains stable reload function reference', () => {
+ it('maintains stable reload function reference', async () => {
const { result, rerender } = renderHook(() => useAdminData('stats'));
const initialReload = result.current.reload;
@@ -479,6 +541,11 @@ describe('useAdminData', () => {
rerender();
expect(result.current.reload).toBe(initialReload);
+
+ // Wait for the hook to complete its async initialization
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
});
});
diff --git a/app/src/hooks/useAdminData.ts b/app/src/hooks/useAdminData.ts
index 730b4ad..519aa9a 100644
--- a/app/src/hooks/useAdminData.ts
+++ b/app/src/hooks/useAdminData.ts
@@ -28,7 +28,7 @@ export const useAdminData = (
type: T
): AdminDataResult => {
// Initialize with the appropriate empty type
- const getInitialData = (): AdminData => {
+ const getInitialData = useCallback((): AdminData => {
if (type === 'stats') {
return {} as SystemStats as AdminData;
} else if (type === 'workspaces') {
@@ -38,12 +38,18 @@ export const useAdminData = (
} else {
return [] as unknown as AdminData;
}
- };
+ }, [type]);
const [data, setData] = useState>(getInitialData());
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ // Reset data when type changes
+ useEffect(() => {
+ setData(getInitialData());
+ setError(null);
+ }, [type, getInitialData]);
+
const loadData = useCallback(async () => {
setLoading(true);
setError(null);
diff --git a/app/src/hooks/useFileContent.test.ts b/app/src/hooks/useFileContent.test.ts
index 9ac10ab..fade57c 100644
--- a/app/src/hooks/useFileContent.test.ts
+++ b/app/src/hooks/useFileContent.test.ts
@@ -439,9 +439,23 @@ describe('useFileContent', () => {
});
describe('function stability', () => {
- it('maintains stable function references across re-renders and workspace changes', () => {
+ it('maintains stable function references across re-renders and workspace changes', async () => {
+ const mockGetFileContent = vi.mocked(fileApi.getFileContent);
+ const mockIsImageFile = vi.mocked(fileHelpers.isImageFile);
+
+ // Mock API calls for both workspaces
+ mockGetFileContent
+ .mockResolvedValueOnce('Content from workspace 1')
+ .mockResolvedValueOnce('Content from workspace 2');
+ mockIsImageFile.mockReturnValue(false);
+
const { result, rerender } = renderHook(() => useFileContent('test.md'));
+ // Wait for initial load to complete
+ await waitFor(() => {
+ expect(result.current.content).toBe('Content from workspace 1');
+ });
+
const initialFunctions = {
setContent: result.current.setContent,
setHasUnsavedChanges: result.current.setHasUnsavedChanges,
@@ -464,14 +478,21 @@ describe('useFileContent', () => {
);
// Change workspace
- mockWorkspaceData.currentWorkspace = {
- id: 2,
- name: 'different-workspace',
- };
+ act(() => {
+ mockWorkspaceData.currentWorkspace = {
+ id: 2,
+ name: 'different-workspace',
+ };
+ });
rerender();
- // Functions should still be stable
+ // Wait for content to load from new workspace
+ await waitFor(() => {
+ expect(result.current.content).toBe('Content from workspace 2');
+ });
+
+ // Functions should still be stable (except handleContentChange which depends on originalContent)
expect(result.current.setContent).toBe(initialFunctions.setContent);
expect(result.current.setHasUnsavedChanges).toBe(
initialFunctions.setHasUnsavedChanges
@@ -479,7 +500,8 @@ describe('useFileContent', () => {
expect(result.current.loadFileContent).not.toBe(
initialFunctions.loadFileContent
);
- expect(result.current.handleContentChange).toBe(
+ // handleContentChange depends on originalContent which changes when workspace changes
+ expect(result.current.handleContentChange).not.toBe(
initialFunctions.handleContentChange
);
});