diff --git a/frontend/app/__tests__/layout.test.tsx b/frontend/app/__tests__/layout.test.tsx
new file mode 100644
index 0000000..dc625b3
--- /dev/null
+++ b/frontend/app/__tests__/layout.test.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import RootLayout, { metadata } from '../layout';
+
+// Mock the ThemeProvider and Providers
+jest.mock('@/lib/theme', () => ({
+ ThemeProvider: ({ children }: { children: React.ReactNode }) =>
{children}
,
+}));
+
+jest.mock('../providers', () => ({
+ Providers: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+// Mock Next.js Script component
+jest.mock('next/script', () => {
+ return function Script(props: any) {
+ return ;
+ };
+});
+
+describe('RootLayout', () => {
+ it('should have correct metadata', () => {
+ expect(metadata.title).toBe('Container Shell - Docker Swarm Terminal');
+ expect(metadata.description).toBe('Docker container management terminal web UI');
+ });
+
+ it('should render children within providers', () => {
+ render(
+
+ Test Content
+
+ );
+
+ expect(screen.getByTestId('test-child')).toBeInTheDocument();
+ expect(screen.getByTestId('theme-provider')).toBeInTheDocument();
+ expect(screen.getByTestId('providers')).toBeInTheDocument();
+ });
+
+ it('should render with proper structure', () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(screen.getByTestId('content')).toBeInTheDocument();
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/frontend/app/__tests__/providers.test.tsx b/frontend/app/__tests__/providers.test.tsx
new file mode 100644
index 0000000..f168011
--- /dev/null
+++ b/frontend/app/__tests__/providers.test.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Providers } from '../providers';
+
+// Mock dependencies
+jest.mock('next/navigation', () => ({
+ useRouter: jest.fn(() => ({
+ push: jest.fn(),
+ })),
+}));
+
+describe('Providers', () => {
+ it('should render children', () => {
+ render(
+
+ Test Content
+
+ );
+
+ expect(screen.getByTestId('test-child')).toBeInTheDocument();
+ });
+
+ it('should wrap children with Redux Provider', () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(screen.getByTestId('test-content')).toBeInTheDocument();
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/__tests__/TerminalModal.test.tsx b/frontend/components/__tests__/TerminalModal.test.tsx
index 37009dc..85a5626 100644
--- a/frontend/components/__tests__/TerminalModal.test.tsx
+++ b/frontend/components/__tests__/TerminalModal.test.tsx
@@ -385,7 +385,7 @@ describe('TerminalModal', () => {
expect(mockUseSimpleTerminal).toHaveBeenCalledWith('container123');
});
- it('should call reset when closing FallbackNotification', () => {
+ it('should call reset when closing FallbackNotification', async () => {
const mockReset = jest.fn();
mockUseTerminalModalState.mockReturnValue({
@@ -405,9 +405,20 @@ describe('TerminalModal', () => {
/>
);
- // FallbackNotification onClose should call modalState.reset()
- // This is passed as a prop to FallbackNotification component
- expect(mockUseTerminalModalState).toHaveBeenCalled();
+ // Find and click the close button on the alert
+ const closeButtons = screen.getAllByRole('button');
+ // The Alert close button is typically the last one or has aria-label="Close"
+ const alertCloseButton = closeButtons.find(btn =>
+ btn.getAttribute('aria-label') === 'Close' ||
+ btn.className.includes('MuiAlert-closeButton')
+ );
+
+ if (alertCloseButton) {
+ fireEvent.click(alertCloseButton);
+ await waitFor(() => {
+ expect(mockReset).toHaveBeenCalled();
+ });
+ }
});
it('should apply minHeight/maxHeight based on isMobile', () => {
@@ -446,4 +457,106 @@ describe('TerminalModal', () => {
// Dialog should now use mobile dimensions (fullScreen)
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
+
+ it('should call handleClose when close button is clicked', () => {
+ const mockReset = jest.fn();
+ const mockCleanup = jest.fn();
+ const mockSimpleReset = jest.fn();
+
+ mockUseTerminalModalState.mockReturnValue({
+ ...defaultModalState,
+ reset: mockReset,
+ });
+
+ mockUseInteractiveTerminal.mockReturnValue({
+ ...defaultInteractiveTerminal,
+ cleanup: mockCleanup,
+ });
+
+ mockUseSimpleTerminal.mockReturnValue({
+ ...defaultSimpleTerminal,
+ reset: mockSimpleReset,
+ });
+
+ render(
+
+ );
+
+ // Click the close button
+ const closeButton = screen.getByRole('button', { name: /close/i });
+ fireEvent.click(closeButton);
+
+ // handleClose should call all cleanup functions
+ expect(mockCleanup).toHaveBeenCalled();
+ expect(mockSimpleReset).toHaveBeenCalled();
+ expect(mockReset).toHaveBeenCalled();
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('should execute command when Enter is pressed without Shift in simple mode', () => {
+ const mockExecuteCommand = jest.fn();
+
+ mockUseTerminalModalState.mockReturnValue({
+ ...defaultModalState,
+ mode: 'simple',
+ });
+
+ mockUseSimpleTerminal.mockReturnValue({
+ ...defaultSimpleTerminal,
+ command: 'ls -la',
+ executeCommand: mockExecuteCommand,
+ });
+
+ render(
+
+ );
+
+ // Find the text field and simulate Enter key press
+ const textField = screen.getByPlaceholderText('ls -la');
+ fireEvent.keyPress(textField, { key: 'Enter', code: 'Enter', charCode: 13, shiftKey: false });
+
+ // handleKeyPress should call preventDefault and executeCommand
+ expect(mockExecuteCommand).toHaveBeenCalled();
+ });
+
+ it('should not execute command when Shift+Enter is pressed in simple mode', () => {
+ const mockExecuteCommand = jest.fn();
+
+ mockUseTerminalModalState.mockReturnValue({
+ ...defaultModalState,
+ mode: 'simple',
+ });
+
+ mockUseSimpleTerminal.mockReturnValue({
+ ...defaultSimpleTerminal,
+ command: 'ls -la',
+ executeCommand: mockExecuteCommand,
+ });
+
+ render(
+
+ );
+
+ // Find the text field and simulate Shift+Enter key press
+ const textField = screen.getByPlaceholderText('ls -la');
+ fireEvent.keyPress(textField, { key: 'Enter', code: 'Enter', charCode: 13, shiftKey: true });
+
+ // handleKeyPress should NOT call executeCommand when Shift is pressed
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
+ });
});
diff --git a/frontend/lib/__tests__/theme.test.tsx b/frontend/lib/__tests__/theme.test.tsx
new file mode 100644
index 0000000..d4571b2
--- /dev/null
+++ b/frontend/lib/__tests__/theme.test.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { ThemeProvider } from '../theme';
+
+describe('ThemeProvider', () => {
+ it('should render children with theme', () => {
+ render(
+
+ Test Content
+
+ );
+
+ expect(screen.getByTestId('test-child')).toBeInTheDocument();
+ expect(screen.getByText('Test Content')).toBeInTheDocument();
+ });
+
+ it('should apply dark mode palette', () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ // CssBaseline should be rendered
+ expect(container).toBeInTheDocument();
+ });
+});
diff --git a/frontend/lib/hooks/__tests__/useAuthRedirect.test.tsx b/frontend/lib/hooks/__tests__/useAuthRedirect.test.tsx
index e56317d..d28f4fd 100644
--- a/frontend/lib/hooks/__tests__/useAuthRedirect.test.tsx
+++ b/frontend/lib/hooks/__tests__/useAuthRedirect.test.tsx
@@ -9,7 +9,7 @@ jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}));
-const createMockStore = (isAuthenticated: boolean) =>
+const createMockStore = (isAuthenticated: boolean, loading = false) =>
configureStore({
reducer: {
auth: authReducer,
@@ -17,7 +17,7 @@ const createMockStore = (isAuthenticated: boolean) =>
preloadedState: {
auth: {
isAuthenticated,
- loading: false,
+ loading,
username: isAuthenticated ? 'testuser' : null,
error: null,
},
@@ -66,4 +66,15 @@ describe('useAuthRedirect', () => {
expect(mockPush).not.toHaveBeenCalled();
});
+
+ it('does not redirect when loading is true', () => {
+ const store = createMockStore(false, true);
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+
+ renderHook(() => useAuthRedirect('/dashboard'), { wrapper });
+
+ expect(mockPush).not.toHaveBeenCalled();
+ });
});
diff --git a/frontend/lib/store/__tests__/store.test.ts b/frontend/lib/store/__tests__/store.test.ts
new file mode 100644
index 0000000..6450350
--- /dev/null
+++ b/frontend/lib/store/__tests__/store.test.ts
@@ -0,0 +1,26 @@
+import { store, RootState, AppDispatch } from '../store';
+
+describe('Store', () => {
+ it('should create store with auth reducer', () => {
+ expect(store).toBeDefined();
+ expect(store.getState()).toHaveProperty('auth');
+ });
+
+ it('should have correct state shape', () => {
+ const state = store.getState();
+ expect(state.auth).toHaveProperty('isAuthenticated');
+ expect(state.auth).toHaveProperty('loading');
+ expect(state.auth).toHaveProperty('username');
+ expect(state.auth).toHaveProperty('error');
+ });
+
+ it('should export RootState type', () => {
+ const state: RootState = store.getState();
+ expect(state).toBeDefined();
+ });
+
+ it('should export AppDispatch type', () => {
+ const dispatch: AppDispatch = store.dispatch;
+ expect(dispatch).toBeDefined();
+ });
+});