diff --git a/frontend/tests/components/AlertBanner.test.tsx b/frontend/tests/components/AlertBanner.test.tsx
new file mode 100644
index 0000000..281259b
--- /dev/null
+++ b/frontend/tests/components/AlertBanner.test.tsx
@@ -0,0 +1,48 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { AlertBanner } from '@/components/AlertBanner';
+
+describe('AlertBanner', () => {
+ it('renders error variant with alert role and alert-error class', () => {
+ const { container } = render(Error message);
+ const el = container.firstChild as HTMLElement;
+ expect(el).toHaveAttribute('role', 'alert');
+ expect(el).toHaveClass('alert-error');
+ expect(screen.getByText('Error message')).toBeInTheDocument();
+ });
+
+ it('renders warn variant with alert role and alert-warn class', () => {
+ const { container } = render(Warning message);
+ const el = container.firstChild as HTMLElement;
+ expect(el).toHaveAttribute('role', 'alert');
+ expect(el).toHaveClass('alert-warn');
+ });
+
+ it('renders success variant with status role and alert-success class', () => {
+ const { container } = render(Done!);
+ const el = container.firstChild as HTMLElement;
+ expect(el).toHaveAttribute('role', 'status');
+ expect(el).toHaveClass('alert-success');
+ });
+
+ it('renders info variant with status role and alert-info class', () => {
+ const { container } = render(FYI);
+ const el = container.firstChild as HTMLElement;
+ expect(el).toHaveAttribute('role', 'status');
+ expect(el).toHaveClass('alert-info');
+ });
+
+ it('renders optional title when provided', () => {
+ render(Detail text);
+ expect(screen.getByText('Heads up')).toBeInTheDocument();
+ expect(screen.getByText('Detail text')).toBeInTheDocument();
+ });
+
+ it('has no rounded-md, transition-*, or shadow-* (brutalism)', () => {
+ const { container } = render(Test);
+ const el = container.firstChild as HTMLElement;
+ expect(el).not.toHaveClass('rounded-md');
+ expect(el.className).not.toMatch(/transition-/);
+ expect(el.className).not.toMatch(/shadow-/);
+ });
+});
diff --git a/frontend/tests/components/BackLink.test.tsx b/frontend/tests/components/BackLink.test.tsx
new file mode 100644
index 0000000..0f922a7
--- /dev/null
+++ b/frontend/tests/components/BackLink.test.tsx
@@ -0,0 +1,39 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { BackLink } from '@/components/BackLink';
+
+function renderBackLink(to: string, label: string) {
+ return render(
+
+ {label}
+ ,
+ );
+}
+
+describe('BackLink', () => {
+ it('renders children as link text', () => {
+ renderBackLink('/engagements', 'Back to engagements');
+ expect(screen.getByText('Back to engagements')).toBeInTheDocument();
+ });
+
+ it('renders as an anchor with the correct href', () => {
+ renderBackLink('/engagements', 'Back to engagements');
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '/engagements');
+ });
+
+ it('has no rounded-md, transition-*, or shadow-* (brutalism)', () => {
+ renderBackLink('/engagements', 'Back');
+ const link = screen.getByRole('link');
+ expect(link).not.toHaveClass('rounded-md');
+ expect(link.className).not.toMatch(/transition-/);
+ expect(link.className).not.toMatch(/shadow-/);
+ });
+
+ it('contains the ArrowLeft icon (svg element)', () => {
+ const { container } = renderBackLink('/engagements', 'Back');
+ const svg = container.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ });
+});
diff --git a/frontend/tests/components/Tabs.test.tsx b/frontend/tests/components/Tabs.test.tsx
new file mode 100644
index 0000000..6fffbe1
--- /dev/null
+++ b/frontend/tests/components/Tabs.test.tsx
@@ -0,0 +1,60 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { Tabs } from '@/components/Tabs';
+
+const ITEMS = [
+ { id: 'schedule', label: 'Schedule' },
+ { id: 'description', label: 'Description' },
+ { id: 'simulations', label: 'Simulations', count: 5 },
+];
+
+describe('Tabs', () => {
+ it('renders all tab labels', () => {
+ render();
+ expect(screen.getByText('Schedule')).toBeInTheDocument();
+ expect(screen.getByText('Description')).toBeInTheDocument();
+ expect(screen.getByText('Simulations')).toBeInTheDocument();
+ });
+
+ it('marks the active tab with aria-selected=true', () => {
+ render();
+ const descBtn = screen.getByRole('tab', { name: /Description/i });
+ expect(descBtn).toHaveAttribute('aria-selected', 'true');
+ const schedBtn = screen.getByRole('tab', { name: /Schedule/i });
+ expect(schedBtn).toHaveAttribute('aria-selected', 'false');
+ });
+
+ it('applies tab-underline-active class to the active tab', () => {
+ render();
+ const activeBtn = screen.getByRole('tab', { name: /Schedule/i });
+ expect(activeBtn).toHaveClass('tab-underline-active');
+ });
+
+ it('renders count pill for the simulations tab', () => {
+ render();
+ const pill = screen.getByText('5');
+ expect(pill).toHaveClass('tab-count-pill');
+ });
+
+ it('applies tab-count-pill-active when the tab with count is active', () => {
+ render();
+ const pill = screen.getByText('5');
+ expect(pill).toHaveClass('tab-count-pill-active');
+ });
+
+ it('calls onChange with the tab id when clicked', () => {
+ const onChange = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole('tab', { name: /Description/i }));
+ expect(onChange).toHaveBeenCalledWith('description');
+ });
+
+ it('has no rounded-md, transition-*, or shadow-* on tab buttons (brutalism)', () => {
+ render();
+ for (const btn of screen.getAllByRole('tab')) {
+ expect(btn).not.toHaveClass('rounded-md');
+ expect(btn.className).not.toMatch(/transition-/);
+ expect(btn.className).not.toMatch(/shadow-/);
+ }
+ });
+});
diff --git a/frontend/tests/hooks/useHashTab.test.tsx b/frontend/tests/hooks/useHashTab.test.tsx
new file mode 100644
index 0000000..5a8ca5f
--- /dev/null
+++ b/frontend/tests/hooks/useHashTab.test.tsx
@@ -0,0 +1,43 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useHashTab } from '@/hooks/useHashTab';
+
+beforeEach(() => {
+ window.location.hash = '';
+});
+
+afterEach(() => {
+ window.location.hash = '';
+});
+
+describe('useHashTab', () => {
+ it('returns the default id when hash is empty', () => {
+ const { result } = renderHook(() => useHashTab('schedule'));
+ expect(result.current[0]).toBe('schedule');
+ });
+
+ it('reads the hash on mount', () => {
+ window.location.hash = 'simulations';
+ const { result } = renderHook(() => useHashTab('schedule'));
+ expect(result.current[0]).toBe('simulations');
+ });
+
+ it('navigate() updates activeId and sets location.hash', () => {
+ const { result } = renderHook(() => useHashTab('schedule'));
+ act(() => {
+ result.current[1]('description');
+ });
+ expect(result.current[0]).toBe('description');
+ expect(window.location.hash).toBe('#description');
+ });
+
+ it('falls back to defaultId when hash becomes empty', () => {
+ window.location.hash = 'simulations';
+ const { result } = renderHook(() => useHashTab('schedule'));
+ act(() => {
+ window.location.hash = '';
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
+ });
+ expect(result.current[0]).toBe('schedule');
+ });
+});