diff --git a/src/components/__tests__/IconPicker.test.tsx b/src/components/__tests__/IconPicker.test.tsx
new file mode 100644
index 0000000..80e94cd
--- /dev/null
+++ b/src/components/__tests__/IconPicker.test.tsx
@@ -0,0 +1,371 @@
+import React from 'react';
+import { StyleSheet, Platform, KeyboardAvoidingView, FlatList } from 'react-native';
+import { render, fireEvent } from '@testing-library/react-native';
+import IconPicker from '../IconPicker';
+import { useThemeColors } from '../../utils/useThemeColors';
+import { Colors, Spacing, BorderRadius } from '../../utils/theme';
+import * as i18nModule from '../../utils/i18n';
+import { t } from '../../utils/i18n';
+
+jest.mock('../../utils/useThemeColors', () => ({
+ useThemeColors: jest.fn(),
+}));
+
+const mockUseThemeColors = useThemeColors as jest.Mock;
+
+describe('IconPicker', () => {
+ const mockOnSelect = jest.fn();
+ const mockOnClose = jest.fn();
+ const originalPlatform = Platform.OS;
+
+ let capturedKavProps: any = null;
+ let capturedFlatListProps: any = null;
+ let kavSpy: jest.SpyInstance;
+ let flatListSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseThemeColors.mockReturnValue({ colors: Colors.light, isDark: false });
+ Object.defineProperty(Platform, 'OS', {
+ value: 'android',
+ writable: true,
+ configurable: true,
+ });
+
+ capturedKavProps = null;
+ const origKavRender = KeyboardAvoidingView.prototype.render;
+ kavSpy = jest.spyOn(KeyboardAvoidingView.prototype, 'render').mockImplementation(function (
+ this: any
+ ) {
+ capturedKavProps = this.props;
+ return origKavRender.call(this);
+ });
+
+ capturedFlatListProps = null;
+ const origFlatListRender = FlatList.prototype.render;
+ flatListSpy = jest.spyOn(FlatList.prototype, 'render').mockImplementation(function (this: any) {
+ capturedFlatListProps = this.props;
+ return origFlatListRender.call(this);
+ });
+ });
+
+ afterEach(() => {
+ kavSpy?.mockRestore();
+ flatListSpy?.mockRestore();
+ Object.defineProperty(Platform, 'OS', {
+ value: originalPlatform,
+ writable: true,
+ configurable: true,
+ });
+ });
+
+ const getPickerElements = (getByText: any) => {
+ const title = getByText(t('manage.selectIcon'));
+ const content = title.parent as any;
+ const keyboardAvoidingView = content.parent as any;
+ const modal = keyboardAvoidingView.parent as any;
+ const searchInput = content.children[1] as any;
+ const flatList = content.children[2] as any;
+ const closeButton = content.children[3] as any;
+
+ return {
+ title,
+ content,
+ keyboardAvoidingView,
+ modal,
+ searchInput,
+ flatList,
+ closeButton,
+ };
+ };
+
+ const getIconCells = (root: any) =>
+ (root as any).queryAll(
+ (node: any) => node.props?.style && StyleSheet.flatten(node.props.style)?.aspectRatio === 1
+ );
+
+ describe('rendering and visibility', () => {
+ it('renders modal with visible=true, transparent=true, and slide animation', async () => {
+ const { getByText } = await render(
+
+ );
+ const { modal } = getPickerElements(getByText);
+
+ expect(modal.props.visible).toBe(true);
+ expect(modal.props.transparent).toBe(true);
+ expect(modal.props.animationType).toBe('slide');
+ });
+
+ it('renders null or invisible modal when visible prop is false', async () => {
+ const { toJSON, queryByText } = await render(
+
+ );
+
+ const title = queryByText(t('manage.selectIcon'));
+ if (title) {
+ const { modal } = getPickerElements(queryByText);
+ expect(modal.props.visible).toBe(false);
+ } else {
+ expect(toJSON()).toBeNull();
+ }
+ });
+
+ it('renders header title and cancel button text using localization', async () => {
+ const { getByText } = await render(
+
+ );
+
+ expect(getByText(t('manage.selectIcon'))).toBeTruthy();
+ expect(getByText(t('common.cancel'))).toBeTruthy();
+ });
+
+ it('configures search TextInput with expected attributes', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ expect(searchInput.props.value).toBe('');
+ expect(searchInput.props.autoCorrect).toBe(false);
+ expect(searchInput.props.autoCapitalize).toBe('none');
+ });
+
+ it('falls back to "Search" placeholder when localized string is empty', async () => {
+ const spy = jest.spyOn(i18nModule, 't').mockImplementation((key: string) => {
+ if (key === 'manage.searchIcon') return '';
+ if (key === 'manage.selectIcon') return 'Select an icon';
+ return key;
+ });
+
+ const { getByPlaceholderText } = await render(
+
+ );
+
+ expect(getByPlaceholderText('Search')).toBeTruthy();
+ spy.mockRestore();
+ });
+
+ it('configures FlatList with 5 columns, keyExtractor, and row style', async () => {
+ await render();
+
+ expect(capturedFlatListProps.numColumns).toBe(5);
+ expect(capturedFlatListProps.keyExtractor('star')).toBe('star');
+ expect(capturedFlatListProps.data.length).toBeGreaterThan(50);
+ expect(capturedFlatListProps.columnWrapperStyle).toBeDefined();
+ });
+
+ it('applies layout styling to content container', async () => {
+ const { getByText } = await render(
+
+ );
+ const { content } = getPickerElements(getByText);
+ const contentStyle = StyleSheet.flatten(content.props.style);
+
+ expect(contentStyle.height).toBe('70%');
+ expect(contentStyle.borderTopLeftRadius).toBe(BorderRadius.xl);
+ expect(contentStyle.borderTopRightRadius).toBe(BorderRadius.xl);
+ expect(contentStyle.padding).toBe(Spacing.lg);
+ });
+ });
+
+ describe('platform keyboard avoiding behavior', () => {
+ it('uses height behavior on Android', async () => {
+ Object.defineProperty(Platform, 'OS', {
+ value: 'android',
+ writable: true,
+ configurable: true,
+ });
+
+ await render();
+
+ expect(capturedKavProps.behavior).toBe('height');
+ });
+
+ it('uses padding behavior on iOS', async () => {
+ Object.defineProperty(Platform, 'OS', {
+ value: 'ios',
+ writable: true,
+ configurable: true,
+ });
+
+ await render();
+
+ expect(capturedKavProps.behavior).toBe('padding');
+ });
+ });
+
+ describe('search filtering', () => {
+ it('filters icons matching query', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ await fireEvent.changeText(searchInput, 'star');
+
+ expect(capturedFlatListProps.data).toEqual(['star']);
+ });
+
+ it('performs case-insensitive search filtering', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ await fireEvent.changeText(searchInput, 'STAR');
+
+ expect(capturedFlatListProps.data).toEqual(['star']);
+ });
+
+ it('matches partial substrings across icon names', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ await fireEvent.changeText(searchInput, 'code');
+
+ expect(capturedFlatListProps.data).toEqual(['code-braces', 'code-tags']);
+ });
+
+ it('returns empty list when no icons match the query', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ await fireEvent.changeText(searchInput, 'unmatchedqueryxyz');
+
+ expect(capturedFlatListProps.data).toEqual([]);
+ });
+
+ it('restores all icons when search is cleared', async () => {
+ const { getByPlaceholderText } = await render(
+
+ );
+ const totalCount = capturedFlatListProps.data.length;
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+
+ await fireEvent.changeText(searchInput, 'music');
+ expect(capturedFlatListProps.data.length).toBe(1);
+
+ await fireEvent.changeText(searchInput, '');
+ expect(capturedFlatListProps.data.length).toBe(totalCount);
+ });
+ });
+
+ describe('user interactions', () => {
+ it('calls onSelect with the selected icon when an icon cell is pressed', async () => {
+ const { root, getByPlaceholderText } = await render(
+
+ );
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+ await fireEvent.changeText(searchInput, 'heart');
+
+ const iconCells = getIconCells(root);
+ expect(iconCells.length).toBeGreaterThan(0);
+ await fireEvent.press(iconCells[0]);
+
+ expect(mockOnSelect).toHaveBeenCalledWith('heart');
+ expect(mockOnSelect).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onClose when the cancel button is pressed', async () => {
+ const { getByText } = await render(
+
+ );
+ const cancelBtn = getByText(t('common.cancel'));
+
+ await fireEvent.press(cancelBtn);
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onClose when onRequestClose is triggered by the Modal', async () => {
+ const { getByText } = await render(
+
+ );
+ const { modal } = getPickerElements(getByText);
+
+ modal.props.onRequestClose();
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('theming', () => {
+ it('applies light theme surface, background, and text colors', async () => {
+ mockUseThemeColors.mockReturnValue({ colors: Colors.light, isDark: false });
+
+ const { root, getByText, getByPlaceholderText } = await render(
+
+ );
+ const { content, keyboardAvoidingView } = getPickerElements(getByText);
+
+ expect(StyleSheet.flatten(keyboardAvoidingView.props.style).backgroundColor).toBe(
+ Colors.light.overlay
+ );
+
+ expect(StyleSheet.flatten(content.props.style).backgroundColor).toBe(Colors.light.surface);
+
+ const title = getByText(t('manage.selectIcon'));
+ expect(StyleSheet.flatten(title.props.style).color).toBe(Colors.light.text);
+
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+ const searchStyle = StyleSheet.flatten(searchInput.props.style);
+ expect(searchStyle.backgroundColor).toBe(Colors.light.background);
+ expect(searchStyle.color).toBe(Colors.light.text);
+ expect(searchStyle.borderColor).toBe(Colors.light.border);
+ expect(searchInput.props.placeholderTextColor).toBe(Colors.light.textSecondary);
+
+ const cancelText = getByText(t('common.cancel'));
+ expect(StyleSheet.flatten(cancelText.props.style).color).toBe(Colors.light.textSecondary);
+
+ const iconCells = getIconCells(root);
+ expect(iconCells.length).toBeGreaterThan(0);
+ const cellStyle = StyleSheet.flatten(iconCells[0].props.style);
+ expect(cellStyle.backgroundColor).toBe(Colors.light.surfaceAlt);
+
+ const iconNode = iconCells[0].children[0];
+ expect(iconNode.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ color: Colors.light.text })])
+ );
+ });
+
+ it('applies dark theme surface, background, and text colors', async () => {
+ mockUseThemeColors.mockReturnValue({ colors: Colors.dark, isDark: true });
+
+ const { root, getByText, getByPlaceholderText } = await render(
+
+ );
+ const { content, keyboardAvoidingView } = getPickerElements(getByText);
+
+ expect(StyleSheet.flatten(keyboardAvoidingView.props.style).backgroundColor).toBe(
+ Colors.dark.overlay
+ );
+
+ expect(StyleSheet.flatten(content.props.style).backgroundColor).toBe(Colors.dark.surface);
+
+ const title = getByText(t('manage.selectIcon'));
+ expect(StyleSheet.flatten(title.props.style).color).toBe(Colors.dark.text);
+
+ const searchInput = getByPlaceholderText(t('manage.searchIcon'));
+ const searchStyle = StyleSheet.flatten(searchInput.props.style);
+ expect(searchStyle.backgroundColor).toBe(Colors.dark.background);
+ expect(searchStyle.color).toBe(Colors.dark.text);
+ expect(searchStyle.borderColor).toBe(Colors.dark.border);
+ expect(searchInput.props.placeholderTextColor).toBe(Colors.dark.textSecondary);
+
+ const cancelText = getByText(t('common.cancel'));
+ expect(StyleSheet.flatten(cancelText.props.style).color).toBe(Colors.dark.textSecondary);
+
+ const iconCells = getIconCells(root);
+ expect(iconCells.length).toBeGreaterThan(0);
+ const cellStyle = StyleSheet.flatten(iconCells[0].props.style);
+ expect(cellStyle.backgroundColor).toBe(Colors.dark.surfaceAlt);
+
+ const iconNode = iconCells[0].children[0];
+ expect(iconNode.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ color: Colors.dark.text })])
+ );
+ });
+ });
+});