Files
syndarix/frontend/tests/components/layout/AppLayout.test.tsx
Felipe Cardoso a4c91cb8c3 refactor(frontend): clean up code by consolidating multi-line JSX into single lines where feasible
- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits.
- Improved consistency in component imports by grouping and consolidating them.
- No functional changes, purely restructuring for clarity and maintainability.
2026-01-01 11:46:57 +01:00

382 lines
9.7 KiB
TypeScript

/**
* Tests for AppLayout and related components
* Verifies layout structure, responsive behavior, and component integration
*/
import { render, screen } from '@testing-library/react';
import { AppLayout, PageContainer, PageHeader } from '@/components/layout/AppLayout';
import { useAuth } from '@/lib/auth/AuthContext';
import { useLogout } from '@/lib/api/hooks/useAuth';
import { mockUsePathname } from 'next-intl/navigation';
import type { User } from '@/lib/stores/authStore';
// Mock dependencies
jest.mock('@/lib/auth/AuthContext', () => ({
useAuth: jest.fn(),
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
jest.mock('@/lib/api/hooks/useAuth', () => ({
useLogout: jest.fn(),
}));
jest.mock('@/components/theme', () => ({
ThemeToggle: () => <div data-testid="theme-toggle">Theme Toggle</div>,
}));
jest.mock('@/components/i18n', () => ({
LocaleSwitcher: () => <div data-testid="locale-switcher">Locale Switcher</div>,
}));
// Helper to create mock user
function createMockUser(overrides: Partial<User> = {}): User {
return {
id: 'user-123',
email: 'test@example.com',
first_name: 'Test',
last_name: 'User',
phone_number: null,
is_active: true,
is_superuser: false,
created_at: new Date().toISOString(),
updated_at: null,
...overrides,
};
}
describe('AppLayout', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUsePathname.mockReturnValue('/projects');
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
(useLogout as jest.Mock).mockReturnValue({
mutate: jest.fn(),
isPending: false,
});
});
describe('Rendering', () => {
it('renders layout container', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
expect(screen.getByTestId('app-layout')).toBeInTheDocument();
});
it('renders children', () => {
render(
<AppLayout>
<div data-testid="test-content">Test Content</div>
</AppLayout>
);
expect(screen.getByTestId('test-content')).toBeInTheDocument();
});
it('renders header', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
expect(screen.getByTestId('app-header')).toBeInTheDocument();
});
it('renders sidebar', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
});
it('renders breadcrumbs', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
expect(screen.getByTestId('breadcrumbs')).toBeInTheDocument();
});
it('renders main content area', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
const main = screen.getByRole('main');
expect(main).toBeInTheDocument();
expect(main).toHaveAttribute('id', 'main-content');
});
});
describe('Configuration Options', () => {
it('hides sidebar when hideSidebar is true', () => {
render(
<AppLayout hideSidebar>
<div>Content</div>
</AppLayout>
);
expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument();
});
it('hides breadcrumbs when hideBreadcrumbs is true', () => {
render(
<AppLayout hideBreadcrumbs>
<div>Content</div>
</AppLayout>
);
expect(screen.queryByTestId('breadcrumbs')).not.toBeInTheDocument();
});
it('passes custom breadcrumbs to AppBreadcrumbs', () => {
const customBreadcrumbs = [{ label: 'Custom', href: '/custom', current: true }];
render(
<AppLayout breadcrumbs={customBreadcrumbs}>
<div>Content</div>
</AppLayout>
);
expect(screen.getByTestId('breadcrumb-custom')).toBeInTheDocument();
});
it('passes project slug to sidebar', () => {
const currentProject = { id: '1', slug: 'test-project', name: 'Test' };
render(
<AppLayout currentProject={currentProject}>
<div>Content</div>
</AppLayout>
);
// Sidebar should show project-specific navigation
expect(screen.getByTestId('nav-dashboard')).toBeInTheDocument();
});
});
describe('Custom ClassNames', () => {
it('applies className to main content', () => {
render(
<AppLayout className="custom-main">
<div>Content</div>
</AppLayout>
);
const main = screen.getByRole('main');
expect(main).toHaveClass('custom-main');
});
it('applies wrapperClassName to outer container', () => {
render(
<AppLayout wrapperClassName="custom-wrapper">
<div>Content</div>
</AppLayout>
);
const layout = screen.getByTestId('app-layout');
expect(layout).toHaveClass('custom-wrapper');
});
});
describe('Accessibility', () => {
it('main content has tabIndex for skip link support', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
const main = screen.getByRole('main');
expect(main).toHaveAttribute('tabIndex', '-1');
});
it('layout has min-h-screen for full viewport', () => {
render(
<AppLayout>
<div>Content</div>
</AppLayout>
);
const layout = screen.getByTestId('app-layout');
expect(layout).toHaveClass('min-h-screen');
});
});
});
describe('PageContainer', () => {
describe('Rendering', () => {
it('renders container', () => {
render(
<PageContainer>
<div>Content</div>
</PageContainer>
);
expect(screen.getByTestId('page-container')).toBeInTheDocument();
});
it('renders children', () => {
render(
<PageContainer>
<div data-testid="test-content">Test Content</div>
</PageContainer>
);
expect(screen.getByTestId('test-content')).toBeInTheDocument();
});
});
describe('Max Width', () => {
it('defaults to max-w-6xl', () => {
render(
<PageContainer>
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('max-w-6xl');
});
it('applies custom max width', () => {
render(
<PageContainer maxWidth="md">
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('max-w-md');
});
it('applies full width', () => {
render(
<PageContainer maxWidth="full">
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('max-w-full');
});
});
describe('Styling', () => {
it('has container and centering classes', () => {
render(
<PageContainer>
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('container', 'mx-auto');
});
it('has responsive padding', () => {
render(
<PageContainer>
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('px-4', 'py-6', 'lg:px-6', 'lg:py-8');
});
it('applies custom className', () => {
render(
<PageContainer className="custom-class">
<div>Content</div>
</PageContainer>
);
const container = screen.getByTestId('page-container');
expect(container).toHaveClass('custom-class');
});
});
});
describe('PageHeader', () => {
describe('Rendering', () => {
it('renders page header', () => {
render(<PageHeader title="Test Title" />);
expect(screen.getByTestId('page-header')).toBeInTheDocument();
});
it('renders title', () => {
render(<PageHeader title="Test Title" />);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Test Title');
});
it('renders description when provided', () => {
render(<PageHeader title="Title" description="Test description" />);
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('does not render description when not provided', () => {
render(<PageHeader title="Title" />);
const header = screen.getByTestId('page-header');
expect(header.querySelectorAll('p')).toHaveLength(0);
});
it('renders actions when provided', () => {
render(
<PageHeader title="Title" actions={<button data-testid="action-button">Action</button>} />
);
expect(screen.getByTestId('action-button')).toBeInTheDocument();
});
});
describe('Styling', () => {
it('has responsive flex layout', () => {
render(<PageHeader title="Title" />);
const header = screen.getByTestId('page-header');
expect(header).toHaveClass('flex', 'flex-col', 'sm:flex-row');
});
it('title has responsive text size', () => {
render(<PageHeader title="Title" />);
const title = screen.getByRole('heading', { level: 1 });
expect(title).toHaveClass('text-2xl', 'sm:text-3xl');
});
it('description has muted styling', () => {
render(<PageHeader title="Title" description="Description" />);
const description = screen.getByText('Description');
expect(description).toHaveClass('text-muted-foreground');
});
it('applies custom className', () => {
render(<PageHeader title="Title" className="custom-class" />);
const header = screen.getByTestId('page-header');
expect(header).toHaveClass('custom-class');
});
});
});