Skip to content

Commit

Permalink
Merge pull request #53698 from callstack-internal/VickyStash/feature/…
Browse files Browse the repository at this point in the history
…51331-poor-connection-simulation

Implement a debug setting that simulates a poor connection by going online/offline a bunch
  • Loading branch information
tgolen authored Dec 17, 2024
2 parents 5a0e70a + ba65360 commit 919c59d
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 11 deletions.
26 changes: 16 additions & 10 deletions src/components/TestToolMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -9,18 +8,13 @@ import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Network as NetworkOnyx, User as UserOnyx} from '@src/types/onyx';
import type {User as UserOnyx} from '@src/types/onyx';
import Button from './Button';
import {withNetwork} from './OnyxProvider';
import Switch from './Switch';
import TestCrash from './TestCrash';
import TestToolRow from './TestToolRow';
import Text from './Text';

type TestToolMenuProps = {
/** Network object in Onyx */
network: OnyxEntry<NetworkOnyx>;
};
const USER_DEFAULT: UserOnyx = {
shouldUseStagingServer: undefined,
isSubscribedToNewsletter: false,
Expand All @@ -30,7 +24,8 @@ const USER_DEFAULT: UserOnyx = {
isDebugModeEnabled: false,
};

function TestToolMenu({network}: TestToolMenuProps) {
function TestToolMenu() {
const [network] = useOnyx(ONYXKEYS.NETWORK);
const [user = USER_DEFAULT] = useOnyx(ONYXKEYS.USER);
const [isUsingImportedState] = useOnyx(ONYXKEYS.IS_USING_IMPORTED_STATE);
const shouldUseStagingServer = user?.shouldUseStagingServer ?? ApiUtils.isUsingStagingApi();
Expand Down Expand Up @@ -74,7 +69,17 @@ function TestToolMenu({network}: TestToolMenuProps) {
accessibilityLabel="Force offline"
isOn={!!network?.shouldForceOffline}
onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)}
disabled={isUsingImportedState}
disabled={!!isUsingImportedState || !!network?.shouldSimulatePoorConnection || network?.shouldFailAllRequests}
/>
</TestToolRow>

{/* When toggled the app will randomly change internet connection every 2-5 seconds */}
<TestToolRow title={translate('initialSettingsPage.troubleshoot.simulatePoorConnection')}>
<Switch
accessibilityLabel="Simulate poor internet connection"
isOn={!!network?.shouldSimulatePoorConnection}
onToggle={() => Network.setShouldSimulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)}
disabled={!!isUsingImportedState || !!network?.shouldFailAllRequests || network?.shouldForceOffline}
/>
</TestToolRow>

Expand All @@ -84,6 +89,7 @@ function TestToolMenu({network}: TestToolMenuProps) {
accessibilityLabel="Simulate failing network requests"
isOn={!!network?.shouldFailAllRequests}
onToggle={() => Network.setShouldFailAllRequests(!network?.shouldFailAllRequests)}
disabled={!!network?.shouldForceOffline || network?.shouldSimulatePoorConnection}
/>
</TestToolRow>

Expand Down Expand Up @@ -112,4 +118,4 @@ function TestToolMenu({network}: TestToolMenuProps) {

TestToolMenu.displayName = 'TestToolMenu';

export default withNetwork()(TestToolMenu);
export default TestToolMenu;
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ const translations = {
testingPreferences: 'Testing preferences',
useStagingServer: 'Use Staging Server',
forceOffline: 'Force offline',
simulatePoorConnection: 'Simulate poor internet connection',
simulatFailingNetworkRequests: 'Simulate failing network requests',
authenticationStatus: 'Authentication status',
deviceCredentials: 'Device credentials',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,7 @@ const translations = {
testingPreferences: 'Preferencias para Tests',
useStagingServer: 'Usar servidor “staging”',
forceOffline: 'Forzar desconexión',
simulatePoorConnection: 'Simular una conexión a internet deficiente',
simulatFailingNetworkRequests: 'Simular fallos en solicitudes de red',
authenticationStatus: 'Estado de autenticación',
deviceCredentials: 'Credenciales del dispositivo',
Expand Down
75 changes: 75 additions & 0 deletions src/libs/NetworkConnection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import NetInfo from '@react-native-community/netinfo';
import {differenceInHours} from 'date-fns/differenceInHours';
import isBoolean from 'lodash/isBoolean';
import throttle from 'lodash/throttle';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type Network from '@src/types/onyx/Network';
import type {ConnectionChanges} from '@src/types/onyx/Network';
import * as NetworkActions from './actions/Network';
import AppStateMonitor from './AppStateMonitor';
import Log from './Log';
Expand Down Expand Up @@ -51,6 +54,7 @@ const triggerReconnectionCallbacks = throttle(
* then all of the reconnection callbacks are triggered
*/
function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void {
trackConnectionChanges();
NetworkActions.setIsOffline(isCurrentlyOffline, reason);

// When reconnecting, ie, going from offline to online, all the reconnection callbacks
Expand All @@ -64,12 +68,20 @@ function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void {

// Update the offline status in response to changes in shouldForceOffline
let shouldForceOffline = false;
let isPoorConnectionSimulated: boolean | undefined;
let connectionChanges: ConnectionChanges | undefined;
Onyx.connect({
key: ONYXKEYS.NETWORK,
callback: (network) => {
if (!network) {
return;
}

simulatePoorConnection(network);

isPoorConnectionSimulated = !!network.shouldSimulatePoorConnection;
connectionChanges = network.connectionChanges;

const currentShouldForceOffline = !!network.shouldForceOffline;
if (currentShouldForceOffline === shouldForceOffline) {
return;
Expand Down Expand Up @@ -104,6 +116,69 @@ Onyx.connect({
},
});

function simulatePoorConnection(network: Network) {
// Starts random network status change when shouldSimulatePoorConnection is turned into true
// or after app restart if shouldSimulatePoorConnection is true already
if (!isPoorConnectionSimulated && !!network.shouldSimulatePoorConnection) {
clearTimeout(network.poorConnectionTimeoutID);
setRandomNetworkStatus(true);
}

// Fetch the NetInfo state to set the correct offline status when shouldSimulatePoorConnection is turned into false
if (isPoorConnectionSimulated && !network.shouldSimulatePoorConnection) {
NetInfo.fetch().then((state) => {
const isInternetUnreachable = !state.isInternetReachable;
const stringifiedState = JSON.stringify(state);
setOfflineStatus(isInternetUnreachable || !isServerUp, 'NetInfo checked if the internet is reachable');
Log.info(
`[NetworkStatus] The poor connection simulation mode was turned off. Getting the device network status from NetInfo. Network state: ${stringifiedState}. Setting the offline status to: ${isInternetUnreachable}.`,
);
});
}
}

/** Sets online/offline connection randomly every 2-5 seconds */
function setRandomNetworkStatus(initialCall = false) {
// The check to ensure no new timeouts are scheduled after poor connection simulation is stopped
if (!isPoorConnectionSimulated && !initialCall) {
return;
}

const statuses = [CONST.NETWORK.NETWORK_STATUS.OFFLINE, CONST.NETWORK.NETWORK_STATUS.ONLINE];
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
const randomInterval = Math.random() * (5000 - 2000) + 2000; // random interval between 2-5 seconds
Log.info(`[NetworkConnection] Set connection status "${randomStatus}" for ${randomInterval} sec`);

setOfflineStatus(randomStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE);

const timeoutID = setTimeout(setRandomNetworkStatus, randomInterval);
NetworkActions.setPoorConnectionTimeoutID(timeoutID);
}

/** Tracks how many times the connection has changed within the time period */
function trackConnectionChanges() {
if (!connectionChanges?.startTime) {
NetworkActions.setConnectionChanges({startTime: new Date().getTime(), amount: 1});
return;
}

const diffInHours = differenceInHours(new Date(), connectionChanges.startTime);
const newAmount = (connectionChanges.amount ?? 0) + 1;

if (diffInHours < 1) {
NetworkActions.setConnectionChanges({amount: newAmount});
return;
}

Log.info(
`[NetworkConnection] Connection has changed ${newAmount} time(s) for the last ${diffInHours} hour(s). Poor connection simulation is turned ${
isPoorConnectionSimulated ? 'on' : 'off'
}`,
);

NetworkActions.setConnectionChanges({startTime: new Date().getTime(), amount: 0});
}

/**
* Set up the event listener for NetInfo to tell whether the user has
* internet connectivity or not. This is more reliable than the Pusher
Expand Down
20 changes: 19 additions & 1 deletion src/libs/actions/Network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import type {NetworkStatus} from '@libs/NetworkConnection';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ConnectionChanges} from '@src/types/onyx/Network';

function setIsOffline(isOffline: boolean, reason = '') {
if (reason) {
Expand Down Expand Up @@ -32,4 +33,21 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) {
Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests});
}

export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus};
function setPoorConnectionTimeoutID(poorConnectionTimeoutID: NodeJS.Timeout | undefined) {
Onyx.merge(ONYXKEYS.NETWORK, {poorConnectionTimeoutID});
}

function setShouldSimulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) {
if (!shouldSimulatePoorConnection) {
clearTimeout(poorConnectionTimeoutID);
Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection, poorConnectionTimeoutID: undefined});
return;
}
Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection});
}

function setConnectionChanges(connectionChanges: ConnectionChanges) {
Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges});
}

export {setIsOffline, setShouldForceOffline, setConnectionChanges, setShouldSimulatePoorConnection, setPoorConnectionTimeoutID, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus};
19 changes: 19 additions & 0 deletions src/types/onyx/Network.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type {NetworkStatus} from '@libs/NetworkConnection';

/** The value where connection changes are tracked */
type ConnectionChanges = {
/** Amount of connection changes */
amount?: number;

/** Start time in milliseconds */
startTime?: number;
};

/** Model of network state */
type Network = {
/** Is the network currently offline or not */
Expand All @@ -8,6 +17,15 @@ type Network = {
/** Should the network be forced offline */
shouldForceOffline?: boolean;

/** Whether we should simulate poor connection */
shouldSimulatePoorConnection?: boolean;

/** Poor connection timeout id */
poorConnectionTimeoutID?: NodeJS.Timeout;

/** The value where connection changes are tracked */
connectionChanges?: ConnectionChanges;

/** Whether we should fail all network requests */
shouldFailAllRequests?: boolean;

Expand All @@ -19,3 +37,4 @@ type Network = {
};

export default Network;
export type {ConnectionChanges};
36 changes: 36 additions & 0 deletions tests/unit/NetworkTest.ts → tests/unit/NetworkTest.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {render, screen} from '@testing-library/react-native';
import {sub as dateSubtract} from 'date-fns/sub';
import type {Mock} from 'jest-mock';
import type {OnyxEntry} from 'react-native-onyx';
import MockedOnyx from 'react-native-onyx';
import TestToolMenu from '@components/TestToolMenu';
import * as App from '@libs/actions/App';
import {resetReauthentication} from '@libs/Middleware/Reauthentication';
import CONST from '@src/CONST';
import * as NetworkActions from '@src/libs/actions/Network';
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
import * as PersistedRequests from '@src/libs/actions/PersistedRequests';
import * as PersonalDetails from '@src/libs/actions/PersonalDetails';
Expand Down Expand Up @@ -391,4 +395,36 @@ describe('NetworkTests', () => {
expect(xhr.mock.calls.length).toBe(3);
});
});

test('poor connection simulation', async () => {
const logSpy = jest.spyOn(Log, 'info');

// Given an opened test tool menu
render(<TestToolMenu />);
expect(screen.getByAccessibilityHint('Force offline')).not.toBeDisabled();
expect(screen.getByAccessibilityHint('Simulate failing network requests')).not.toBeDisabled();

// When the connection simulation is turned on
NetworkActions.setShouldSimulatePoorConnection(true, undefined);
await waitForBatchedUpdates();

// Then the connection status change log should be displayed as well as Force offline/Simulate failing network requests toggles should be disabled
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/\[NetworkConnection\] Set connection status "(online|offline)" for (\d+(?:\.\d+)?) sec/));
expect(screen.getByAccessibilityHint('Force offline')).toBeDisabled();
expect(screen.getByAccessibilityHint('Simulate failing network requests')).toBeDisabled();
});

test('connection changes tracking', async () => {
const logSpy = jest.spyOn(Log, 'info');

// Given tracked connection changes started at least an hour ago
Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {amount: 5, startTime: dateSubtract(new Date(), {hours: 1}).getTime()}});
await waitForBatchedUpdates();

// When the connection is changed one more time
NetworkConnection.setOfflineStatus(true);

// Then the log with information about connection changes since the start time should be shown
expect(logSpy).toHaveBeenCalledWith('[NetworkConnection] Connection has changed 6 time(s) for the last 1 hour(s). Poor connection simulation is turned off');
});
});

0 comments on commit 919c59d

Please sign in to comment.