feat: full app integration — all screens wired and working
This commit is contained in:
@@ -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();
|
||||
}, []);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
36
mobile/src/__tests__/test-utils.tsx
Normal file
36
mobile/src/__tests__/test-utils.tsx
Normal file
@@ -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<object>;
|
||||
|
||||
const TestProviders = ({ children }: TestProvidersProps) => (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
|
||||
const TestProvidersWithNavigation = ({ children }: TestProvidersProps) => (
|
||||
<TestProviders>
|
||||
<NavigationContainer>{children}</NavigationContainer>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
interface RenderWithProvidersOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||
withNavigation?: boolean;
|
||||
}
|
||||
|
||||
export const renderWithProviders = (
|
||||
ui: React.ReactElement,
|
||||
{ withNavigation, ...options }: RenderWithProvidersOptions = {},
|
||||
) => {
|
||||
return render(ui, {
|
||||
...options,
|
||||
wrapper: withNavigation ? TestProvidersWithNavigation : TestProviders,
|
||||
});
|
||||
};
|
||||
@@ -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<typeof usePoseStore.getState>['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 };
|
||||
}
|
||||
|
||||
@@ -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<number> => {
|
||||
try {
|
||||
const mod = (await import('../stores/matStore')) as Record<string, unknown>;
|
||||
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 (
|
||||
<Tab.Navigator
|
||||
|
||||
@@ -98,10 +98,11 @@ export const SettingsScreen = () => {
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user