Add tests for UserMenu and WorkspaceSwitcher components

This commit is contained in:
2025-07-04 22:44:19 +02:00
parent eaa37a262e
commit fffd93afeb
4 changed files with 413 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../test/utils';
import UserMenu from './UserMenu';
import { UserRole } from '../../types/models';
// Mock the contexts
vi.mock('../../contexts/AuthContext', () => ({
useAuth: vi.fn(),
}));
// Mock the settings components
vi.mock('../settings/account/AccountSettings', () => ({
default: ({ opened, onClose }: { opened: boolean; onClose: () => void }) => (
<div data-testid="account-settings-modal" data-opened={opened}>
<button onClick={onClose}>Close Account Settings</button>
</div>
),
}));
vi.mock('../settings/admin/AdminDashboard', () => ({
default: ({ opened, onClose }: { opened: boolean; onClose: () => void }) => (
<div data-testid="admin-dashboard-modal" data-opened={opened}>
<button onClick={onClose}>Close Admin Dashboard</button>
</div>
),
}));
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
);
describe('UserMenu', () => {
const mockLogout = vi.fn();
const mockUser = {
id: 1,
email: 'test@example.com',
displayName: 'Test User',
role: UserRole.Editor,
createdAt: '2024-01-01T00:00:00Z',
lastWorkspaceId: 1,
};
beforeEach(async () => {
vi.clearAllMocks();
const { useAuth } = await import('../../contexts/AuthContext');
vi.mocked(useAuth).mockReturnValue({
user: mockUser,
logout: mockLogout,
loading: false,
initialized: true,
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
});
});
it('renders user avatar and shows user info when clicked', async () => {
const { getByLabelText, getByText } = render(
<TestWrapper>
<UserMenu />
</TestWrapper>
);
// Find and click the avatar
const avatar = getByLabelText('User menu');
fireEvent.click(avatar);
// Check if user info is displayed in popover
await waitFor(() => {
expect(getByText('Test User')).toBeInTheDocument();
});
});
it('shows admin dashboard option for admin users only', async () => {
// Test admin user sees admin option
const { useAuth } = await import('../../contexts/AuthContext');
vi.mocked(useAuth).mockReturnValue({
user: { ...mockUser, role: UserRole.Admin },
logout: mockLogout,
loading: false,
initialized: true,
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
});
const { getByLabelText, getByText } = render(
<TestWrapper>
<UserMenu />
</TestWrapper>
);
const avatar = getByLabelText('User menu');
fireEvent.click(avatar);
await waitFor(() => {
expect(getByText('Admin Dashboard')).toBeInTheDocument();
});
});
it('opens account settings modal when clicked', async () => {
const { getByLabelText, getByText, getByTestId } = render(
<TestWrapper>
<UserMenu />
</TestWrapper>
);
const avatar = getByLabelText('User menu');
fireEvent.click(avatar);
await waitFor(() => {
const accountSettingsButton = getByText('Account Settings');
fireEvent.click(accountSettingsButton);
});
await waitFor(() => {
const modal = getByTestId('account-settings-modal');
expect(modal).toHaveAttribute('data-opened', 'true');
});
});
it('calls logout when logout button is clicked', async () => {
const { getByLabelText, getByText } = render(
<TestWrapper>
<UserMenu />
</TestWrapper>
);
const avatar = getByLabelText('User menu');
fireEvent.click(avatar);
await waitFor(() => {
const logoutButton = getByText('Logout');
fireEvent.click(logoutButton);
});
expect(mockLogout).toHaveBeenCalledOnce();
});
it('displays user email when displayName is not available', async () => {
const { useAuth } = await import('../../contexts/AuthContext');
const userWithoutDisplayName = {
id: mockUser.id,
email: mockUser.email,
role: mockUser.role,
createdAt: mockUser.createdAt,
lastWorkspaceId: mockUser.lastWorkspaceId,
};
vi.mocked(useAuth).mockReturnValue({
user: userWithoutDisplayName,
logout: mockLogout,
loading: false,
initialized: true,
login: vi.fn(),
refreshToken: vi.fn(),
refreshUser: vi.fn(),
});
const { getByLabelText, getByText } = render(
<TestWrapper>
<UserMenu />
</TestWrapper>
);
const avatar = getByLabelText('User menu');
fireEvent.click(avatar);
await waitFor(() => {
expect(getByText('test@example.com')).toBeInTheDocument();
});
});
});

View File

