diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index 10721cb..ee68a4b 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -3,7 +3,15 @@ // app, NOT the Ungoogled Chromium engine — see docs/mobile-architecture.md. import { StatusBar } from 'expo-status-bar'; import { useState, type ReactNode } from 'react'; -import { Keyboard, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { BrowserScreen } from './src/screens/BrowserScreen'; import { ChatScreen } from './src/screens/ChatScreen'; @@ -57,7 +65,12 @@ function AppShell() { const insets = useSafeAreaInsets(); return ( - - + ); } diff --git a/apps/mobile/test/keyboard.test.tsx b/apps/mobile/test/keyboard.test.tsx new file mode 100644 index 0000000..1707bd2 --- /dev/null +++ b/apps/mobile/test/keyboard.test.tsx @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import App from '../App'; +import { flat, hosts, renderScreen, scenes, tabButton } from './harness'; +import { Platform } from './mocks/react-native'; +import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context'; + +// Native geometry is exercised separately with the APK. These tests pin the +// platform wiring so mocked native components cannot claim to prove IME layout. +describe('shell keyboard avoidance', () => { + it('resizes the Android shell containing the scenes and tabs', async () => { + const renderer = await renderScreen(); + const shell = hosts(renderer.root, 'KeyboardAvoidingView').find( + (node) => node.props.enabled === true, + ); + expect(shell).toBeDefined(); + expect(shell!.props.behavior).toBe('height'); + expect(shell!.props.keyboardVerticalOffset).toBe(0); + expect(scenes(shell!)).toHaveLength(4); + expect(tabButton(shell!, 'Chat')).toBeDefined(); + }); + + it('keeps system insets inside the keyboard-aware root without a second offset', async () => { + setMockSafeAreaInsets({ top: 37, right: 12, bottom: 48, left: 12 }); + const renderer = await renderScreen(); + const shell = hosts(renderer.root, 'KeyboardAvoidingView').find( + (node) => node.props.enabled === true, + ); + expect(shell).toBeDefined(); + expect(flat(shell!)).toMatchObject({ paddingTop: 37, paddingLeft: 12, paddingRight: 12 }); + expect(shell!.props.keyboardVerticalOffset).toBe(0); + }); + + it('does not add a second keyboard adjustment to the existing iOS chat', async () => { + Platform.OS = 'ios'; + const renderer = await renderScreen(); + const views = hosts(renderer.root, 'KeyboardAvoidingView'); + const shell = views.find((node) => node.props.enabled === false); + expect(shell).toBeDefined(); + expect(shell!.props.behavior).toBeUndefined(); + expect(views.some((node) => node.props.behavior === 'padding')).toBe(true); + }); +}); diff --git a/apps/mobile/test/safe-area.test.tsx b/apps/mobile/test/safe-area.test.tsx index 8b8821a..2ddd139 100644 --- a/apps/mobile/test/safe-area.test.tsx +++ b/apps/mobile/test/safe-area.test.tsx @@ -11,7 +11,7 @@ import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context'; import type { ReactTestInstance } from 'react-test-renderer'; -const shellRoot = (root: ReactTestInstance) => hosts(root, 'View')[0]; +const shellRoot = (root: ReactTestInstance) => hosts(root, 'KeyboardAvoidingView')[0]; const tabBar = (root: ReactTestInstance) => hostWhere(