Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automated tests for Unread Indicators feature and final polish #10929

Merged
merged 19 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions __mocks__/@react-native-firebase/crashlytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// <App> uses <ErrorBoundary> and we need to mock the imported crashlytics module
// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475
export default {
log: jest.fn(),
recordError: jest.fn(),
};
8 changes: 7 additions & 1 deletion __mocks__/pusher-js/react-native.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {PusherMock} from 'pusher-js-mock';

export default PusherMock;
class PusherMockWithDisconnect extends PusherMock {
disconnect() {
return jest.fn();
}
}

export default PusherMockWithDisconnect;
44 changes: 44 additions & 0 deletions __mocks__/react-native-safe-area-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {forwardRef} from 'react';
import {View} from 'react-native';

const insets = {
top: 0, right: 0, bottom: 0, left: 0,
};

function withSafeAreaInsets(WrappedComponent) {
const WithSafeAreaInsets = props => (
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
// eslint-disable-next-line react/prop-types
ref={props.forwardedRef}
insets={insets}
/>
);
return forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<WithSafeAreaInsets {...props} forwardedRef={ref} />
));
}

const SafeAreaView = View;
const SafeAreaProvider = props => props.children;
const SafeAreaConsumer = props => props.children(insets);
const SafeAreaInsetsContext = {
Consumer: SafeAreaConsumer,
};

const useSafeAreaFrame = jest.fn(() => ({
x: 0, y: 0, width: 390, height: 844,
}));
const useSafeAreaInsets = jest.fn(() => insets);

export {
SafeAreaProvider,
SafeAreaConsumer,
SafeAreaInsetsContext,
withSafeAreaInsets,
SafeAreaView,
useSafeAreaFrame,
useSafeAreaInsets,
};
72 changes: 72 additions & 0 deletions __mocks__/react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// eslint-disable-next-line no-restricted-imports
import * as ReactNative from 'react-native';
import _ from 'underscore';
import CONST from '../src/CONST';

jest.doMock('react-native', () => {
let url = 'https://new.expensify.com/';
const getInitialURL = () => Promise.resolve(url);

let appState = 'active';
let count = 0;
const changeListeners = {};

// Tests will run with the app in a typical small screen size by default. We do this since the react-native test renderer
// runs against index.native.js source and so anything that is testing a component reliant on withWindowDimensions()
// would be most commonly assumed to be on a mobile phone vs. a tablet or desktop style view. This behavior can be
// overridden by explicitly setting the dimensions inside a test via Dimensions.set()
let dimensions = CONST.TESTING.SCREEN_SIZE.SMALL;

return Object.setPrototypeOf(
{
NativeModules: {
...ReactNative.NativeModules,
BootSplash: {
getVisibilityStatus: jest.fn(),
hide: jest.fn(),
},
StartupTimer: {stop: jest.fn()},
},
Linking: {
...ReactNative.Linking,
getInitialURL,
setInitialURL(newUrl) {
url = newUrl;
},
},
AppState: {
...ReactNative.AppState,
get currentState() {
return appState;
},
emitCurrentTestState(state) {
appState = state;
_.each(changeListeners, listener => listener(appState));
},
addEventListener(type, listener) {
if (type === 'change') {
const originalCount = count;
changeListeners[originalCount] = listener;
++count;
return {
remove: () => {
delete changeListeners[originalCount];
},
};
}

return ReactNative.AppState.addEventListener(type, listener);
},
},
Dimensions: {
...ReactNative.Dimensions,
addEventListener: jest.fn(),
get: () => dimensions,
set: (newDimensions) => {
dimensions = newDimensions;
},
},
},
ReactNative,
);
});
12 changes: 11 additions & 1 deletion __mocks__/urbanairship-react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ const EventType = {
PushReceived: 'pushReceived',
};

export default {
const UrbanAirship = {
setUserNotificationsEnabled: jest.fn(),
clearNotifications: jest.fn(),
addListener: jest.fn(),
getNamedUser: jest.fn(),
enableUserPushNotifications: () => Promise.resolve(false),
setNamedUser: jest.fn(),
removeAllListeners: jest.fn(),
setBadgeNumber: jest.fn(),
};

export default UrbanAirship;

export {
EventType,
UrbanAirship,
};
63 changes: 56 additions & 7 deletions jest/setup.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,64 @@
import fs from 'fs';
import path from 'path';
import 'react-native-gesture-handler/jestSetup';
import _ from 'underscore';

require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests();

// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native-blob-util', () => ({}));

// These two mocks are required as per setup instructions for react-navigation testing
// https://reactnavigation.org/docs/testing/#mocking-native-modules
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
Reanimated.default.call = () => {};
return Reanimated;
});

