diff --git a/src/components/__tests__/CampItem.test.tsx b/src/components/__tests__/CampItem.test.tsx
new file mode 100644
index 0000000..ad2d2c7
--- /dev/null
+++ b/src/components/__tests__/CampItem.test.tsx
@@ -0,0 +1,311 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { render, fireEvent } from '@testing-library/react-native';
+import CampItem from '../CampItem';
+import { useThemeColors } from '../../utils/useThemeColors';
+import { Colors, Spacing, BorderRadius, FontSize } from '../../utils/theme';
+import type { Camp } from '../../types';
+
+jest.mock('../../utils/useThemeColors', () => ({
+ useThemeColors: jest.fn(),
+}));
+
+const mockUseThemeColors = useThemeColors as jest.Mock;
+
+describe('CampItem', () => {
+ const defaultCamp: Camp = {
+ id: 'camp-1',
+ name: 'Base Camp',
+ done: false,
+ order: 0,
+ };
+
+ const mockOnToggle = jest.fn();
+ const mockOnDelete = jest.fn();
+ const mockOnEdit = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseThemeColors.mockReturnValue({ colors: Colors.light, isDark: false });
+ });
+
+ const getContainerChildren = (root: any) => {
+ let node = root;
+ while (node && node.children && node.children.length === 1 && node.children[0].children) {
+ if (node.children[0].children.length > 1) {
+ return node.children[0].children;
+ }
+ node = node.children[0];
+ }
+ return node?.children || [];
+ };
+
+ describe('rendering and content', () => {
+ it('renders camp name with single line limit', async () => {
+ const { getByText } = await render(
+
+ );
+ const nameElement = getByText('Base Camp');
+
+ expect(nameElement).toBeTruthy();
+ expect(nameElement.props.numberOfLines).toBe(1);
+ });
+
+ it('applies container layout styling', async () => {
+ const { toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const flattenedStyle = StyleSheet.flatten(tree.props.style);
+
+ expect(flattenedStyle.flexDirection).toBe('row');
+ expect(flattenedStyle.alignItems).toBe('center');
+ expect(flattenedStyle.paddingVertical).toBe(Spacing.sm);
+ expect(flattenedStyle.paddingHorizontal).toBe(Spacing.md);
+ expect(flattenedStyle.borderRadius).toBe(BorderRadius.sm);
+ expect(flattenedStyle.borderWidth).toBe(1);
+ expect(flattenedStyle.marginBottom).toBe(Spacing.sm);
+ });
+ });
+
+ describe('incomplete state', () => {
+ it('does not render checkmark text when done is false', async () => {
+ const { queryByText } = await render(
+
+ );
+
+ expect(queryByText('✓')).toBeNull();
+ });
+
+ it('applies incomplete styling to checkbox and name', async () => {
+ const { getByText, toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const checkboxView = tree.children[0].children[0];
+ const checkboxStyle = StyleSheet.flatten(checkboxView.props.style);
+
+ expect(checkboxStyle.borderColor).toBe(Colors.light.textTertiary);
+ expect(checkboxStyle.backgroundColor).toBe('transparent');
+
+ const nameElement = getByText('Base Camp');
+ const nameStyle = StyleSheet.flatten(nameElement.props.style);
+ expect(nameStyle.color).toBe(Colors.light.text);
+ expect(nameStyle.textDecorationLine).toBeUndefined();
+ });
+ });
+
+ describe('completed state', () => {
+ it('renders checkmark text when done is true', async () => {
+ const { getByText } = await render(
+
+ );
+ const checkmark = getByText('✓');
+
+ expect(checkmark).toBeTruthy();
+ const checkmarkStyle = StyleSheet.flatten(checkmark.props.style);
+ expect(checkmarkStyle.color).toBe('#FFFFFF');
+ expect(checkmarkStyle.fontSize).toBe(FontSize.sm);
+ expect(checkmarkStyle.fontWeight).toBe('700');
+ expect(checkmarkStyle.lineHeight).toBe(FontSize.sm);
+ });
+
+ it('applies completed styling to checkbox and strikethrough to name', async () => {
+ const { getByText, toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const checkboxView = tree.children[0].children[0];
+ const checkboxStyle = StyleSheet.flatten(checkboxView.props.style);
+
+ expect(checkboxStyle.borderColor).toBe(Colors.light.success);
+ expect(checkboxStyle.backgroundColor).toBe(Colors.light.success);
+
+ const nameElement = getByText('Base Camp');
+ const nameStyle = StyleSheet.flatten(nameElement.props.style);
+ expect(nameStyle.color).toBe(Colors.light.textTertiary);
+ expect(nameStyle.textDecorationLine).toBe('line-through');
+ });
+ });
+
+ describe('edit action', () => {
+ it('renders edit button with pencil icon when onEdit is provided', async () => {
+ const { toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ expect(tree.children.length).toBe(4);
+
+ const editButton = tree.children[2];
+ const pencilIcon = editButton.children[0];
+ expect(pencilIcon.props.style).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ color: Colors.light.textTertiary, fontSize: 18 }),
+ expect.objectContaining({ fontFamily: 'material-community' }),
+ ])
+ );
+ });
+
+ it('does not render edit button when onEdit is omitted', async () => {
+ const { toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ expect(tree.children.length).toBe(3);
+ });
+ });
+
+ describe('delete action', () => {
+ it('always renders delete button with trash icon', async () => {
+ const { toJSON } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const deleteButton = tree.children[2];
+ const trashIcon = deleteButton.children[0];
+
+ expect(trashIcon.props.style).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ color: Colors.light.danger, fontSize: 18 }),
+ expect.objectContaining({ fontFamily: 'material-community' }),
+ ])
+ );
+ });
+ });
+
+ describe('user interactions', () => {
+ it('calls onToggle when pressing checkbox area', async () => {
+ const { root } = await render(
+
+ );
+ const children = getContainerChildren(root);
+ const checkboxTouchable = children[0];
+
+ await fireEvent.press(checkboxTouchable);
+ expect(mockOnToggle).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onEdit when pressing edit button', async () => {
+ const { root } = await render(
+
+ );
+ const children = getContainerChildren(root);
+ const editTouchable = children[2];
+
+ await fireEvent.press(editTouchable);
+ expect(mockOnEdit).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onDelete when pressing delete button', async () => {
+ const { root } = await render(
+
+ );
+ const children = getContainerChildren(root);
+ const deleteTouchable = children[3];
+
+ await fireEvent.press(deleteTouchable);
+ expect(mockOnDelete).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onDelete when pressing delete button without onEdit provided', async () => {
+ const { root } = await render(
+
+ );
+ const children = getContainerChildren(root);
+ const deleteTouchable = children[2];
+
+ await fireEvent.press(deleteTouchable);
+ expect(mockOnDelete).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('theming', () => {
+ it('applies light theme surface, border, and text colors', async () => {
+ mockUseThemeColors.mockReturnValue({ colors: Colors.light, isDark: false });
+
+ const { toJSON, getByText } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const containerStyle = StyleSheet.flatten(tree.props.style);
+
+ expect(containerStyle.backgroundColor).toBe(Colors.light.surface);
+ expect(containerStyle.borderColor).toBe(Colors.light.border);
+
+ const nameStyle = StyleSheet.flatten(getByText('Base Camp').props.style);
+ expect(nameStyle.color).toBe(Colors.light.text);
+ });
+
+ it('applies dark theme surface, border, text, and icon colors', async () => {
+ mockUseThemeColors.mockReturnValue({ colors: Colors.dark, isDark: true });
+
+ const { toJSON, getByText } = await render(
+
+ );
+ const tree = toJSON() as any;
+ const containerStyle = StyleSheet.flatten(tree.props.style);
+
+ expect(containerStyle.backgroundColor).toBe(Colors.dark.surface);
+ expect(containerStyle.borderColor).toBe(Colors.dark.border);
+
+ const nameStyle = StyleSheet.flatten(getByText('Base Camp').props.style);
+ expect(nameStyle.color).toBe(Colors.dark.textTertiary);
+
+ const checkboxView = tree.children[0].children[0];
+ const checkboxStyle = StyleSheet.flatten(checkboxView.props.style);
+ expect(checkboxStyle.borderColor).toBe(Colors.dark.success);
+ expect(checkboxStyle.backgroundColor).toBe(Colors.dark.success);
+
+ const pencilIcon = tree.children[2].children[0];
+ const trashIcon = tree.children[3].children[0];
+
+ expect(pencilIcon.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ color: Colors.dark.textTertiary })])
+ );
+ expect(trashIcon.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ color: Colors.dark.danger })])
+ );
+ });
+ });
+});