diff --git a/src/frontend/screens/Exchange/ExchangeScreenContent.test.tsx b/src/frontend/screens/Exchange/ExchangeScreenContent.test.tsx new file mode 100644 index 000000000..fbafff82b --- /dev/null +++ b/src/frontend/screens/Exchange/ExchangeScreenContent.test.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import type {SyncState} from '@comapeo/core-react'; +import { + createNavigationContainerRef, + NavigationContainer, +} from '@react-navigation/native'; +import {createNativeStackNavigator} from '@react-navigation/native-stack'; +import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {act, render, screen} from '@testing-library/react-native'; +import {IntlProvider} from 'react-intl'; +import {Text} from 'react-native'; + +import {ExchangeScreenContent} from './ExchangeScreenContent'; + +const mockConnectServers = jest.fn(); +const mockDisconnectServers = jest.fn(); +const mockSetAutostopDataSyncTimeout = jest.fn(); + +jest.mock('@comapeo/core-react', () => ({ + ...jest.requireActual('@comapeo/core-react'), + useDataSyncProgress: () => null, + useIsArchiveDevice: () => ({data: false}), + useStartSync: () => ({mutate: jest.fn()}), + useStopSync: () => ({mutate: jest.fn()}), + useConnectSyncServers: () => ({mutate: mockConnectServers}), + useDisconnectSyncServers: () => ({mutate: mockDisconnectServers}), + useSetAutostopDataSyncTimeout: () => ({ + mutate: mockSetAutostopDataSyncTimeout, + }), +})); + +jest.mock('../../contexts/ActiveProjectContext', () => ({ + useActiveProject: () => ({projectId: 'project-id'}), +})); + +jest.mock('../../hooks/server/projects', () => ({ + useActiveArchiveServer: () => undefined, +})); + +jest.mock('../../hooks/useLocalDiscoveryState', () => ({ + useLocalDiscoveryState: () => 'CoMapeo Test Wi-Fi', +})); + +const syncState = { + initial: {isSyncEnabled: true}, + data: {isSyncEnabled: false}, + remoteDeviceSyncState: {}, +} satisfies SyncState; + +type StackParams = {Sync: undefined; Other: undefined}; + +const Stack = createNativeStackNavigator(); + +const queryClient = new QueryClient(); + +const Wrapper = ({children}: {children: React.ReactNode}) => ( + + {children} + +); + +test('connects servers on focus and disconnects them on blur', async () => { + const navigationRef = createNavigationContainerRef(); + + await render( + + + + + {() => } + + {() => Other} + + + , + {wrapper: Wrapper}, + ); + + await expect(screen.findByText('No devices found.')).resolves.toBeVisible(); + + expect(mockSetAutostopDataSyncTimeout).toHaveBeenCalledTimes(1); + expect(mockSetAutostopDataSyncTimeout).toHaveBeenCalledWith({after: null}); + expect(mockConnectServers).toHaveBeenCalledTimes(1); + expect(mockDisconnectServers).not.toHaveBeenCalled(); + + await act(async () => { + navigationRef.navigate('Other'); + }); + + await expect(screen.findByText('Other')).resolves.toBeVisible(); + + expect(mockDisconnectServers).toHaveBeenCalledTimes(1); + expect(mockConnectServers).toHaveBeenCalledTimes(1); +}); diff --git a/src/frontend/screens/Exchange/ExchangeScreenContent.tsx b/src/frontend/screens/Exchange/ExchangeScreenContent.tsx index a172bb095..ec89974a2 100644 --- a/src/frontend/screens/Exchange/ExchangeScreenContent.tsx +++ b/src/frontend/screens/Exchange/ExchangeScreenContent.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { + useConnectSyncServers, useDataSyncProgress, + useDisconnectSyncServers, useIsArchiveDevice, + useSetAutostopDataSyncTimeout, useStartSync, + useStopSync, type SyncState, } from '@comapeo/core-react'; import {useFocusEffect} from '@react-navigation/native'; @@ -128,9 +132,15 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { const {formatMessage: t} = useIntl(); const queryClient = useQueryClient(); const navigation = useNavigationFromRoot(); - const {projectApi, projectId} = useActiveProject(); + const {projectId} = useActiveProject(); const progress = useDataSyncProgress({projectId}); const startSync = useStartSync({projectId}); + const stopSync = useStopSync({projectId}); + const {mutate: connectServers} = useConnectSyncServers({projectId}); + const {mutate: disconnectServers} = useDisconnectSyncServers({projectId}); + const {mutate: setAutostopDataSyncTimeout} = useSetAutostopDataSyncTimeout({ + projectId, + }); const {data: isArchive} = useIsArchiveDevice(); const currentMediaSetting = isArchive ? 'everything' : 'previews'; @@ -157,13 +167,13 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { useFocusEffect( React.useCallback(() => { // Unset sync autostop when screen mounts - projectApi.$sync.setAutostopDataSyncTimeout(null); + setAutostopDataSyncTimeout({after: null}); // Connects to Servers - projectApi.$sync.connectServers(); + connectServers(); return () => { - projectApi.$sync.disconnectServers(); + disconnectServers(); }; - }, [projectApi]), + }, [connectServers, disconnectServers, setAutostopDataSyncTimeout]), ); const shouldAutostopSyncWhenLeavingScreen = @@ -174,7 +184,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { React.useCallback(() => { const unsubscribe = navigation.addListener('beforeRemove', () => { if (shouldAutostopSyncWhenLeavingScreen) { - projectApi.$sync.setAutostopDataSyncTimeout(30_000); + setAutostopDataSyncTimeout({after: 30_000}); } queryClient.invalidateQueries({ queryKey: [ROOT_QUERY_KEY, 'projects', projectId], @@ -187,7 +197,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { }; }, [ navigation, - projectApi, + setAutostopDataSyncTimeout, queryClient, projectId, shouldAutostopSyncWhenLeavingScreen, @@ -245,7 +255,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { fullSize={true} onPress={() => { // TODO: Catch/surface error - projectApi.$sync.stop(); + stopSync.mutate(undefined); }} text={t(m.stop)} renderIcon={({size, color}) => ( @@ -271,7 +281,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { fullSize={true} onPress={() => { // TODO: Catch/surface error - projectApi.$sync.stop(); + stopSync.mutate(undefined); }} text={t(m.stop)} renderIcon={({size, color}) => ( @@ -299,7 +309,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { fullSize={true} onPress={() => { // TODO: Catch/surface error - projectApi.$sync.stop(); + stopSync.mutate(undefined); }} text={t(m.stop)} renderIcon={({size, color}) => ( @@ -325,7 +335,7 @@ export const ExchangeScreenContent = ({syncState}: {syncState: SyncState}) => { fullSize={true} onPress={() => { // TODO: Catch/surface error - projectApi.$sync.stop(); + stopSync.mutate(undefined); }} text={t(m.stop)} renderIcon={({size, color}) => (