Skip to content
6 changes: 1 addition & 5 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { AppNavigator } from './src/navigation';
import { useTheme } from './src/theme';
import { hardwareService, modelManager, authService, backgroundDownloadService } from './src/services';
import { hardwareService, modelManager, authService } from './src/services';
import logger from './src/utils/logger';
import { useAppStore, useAuthStore } from './src/stores';
import { LockScreen } from './src/screens';
Expand Down Expand Up @@ -75,10 +75,6 @@ function App() {
// Ensure persisted download metadata is loaded before restore logic reads it.
await ensureAppStoreHydrated();

// Request POST_NOTIFICATIONS permission on Android 13+ so system
// DownloadManager shows progress notifications.
backgroundDownloadService.requestNotificationPermission().catch((err) => logger.warn('Failed to request notification permission', err));

// Phase 1: Quick initialization - get app ready to show UI
// Initialize hardware detection
const deviceInfo = await hardwareService.getDeviceInfo();
Expand Down
64 changes: 64 additions & 0 deletions __tests__/rntl/components/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1830,4 +1830,68 @@ describe('ChatInput', () => {
jest.useRealTimers();
});
});

// ============================================================================
// Icon animation on focus / blur (covers handleInputFocus + handleInputBlur)
// ============================================================================
describe('icon animation on focus/blur', () => {
const ICONS_WIDTH = 108;

beforeEach(() => {
const { useReducedMotion } = require('react-native-reanimated');
(useReducedMotion as jest.Mock).mockReturnValue(false);
});

afterEach(() => {
const { useReducedMotion } = require('react-native-reanimated');
(useReducedMotion as jest.Mock).mockReturnValue(false);
});

it('collapses icons width and fades opacity to 0 on focus at 200ms', () => {
const { withTiming } = require('react-native-reanimated');
const { getByTestId } = render(<ChatInput {...defaultProps} />);

act(() => { getByTestId('chat-input').props.onFocus?.(); });

// opacity → 0, width → 0
expect(withTiming).toHaveBeenCalledTimes(2);
expect(withTiming).toHaveBeenCalledWith(0, { duration: 200 });
});

it('restores icons width to 108 and fades opacity to 1 on blur at 250ms', () => {
const { withTiming } = require('react-native-reanimated');
const { getByTestId } = render(<ChatInput {...defaultProps} />);

act(() => { getByTestId('chat-input').props.onBlur?.(); });

expect(withTiming).toHaveBeenCalledWith(1, { duration: 250 });
expect(withTiming).toHaveBeenCalledWith(ICONS_WIDTH, { duration: 250 });
});

it('uses duration 0 for both values on focus when reduced motion is enabled', () => {
const { withTiming, useReducedMotion } = require('react-native-reanimated');
(useReducedMotion as jest.Mock).mockReturnValue(true);

const { getByTestId } = render(<ChatInput {...defaultProps} />);

act(() => { getByTestId('chat-input').props.onFocus?.(); });

expect(withTiming).toHaveBeenCalledTimes(2);
expect(withTiming).toHaveBeenCalledWith(0, { duration: 0 });
expect(withTiming).not.toHaveBeenCalledWith(expect.anything(), { duration: 200 });
});

it('uses duration 0 for both values on blur when reduced motion is enabled', () => {
const { withTiming, useReducedMotion } = require('react-native-reanimated');
(useReducedMotion as jest.Mock).mockReturnValue(true);

const { getByTestId } = render(<ChatInput {...defaultProps} />);

act(() => { getByTestId('chat-input').props.onBlur?.(); });

expect(withTiming).toHaveBeenCalledWith(1, { duration: 0 });
expect(withTiming).toHaveBeenCalledWith(ICONS_WIDTH, { duration: 0 });
expect(withTiming).not.toHaveBeenCalledWith(expect.anything(), { duration: 250 });
});
});
});
Loading
Loading