Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection, poorConnectionTimeoutID: undefined});
setPoorConnectionTimeoutID(null);

It seems we set shouldSimulatePoorConnection 2 times if shouldSimulatePoorConnection=false. I think we either use else condition or just only setPoorConnectionTimeoutID is null here. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! I forgot to add an earlier return, updated!

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');
});
});