From df394019ccb2863fdb81ac1e6a29e37f1df02e8a Mon Sep 17 00:00:00 2001 From: Yossi Elkrief Date: Mon, 2 Mar 2026 13:02:50 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20full=20app=20integration=20=E2=80=94=20?= =?UTF-8?q?all=20screens=20wired=20and=20working?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/App.tsx | 36 +++++++++++++++++++ mobile/jest.setup.ts | 13 +++++++ mobile/src/__tests__/test-utils.tsx | 36 +++++++++++++++++++ mobile/src/hooks/usePoseStream.ts | 6 +--- mobile/src/navigation/MainTabs.tsx | 39 ++------------------- mobile/src/screens/SettingsScreen/index.tsx | 7 ++-- 6 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 mobile/src/__tests__/test-utils.tsx diff --git a/mobile/App.tsx b/mobile/App.tsx index 4b452af..ff6fa81 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -3,10 +3,46 @@ import { NavigationContainer, DarkTheme } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { StatusBar } from 'expo-status-bar'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { apiService } from '@/services/api.service'; +import { rssiService } from '@/services/rssi.service'; +import { wsService } from '@/services/ws.service'; import { ThemeProvider } from './src/theme/ThemeContext'; +import { usePoseStore } from './src/stores/poseStore'; +import { useSettingsStore } from './src/stores/settingsStore'; import { RootNavigator } from './src/navigation/RootNavigator'; export default function App() { + const serverUrl = useSettingsStore((state) => state.serverUrl); + const rssiScanEnabled = useSettingsStore((state) => state.rssiScanEnabled); + + useEffect(() => { + apiService.setBaseUrl(serverUrl); + const unsubscribe = wsService.subscribe(usePoseStore.getState().handleFrame); + wsService.connect(serverUrl); + + return () => { + unsubscribe(); + wsService.disconnect(); + }; + }, [serverUrl]); + + useEffect(() => { + if (!rssiScanEnabled) { + rssiService.stopScanning(); + return; + } + + const unsubscribe = rssiService.subscribe(() => { + // Consumers can subscribe elsewhere for RSSI events. + }); + rssiService.startScanning(2000); + + return () => { + unsubscribe(); + rssiService.stopScanning(); + }; + }, [rssiScanEnabled]); + useEffect(() => { (globalThis as { __appStartTime?: number }).__appStartTime = Date.now(); }, []); diff --git a/mobile/jest.setup.ts b/mobile/jest.setup.ts index b61a2a8..44de669 100644 --- a/mobile/jest.setup.ts +++ b/mobile/jest.setup.ts @@ -9,3 +9,16 @@ jest.mock('react-native-wifi-reborn', () => ({ jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock') ); + +jest.mock('react-native-webview', () => { + const React = require('react'); + const { View } = require('react-native'); + + const MockWebView = (props: unknown) => React.createElement(View, props); + + return { + __esModule: true, + default: MockWebView, + WebView: MockWebView, + }; +}); diff --git a/mobile/src/__tests__/test-utils.tsx b/mobile/src/__tests__/test-utils.tsx new file mode 100644 index 0000000..d9d0d50 --- /dev/null +++ b/mobile/src/__tests__/test-utils.tsx @@ -0,0 +1,36 @@ +import React, { PropsWithChildren } from 'react'; +import { render, type RenderOptions } from '@testing-library/react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { ThemeProvider } from '@/theme/ThemeContext'; + +type TestProvidersProps = PropsWithChildren; + +const TestProviders = ({ children }: TestProvidersProps) => ( + + + {children} + + +); + +const TestProvidersWithNavigation = ({ children }: TestProvidersProps) => ( + + {children} + +); + +interface RenderWithProvidersOptions extends Omit { + withNavigation?: boolean; +} + +export const renderWithProviders = ( + ui: React.ReactElement, + { withNavigation, ...options }: RenderWithProvidersOptions = {}, +) => { + return render(ui, { + ...options, + wrapper: withNavigation ? TestProvidersWithNavigation : TestProviders, + }); +}; diff --git a/mobile/src/hooks/usePoseStream.ts b/mobile/src/hooks/usePoseStream.ts index 10cf8b6..bebab71 100644 --- a/mobile/src/hooks/usePoseStream.ts +++ b/mobile/src/hooks/usePoseStream.ts @@ -1,7 +1,6 @@ import { useEffect } from 'react'; import { wsService } from '@/services/ws.service'; import { usePoseStore } from '@/stores/poseStore'; -import { useSettingsStore } from '@/stores/settingsStore'; export interface UsePoseStreamResult { connectionStatus: ReturnType['connectionStatus']; @@ -10,7 +9,6 @@ export interface UsePoseStreamResult { } export function usePoseStream(): UsePoseStreamResult { - const serverUrl = useSettingsStore((state) => state.serverUrl); const connectionStatus = usePoseStore((state) => state.connectionStatus); const lastFrame = usePoseStore((state) => state.lastFrame); const isSimulated = usePoseStore((state) => state.isSimulated); @@ -19,13 +17,11 @@ export function usePoseStream(): UsePoseStreamResult { const unsubscribe = wsService.subscribe((frame) => { usePoseStore.getState().handleFrame(frame); }); - wsService.connect(serverUrl); return () => { unsubscribe(); - wsService.disconnect(); }; - }, [serverUrl]); + }, []); return { connectionStatus, lastFrame, isSimulated }; } diff --git a/mobile/src/navigation/MainTabs.tsx b/mobile/src/navigation/MainTabs.tsx index 615a5c4..45743e0 100644 --- a/mobile/src/navigation/MainTabs.tsx +++ b/mobile/src/navigation/MainTabs.tsx @@ -1,10 +1,11 @@ -import React, { Suspense, useEffect, useState } from 'react'; +import React, { Suspense } from 'react'; import { ActivityIndicator } from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Ionicons } from '@expo/vector-icons'; import { ThemedText } from '../components/ThemedText'; import { ThemedView } from '../components/ThemedView'; import { colors } from '../theme/colors'; +import { useMatStore } from '../stores/matStore'; import { MainTabsParamList } from './types'; const createPlaceholder = (label: string) => { @@ -74,29 +75,6 @@ const toIconName = (routeName: keyof MainTabsParamList) => { } }; -const getMatAlertCount = async (): Promise => { - try { - const mod = (await import('../stores/matStore')) as Record; - const candidates = [mod.useMatStore, mod.useStore].filter((candidate) => { - return ( - !!candidate && - typeof candidate === 'function' && - typeof (candidate as { getState?: () => unknown }).getState === 'function' - ); - }) as Array<{ getState: () => { alerts?: unknown[] } }>; - - for (const store of candidates) { - const alerts = store.getState().alerts; - if (Array.isArray(alerts)) { - return alerts.length; - } - } - } catch { - return 0; - } - return 0; -}; - const screens: ReadonlyArray<{ name: keyof MainTabsParamList; component: React.ComponentType }> = [ { name: 'Live', component: LiveScreen }, { name: 'Vitals', component: VitalsScreen }, @@ -114,18 +92,7 @@ const Suspended = ({ component: Component }: { component: React.ComponentType }) ); export const MainTabs = () => { - const [matAlertCount, setMatAlertCount] = useState(0); - - useEffect(() => { - const readCount = async () => { - const count = await getMatAlertCount(); - setMatAlertCount(count); - }; - - void readCount(); - const timer = setInterval(readCount, 2000); - return () => clearInterval(timer); - }, []); + const matAlertCount = useMatStore((state) => state.alerts.length); return ( { const intervalSummary = useMemo(() => `${scanInterval}s`, [scanInterval]); const handleSaveUrl = () => { - setServerUrl(draftUrl); - apiService.setBaseUrl(draftUrl); + const newUrl = draftUrl.trim(); + setServerUrl(newUrl); wsService.disconnect(); - wsService.connect(draftUrl); + wsService.connect(newUrl); + apiService.setBaseUrl(newUrl); }; const handleOpenGitHub = async () => {