// Set up manual mocks for methods used in the actions so our test does not fail.
jest.mock('../src/libs/Notification/PushNotification', () => ({
// There is no need for a jest.fn() since we don't need to make assertions against it.
register: () => {},
deregister: () => {},
// The main app uses a NativeModule called BootSplash to show/hide a splash screen. Since we can't use this in the node environment
// where tests run we simulate a behavior where the splash screen is always hidden (similar to web which has no splash screen at all).
Comment on lines +19 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️ I love this comment. It makes it so clear why the mock is necessary!

jest.mock('../src/libs/BootSplash', () => ({
hide: jest.fn(),
getVisibilityStatus: jest.fn().mockResolvedValue('hidden'),
}));

jest.mock('react-native-blob-util', () => ({}));
// Local notifications (a.k.a. browser notifications) do not run in native code. Our jest tests will also run against
// any index.native.js files as they are using a react-native plugin. However, it is useful to mock this behavior so that we
// can test the expected web behavior and see if a browser notification would be shown or not.
jest.mock('../src/libs/Notification/LocalNotification', () => ({
showCommentNotification: jest.fn(),
}));

/**
* @param {String} imagePath
*/
function mockImages(imagePath) {
const imageFilenames = fs.readdirSync(path.resolve(__dirname, `../assets/${imagePath}/`));
// eslint-disable-next-line rulesdir/prefer-early-return
_.each(imageFilenames, (fileName) => {
if (/\.svg/.test(fileName)) {
jest.mock(`../assets/${imagePath}/${fileName}`, () => () => '');
}
});
}

// We are mocking all images so that Icons and other assets cannot break tests. In the testing environment, importing things like .svg
// directly will lead to undefined variables instead of a component or string (which is what React expects). Loading these assets is
// not required as the test environment does not actually render any UI anywhere and just needs them to noop so the test renderer
// (which is a virtual implemented DOM) can do it's thing.
mockImages('images');
mockImages('images/avatars');
mockImages('images/bankicons');
mockImages('images/product-illustrations');
jest.mock('../src/components/Icon/Expensicons', () => {
const reduce = require('underscore').reduce;
const Expensicons = jest.requireActual('../src/components/Icon/Expensicons');
return reduce(Expensicons, (prev, _curr, key) => {
// We set the name of the anonymous mock function here so we can dynamically build the list of mocks and access the
// "name" property to use in accessibility hints for element querying
const fn = () => '';
Object.defineProperty(fn, 'name', {value: key});
return {...prev, [key]: fn};
}, {});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
"<rootDir>/node_modules/"
],
"testMatch": [
"**/tests/ui/**/*.[jt]s?(x)",
"**/tests/unit/**/*.[jt]s?(x)",
"**/tests/actions/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[jt]s?(x)"
Expand Down
7 changes: 7 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,13 @@ const CONST = {
INCORRECT_PASSWORD: 2,
},
},
TESTING: {
SCREEN_SIZE: {
SMALL: {
width: 300, height: 700, scale: 1, fontScale: 1,
},
},
},
};

export default CONST;
3 changes: 2 additions & 1 deletion src/components/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as Expensicons from './Icon/Expensicons';

const propTypes = {
/** Whether checkbox is checked */
isChecked: PropTypes.bool.isRequired,
isChecked: PropTypes.bool,

/** A function that is called when the box/label is pressed */
onPress: PropTypes.func.isRequired,
Expand All @@ -33,6 +33,7 @@ const propTypes = {
};

const defaultProps = {
isChecked: false,
hasError: false,
disabled: false,
style: [],
Expand Down
2 changes: 1 addition & 1 deletion src/components/DisplayNames/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Text from '../Text';

// As we don't have to show tooltips of the Native platform so we simply render the full display names list.
const DisplayNames = props => (
<Text style={props.textStyles} numberOfLines={props.numberOfLines}>
<Text accessibilityLabel={props.accessibilityLabel} style={props.textStyles} numberOfLines={props.numberOfLines}>
{props.fullTitle}
</Text>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/OptionRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ const OptionRow = (props) => {
<TouchableOpacity
ref={el => touchableRef = el}
onPress={(e) => {
e.preventDefault();
if (e) {
e.preventDefault();
}

props.onSelectRow(props.option, touchableRef);
}}
disabled={props.isDisabled}
Expand All @@ -145,7 +148,7 @@ const OptionRow = (props) => {
props.isDisabled && styles.cursorDisabled,
]}
>
<View style={sidebarInnerRowStyle}>
<View accessibilityHint={props.accessibilityHint} style={sidebarInnerRowStyle}>
<View
style={[
styles.flexRow,
Expand Down Expand Up @@ -183,6 +186,7 @@ const OptionRow = (props) => {
}
<View style={contentContainerStyles}>
<DisplayNames
accessibilityLabel="Chat user display names"
fullTitle={props.option.text}
displayNamesWithTooltips={displayNamesWithTooltips}
tooltipEnabled={props.showTitleTooltip}
Expand All @@ -192,6 +196,7 @@ const OptionRow = (props) => {
/>
{props.option.alternateText ? (
<Text
accessibilityLabel={props.alternateTextAccessibilityLabel}
style={alternateTextStyle}
numberOfLines={1}
>
Expand Down
2 changes: 2 additions & 0 deletions src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class BaseOptionsList extends Component {
renderItem({item, index, section}) {
return (
<OptionRow
alternateTextAccessibilityLabel={this.props.optionRowAlternateTextAccessibilityLabel}
accessibilityHint={this.props.optionRowAccessibilityHint}
option={item}
mode={this.props.optionMode}
showTitleTooltip={this.props.showTitleTooltip}
Expand Down
3 changes: 2 additions & 1 deletion src/components/ReportWelcomeText.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ const propTypes = {
policies: PropTypes.shape({
/** The policy name */
name: PropTypes.string,
}).isRequired,
}),

...withLocalizePropTypes,
};

const defaultProps = {
report: {},
policies: {},
};

const ReportWelcomeText = (props) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/UnreadActionIndicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Text from './Text';
import withLocalize, {withLocalizePropTypes} from './withLocalize';

const UnreadActionIndicator = props => (
<View style={styles.unreadIndicatorContainer}>
<View accessibilityLabel="New message line indicator" data-sequence-number={props.sequenceNumber} style={styles.unreadIndicatorContainer}>
<View style={styles.unreadIndicatorLine} />
<Text style={styles.unreadIndicatorText}>
{props.translate('common.new')}
Expand Down
Loading