diff --git a/frontend/testing/unit/components/AppShell.test.tsx b/frontend/testing/unit/components/AppShell.test.tsx
new file mode 100644
index 00000000..6306f212
--- /dev/null
+++ b/frontend/testing/unit/components/AppShell.test.tsx
@@ -0,0 +1,116 @@
+import React from 'react'
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+
+let pathname = '/'
+
+vi.mock('react-router-dom', () => ({
+ NavLink: ({ children, to }: any) => (
+ {children}
+ ),
+ useLocation: () => ({
+ pathname,
+ }),
+}))
+
+vi.mock('../../../src/components/Sidebar', () => ({
+ default: () =>
,
+}))
+
+vi.mock('../../../src/components/Background', () => ({
+ default: () => ,
+}))
+
+vi.mock('../../../src/hooks/useShortcuts', () => ({
+ useShortcuts: vi.fn(),
+}))
+
+import AppShell from '../../../src/components/AppShell'
+
+describe('AppShell', () => {
+ beforeEach(() => {
+ pathname = '/'
+ localStorage.clear()
+ })
+
+ it('renders children content', () => {
+ render(
+
+ Test Content
+
+ )
+
+ expect(screen.getByText('Test Content')).toBeTruthy()
+ })
+
+ it('opens and closes mobile drawer from menu button', () => {
+ render(
+
+ Content
+
+ )
+
+ const button = screen.getByLabelText(/toggle navigation menu/i)
+
+ fireEvent.click(button)
+
+ expect(screen.getByText('Settings')).toBeTruthy()
+
+ fireEvent.click(button)
+
+ expect(screen.queryByText('Settings')).toBeNull()
+ })
+
+ it('renders bottom navigation items', () => {
+ render(
+
+ Content
+
+ )
+
+ expect(screen.getByText('Dashboard')).toBeTruthy()
+ expect(screen.getByText('Scans')).toBeTruthy()
+ expect(screen.getByText('Findings')).toBeTruthy()
+ expect(screen.getByText('Reports')).toBeTruthy()
+ expect(screen.getByText('Workflows')).toBeTruthy()
+ })
+
+ it('renders drawer navigation items when menu is opened', () => {
+ render(
+
+ Content
+
+ )
+
+ fireEvent.click(
+ screen.getByLabelText(/toggle navigation menu/i)
+ )
+
+ expect(screen.getByText('Settings')).toBeTruthy()
+ expect(screen.getAllByText('Dashboard').length).toBeGreaterThan(0)
+ })
+
+ it('closes mobile drawer when route changes', () => {
+ const { rerender } = render(
+
+ Content
+
+ )
+
+ fireEvent.click(
+ screen.getByLabelText(/toggle navigation menu/i)
+ )
+
+ expect(screen.getByText('Settings')).toBeTruthy()
+
+ pathname = '/reports'
+
+ rerender(
+
+ Content
+
+ )
+
+ expect(screen.queryByText('Settings')).toBeNull()
+ })
+})
\ No newline at end of file