@@ -47,6 +47,10 @@ const UserMenu: React.FC = () => {
radius="xl"
style={{ cursor: 'pointer' }}
onClick={() => setOpened((o) => !o)}
aria-label="User menu"
aria-expanded={opened}
aria-haspopup="menu"
role="button"
>
<IconUser size={24} />
</Avatar>

View File

@@ -0,0 +1,232 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../test/utils';
import WorkspaceSwitcher from './WorkspaceSwitcher';
import { Theme } from '../../types/models';
// Mock the hooks and contexts
vi.mock('../../hooks/useWorkspace', () => ({
useWorkspace: vi.fn(),
}));
vi.mock('../../contexts/ModalContext', () => ({
useModalContext: vi.fn(),
}));
// Mock API
vi.mock('../../api/workspace', () => ({
listWorkspaces: vi.fn(),
}));
// Mock the CreateWorkspaceModal component
vi.mock('../modals/workspace/CreateWorkspaceModal', () => ({
default: ({
onWorkspaceCreated,
}: {
onWorkspaceCreated: (workspace: {
name: string;
createdAt: number;
}) => void;
}) => (
<div data-testid="create-workspace-modal">
<button
onClick={() =>
onWorkspaceCreated({ name: 'New Workspace', createdAt: Date.now() })
}
>
Create Test Workspace
</button>
</div>
),
}));
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
);
describe('WorkspaceSwitcher', () => {
const mockSwitchWorkspace = vi.fn();
const mockSetSettingsModalVisible = vi.fn();
const mockSetCreateWorkspaceModalVisible = vi.fn();
const mockCurrentWorkspace = {
id: 1,
name: 'Current Workspace',
createdAt: '2024-01-01T00:00:00Z',
theme: Theme.Light,
autoSave: false,
showHiddenFiles: false,
gitEnabled: false,
gitUrl: '',
gitUser: '',
gitToken: '',
gitAutoCommit: false,
gitCommitMsgTemplate: '${action} ${filename}',
gitCommitName: '',
gitCommitEmail: '',
};
const mockWorkspaces = [
mockCurrentWorkspace,
{
id: 2,
name: 'Other Workspace',
createdAt: '2024-01-02T00:00:00Z',
theme: Theme.Dark,
autoSave: true,
showHiddenFiles: true,
gitEnabled: true,
gitUrl: 'https://github.com/test/repo',
gitUser: 'testuser',
gitToken: 'token',
gitAutoCommit: true,
gitCommitMsgTemplate: 'Auto: ${action} ${filename}',
gitCommitName: 'Test User',
gitCommitEmail: 'test@example.com',
},
];
beforeEach(async () => {
vi.clearAllMocks();
const { useWorkspace } = await import('../../hooks/useWorkspace');
vi.mocked(useWorkspace).mockReturnValue({
currentWorkspace: mockCurrentWorkspace,
workspaces: [],
settings: mockCurrentWorkspace,
updateSettings: vi.fn(),
loading: false,
colorScheme: 'light',
updateColorScheme: vi.fn(),
switchWorkspace: mockSwitchWorkspace,
deleteCurrentWorkspace: vi.fn(),
});
const { useModalContext } = await import('../../contexts/ModalContext');
vi.mocked(useModalContext).mockReturnValue({
newFileModalVisible: false,
setNewFileModalVisible: vi.fn(),
deleteFileModalVisible: false,
setDeleteFileModalVisible: vi.fn(),
commitMessageModalVisible: false,
setCommitMessageModalVisible: vi.fn(),
settingsModalVisible: false,
setSettingsModalVisible: mockSetSettingsModalVisible,
switchWorkspaceModalVisible: false,
setSwitchWorkspaceModalVisible: vi.fn(),
createWorkspaceModalVisible: false,
setCreateWorkspaceModalVisible: mockSetCreateWorkspaceModalVisible,
});
const { listWorkspaces } = await import('../../api/workspace');
vi.mocked(listWorkspaces).mockResolvedValue(mockWorkspaces);
});
it('renders current workspace name', () => {
const { getByText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
expect(getByText('Current Workspace')).toBeInTheDocument();
});
it('shows "No workspace" when no current workspace', async () => {
const { useWorkspace } = await import('../../hooks/useWorkspace');
vi.mocked(useWorkspace).mockReturnValue({
currentWorkspace: null,
workspaces: [],
settings: mockCurrentWorkspace,
updateSettings: vi.fn(),
loading: false,
colorScheme: 'light',
updateColorScheme: vi.fn(),
switchWorkspace: mockSwitchWorkspace,
deleteCurrentWorkspace: vi.fn(),
});
const { getByText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
expect(getByText('No workspace')).toBeInTheDocument();
});
it('opens popover and shows workspace list when clicked', async () => {
const { getByText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
// Click to open popover
const trigger = getByText('Current Workspace');
fireEvent.click(trigger);
// Should see the workspaces header and workspace list
await waitFor(() => {
expect(getByText('Workspaces')).toBeInTheDocument();
expect(getByText('Other Workspace')).toBeInTheDocument();
});
});
it('switches workspace when another workspace is clicked', async () => {
const { getByText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
// Open popover and click on other workspace
const trigger = getByText('Current Workspace');
fireEvent.click(trigger);
await waitFor(() => {
const otherWorkspace = getByText('Other Workspace');
fireEvent.click(otherWorkspace);
});
expect(mockSwitchWorkspace).toHaveBeenCalledWith('Other Workspace');
});
it('opens create workspace modal when create button is clicked', async () => {
const { getByText, getByLabelText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
// Open popover and click create button
const trigger = getByText('Current Workspace');
fireEvent.click(trigger);
await waitFor(() => {
const createButton = getByLabelText('Create New Workspace');
fireEvent.click(createButton);
});
expect(mockSetCreateWorkspaceModalVisible).toHaveBeenCalledWith(true);
});
it('opens settings modal when settings button is clicked', async () => {
const { getByText, getByLabelText } = render(
<TestWrapper>
<WorkspaceSwitcher />
</TestWrapper>
);
// Open popover and click settings button
const trigger = getByText('Current Workspace');
fireEvent.click(trigger);
await waitFor(() => {
const settingsButton = getByLabelText('Workspace Settings');
fireEvent.click(settingsButton);
});
expect(mockSetSettingsModalVisible).toHaveBeenCalledWith(true);
});
});

View File

@@ -95,6 +95,7 @@ const WorkspaceSwitcher: React.FC = () => {
<ActionIcon
variant="default"
size="md"
aria-label="Create New Workspace"
onClick={handleCreateWorkspace}
>
<IconFolderPlus size={16} />
@@ -152,6 +153,7 @@ const WorkspaceSwitcher: React.FC = () => {
variant="subtle"
size="lg"
color={getConditionalColor(theme, true)}
aria-label="Workspace Settings"
onClick={(e) => {
e.stopPropagation();
setSettingsModalVisible(true);