From e9ad92f737e8661e8bef69b84a4d94672b6d0f8b Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Thu, 4 Aug 2022 18:41:45 +0530 Subject: [PATCH 01/59] Add new API method SetPasswordForNewUserAndSignin --- src/libs/actions/Session/index.js | 62 +++++++++++++++++++++++++++++++ src/pages/SetPasswordPage.js | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 5059a434ad43..43f172289fa4 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -441,6 +441,67 @@ function changePasswordAndSignIn(authToken, password) { }); } +/** + * Validates new user login, sets a new password and authenticates them + * @param {String} authToken + * @param {String} password + */ +function setPasswordForNewUserAndSignin(authToken, password) { + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + validateCodeExpired: false, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: '', + }, + }, + ]; + + const successData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: '', + }, + }, + ]; + + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + error: 'Unable to set Password', + }, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + error: 'setPasswordPage.passwordNotSet', + }, + }, + ]; + API.write('SetPasswordForNewAccountAndSignin', {authToken, password}, {optimisticData, successData, failureData}); +} + /** * @param {Number} accountID * @param {String} validateCode @@ -539,6 +600,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) { export { fetchAccountDetails, setPassword, + setPasswordForNewUserAndSignin, signIn, signInWithShortLivedToken, signOut, diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index ec5c3fb3599d..c33293dc1333 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -105,7 +105,7 @@ class SetPasswordPage extends Component { const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); if (this.props.userSignUp.authToken) { - Session.changePasswordAndSignIn(this.props.userSignUp.authToken, this.state.password); + Session.setPasswordForNewUserAndSignin(this.props.userSignUp.authToken, this.state.password); } else { Session.setPassword(this.state.password, validateCode, accountID); } From eb766699512a2c1325e38d0bfa768741b5aefbf5 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 15:45:14 +0100 Subject: [PATCH 02/59] add the method --- src/libs/actions/Wallet.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index eb75d3969119..0f4154ca5360 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -351,6 +351,10 @@ function updateCurrentStep(currentStep) { Onyx.merge(ONYXKEYS.USER_WALLET, {currentStep}); } +function answerQuestionsForWallet() { + +} + export { fetchOnfidoToken, activateWallet, From 46567a465a18ee4aa88f71cb9cdf0f9f5c8a6948 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:08:37 +0100 Subject: [PATCH 03/59] adding the OnyxData --- src/libs/actions/Wallet.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 0f4154ca5360..a5a2d302082b 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -7,6 +7,7 @@ import * as DeprecatedAPI from '../deprecatedAPI'; import CONST from '../../CONST'; import * as PaymentMethods from './PaymentMethods'; import * as Localize from '../Localize'; +import * as API from "../API"; /** * Fetch and save locally the Onfido SDK token and applicantID @@ -351,8 +352,37 @@ function updateCurrentStep(currentStep) { Onyx.merge(ONYXKEYS.USER_WALLET, {currentStep}); } -function answerQuestionsForWallet() { - +/** + * @param {String} idologyAnswers + */ +function answerQuestionsForWallet(idologyAnswers) { + API.write('AnswerQuestionsForWallet', + { + idologyAnswers, + }, + { + optimisticData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: true, + }, + }], + successData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: false, + }, + }], + failureData: [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + value: { + isLoading: false, + }, + }], + }); } export { From 77f30ded65b930a2bb1a9905decccf4b7a4dc34e Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:15:44 +0100 Subject: [PATCH 04/59] calling api from the right js file --- src/libs/actions/BankAccounts.js | 1 + src/libs/actions/Wallet.js | 1 + src/pages/EnablePayments/IdologyQuestions.js | 11 +++++------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 8fb86b3c25dd..2b7be34287f4 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -34,6 +34,7 @@ export { fetchOnfidoToken, activateWallet, fetchUserWallet, + answerQuestionsForWallet, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index a5a2d302082b..0749055a2d87 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -395,4 +395,5 @@ export { setAdditionalDetailsQuestions, buildIdologyError, updateCurrentStep, + answerQuestionsForWallet, }; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index cd8ff54411bd..3d711d4236a6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -103,12 +103,11 @@ class IdologyQuestions extends React.Component { } } - BankAccounts.activateWallet(CONST.WALLET.STEP.ADDITIONAL_DETAILS, { - idologyAnswers: { - answers, - idNumber: this.props.idNumber, - }, - }); + let idologyAnswers = { + answers, + idNumber: this.props.idNumber, + }; + BankAccounts.answerQuestionsForWallet(idologyAnswers); return {answers, isLoading: true}; } From 29ae567a3eb5977c1b37f5e6eaa3e987d82922ae Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:21:08 +0100 Subject: [PATCH 05/59] adding onyxKey --- src/pages/EnablePayments/IdologyQuestions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 3d711d4236a6..5b45963198a0 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -13,6 +13,9 @@ import Text from '../../components/Text'; import TextLink from '../../components/TextLink'; import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; +import compose from "../../libs/compose"; +import {withOnyx} from "react-native-onyx"; +import ONYXKEYS from "../../ONYXKEYS"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -108,7 +111,7 @@ class IdologyQuestions extends React.Component { idNumber: this.props.idNumber, }; BankAccounts.answerQuestionsForWallet(idologyAnswers); - return {answers, isLoading: true}; + return {answers}; } // Else, show next question @@ -171,4 +174,8 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; -export default withLocalize(IdologyQuestions); +export default compose(withLocalize(IdologyQuestions), withOnyx({ + additionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, +}))(IdologyQuestions); From 30ad2f148fab6b5645ff3ca1581b0a6b094f744d Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 9 Aug 2022 16:25:20 +0100 Subject: [PATCH 06/59] using the isLoading --- src/pages/EnablePayments/IdologyQuestions.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 5b45963198a0..2583300f6afd 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -33,11 +33,18 @@ const propTypes = { /** ID from Idology, referencing those questions */ idNumber: PropTypes.string, + + additionalDetails: PropTypes.shape({ + isLoading: PropTypes.bool, + }), }; const defaultProps = { questions: [], idNumber: '', + additionalDetails: { + isLoading: false, + }, }; class IdologyQuestions extends React.Component { @@ -57,9 +64,6 @@ class IdologyQuestions extends React.Component { /** Any error message */ errorMessage: '', - - /** Did the user just submitted all his answers? */ - isLoading: false, }; } @@ -163,7 +167,7 @@ class IdologyQuestions extends React.Component { this.form.scrollTo({y: 0, animated: true}); }} message={this.state.errorMessage} - isLoading={this.state.isLoading} + isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From 86fe203e690f98c4ef1e568d954a2e17f0121305 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 15:14:57 +0100 Subject: [PATCH 07/59] linter requested changes --- src/libs/actions/Wallet.js | 2 +- src/pages/EnablePayments/IdologyQuestions.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 0749055a2d87..b2edf801e033 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -7,7 +7,7 @@ import * as DeprecatedAPI from '../deprecatedAPI'; import CONST from '../../CONST'; import * as PaymentMethods from './PaymentMethods'; import * as Localize from '../Localize'; -import * as API from "../API"; +import * as API from '../API'; /** * Fetch and save locally the Onfido SDK token and applicantID diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 2583300f6afd..92d1f47144d8 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -4,18 +4,17 @@ import PropTypes from 'prop-types'; import { View, } from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; import * as BankAccounts from '../../libs/actions/BankAccounts'; -import CONST from '../../CONST'; import Text from '../../components/Text'; import TextLink from '../../components/TextLink'; import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; -import compose from "../../libs/compose"; -import {withOnyx} from "react-native-onyx"; -import ONYXKEYS from "../../ONYXKEYS"; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -110,7 +109,7 @@ class IdologyQuestions extends React.Component { } } - let idologyAnswers = { + const idologyAnswers = { answers, idNumber: this.props.idNumber, }; From c31819fcfbf4d090eb121d576342213a1d9533aa Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 15:34:06 +0100 Subject: [PATCH 08/59] using the errors and error code --- src/pages/EnablePayments/IdologyQuestions.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 92d1f47144d8..9a0eebdc89fd 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -35,6 +35,8 @@ const propTypes = { additionalDetails: PropTypes.shape({ isLoading: PropTypes.bool, + errors: PropTypes.arrayOf(PropTypes.string), + errorCode: PropTypes.string, }), }; @@ -43,6 +45,8 @@ const defaultProps = { idNumber: '', additionalDetails: { isLoading: false, + errors: [], + errorCode: '', }, }; @@ -160,12 +164,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={this.state.errorMessage} + message={_.isEmpty(this.props.additionalDetails.errors) ? this.props.additionalDetails.errors.find(x=>x!==undefined) : this.state.errorMessage : } isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From ed809baab39a77e1a5e2844cb399f4c10651de71 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 16:53:29 +0100 Subject: [PATCH 09/59] need to json stringify first? --- src/libs/actions/Wallet.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index b2edf801e033..7d59733da7bc 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -356,9 +356,10 @@ function updateCurrentStep(currentStep) { * @param {String} idologyAnswers */ function answerQuestionsForWallet(idologyAnswers) { + const answers = JSON.stringify(idologyAnswers); API.write('AnswerQuestionsForWallet', { - idologyAnswers, + answers, }, { optimisticData: [{ From 7477c23b3ade0a9dce1ac35486f602609bca7b94 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Wed, 10 Aug 2022 16:53:48 +0100 Subject: [PATCH 10/59] I think this is where the error code will be used --- src/pages/EnablePayments/EnablePaymentsPage.js | 15 ++++++++++++++- src/pages/EnablePayments/IdologyQuestions.js | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 53f25dfeffd7..c8d481649e7e 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -21,17 +21,27 @@ import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; import compose from '../../libs/compose'; import withLocalize from '../../components/withLocalize'; +import PropTypes from "prop-types"; const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, + + additionalDetails: PropTypes.shape({ + errorCode: PropTypes.string, + }), + ...userWalletPropTypes, }; const defaultProps = { // eslint-disable-next-line react/default-props-match-prop-types userWallet: {}, + + additionalDetails: { + errorCode: '', + }, }; class EnablePaymentsPage extends React.Component { @@ -56,7 +66,7 @@ class EnablePaymentsPage extends React.Component { return ; } - if (this.props.userWallet.shouldShowFailedKYC) { + if (this.props.additionalDetails.errorCode === 'kycFailed') { return ( { this.form.scrollTo({y: 0, animated: true}); From eb673763eba4accfca1107374ea882100fce527a Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:25:06 +0100 Subject: [PATCH 11/59] remove import duplicated --- src/pages/EnablePayments/EnablePaymentsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index feda0930409a..836c3103b7ba 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -22,7 +22,6 @@ import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; import compose from '../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import PropTypes from "prop-types"; const propTypes = { /** Information about the network from Onyx */ From c67d1844abe0c34b50c8b0702d9e09da56a67038 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:34:08 +0100 Subject: [PATCH 12/59] linter removing extra line (bad merge?) --- src/pages/EnablePayments/EnablePaymentsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 836c3103b7ba..a9b5247f2e0f 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -27,7 +27,6 @@ const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, - additionalDetails: PropTypes.shape({ errorCode: PropTypes.string, }), From ed6a04a2cecd11352e8431d887110ab23bdd161f Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Fri, 19 Aug 2022 16:58:20 +0100 Subject: [PATCH 13/59] fixing linter, I think it's good this time --- src/pages/EnablePayments/IdologyQuestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 3344be78ef83..a92a47f450c4 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { View, } from 'react-native'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -169,7 +169,7 @@ class IdologyQuestions extends React.Component { onFixTheErrorsLinkPressed={() => { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.additionalDetails.errors) ? this.props.additionalDetails.errors.find(x=>x!==undefined) : this.state.errorMessage : } + message={_.isEmpty(this.props.additionalDetails.errors) ? _.find(this.props.additionalDetails.errors, error => error !== undefined) : this.state.errorMessage} isLoading={this.props.additionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From ccf63b3452783ad7bb936e6313286be8c37afc2e Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 12:52:02 +0200 Subject: [PATCH 14/59] correcting the name to walletAdditionalDetails --- src/pages/EnablePayments/IdologyQuestions.js | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index a92a47f450c4..78ed9ac162a6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -33,9 +33,14 @@ const propTypes = { /** ID from Idology, referencing those questions */ idNumber: PropTypes.string, - additionalDetails: PropTypes.shape({ + walletAdditionalDetails: PropTypes.shape({ + /** Are we waiting for a response? */ isLoading: PropTypes.bool, - errors: PropTypes.arrayOf(PropTypes.string), + + /** Any additional error message to show */ + errors: PropTypes.objectOf(PropTypes.string), + + /** What error do we need to handle */ errorCode: PropTypes.string, }), }; @@ -43,9 +48,9 @@ const propTypes = { const defaultProps = { questions: [], idNumber: '', - additionalDetails: { + walletAdditionalDetails: { isLoading: false, - errors: [], + errors: {}, errorCode: '', }, }; @@ -164,13 +169,13 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.additionalDetails.errors) ? _.find(this.props.additionalDetails.errors, error => error !== undefined) : this.state.errorMessage} - isLoading={this.props.additionalDetails.isLoading} + message={_.isEmpty(this.props.walletAdditionalDetails.errors) ? _.find(this.props.walletAdditionalDetails.errors, error => error !== undefined) : this.state.errorMessage} + isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> @@ -182,7 +187,7 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; export default compose(withLocalize(IdologyQuestions), withOnyx({ - additionalDetails: { + walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, }))(IdologyQuestions); From 66fc73481047948d9d4c9a37cf62e9a8c87df295 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 16:24:30 +0200 Subject: [PATCH 15/59] adding some TODO so when I'm done. I can remove this mehtod --- src/libs/actions/Wallet.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index ee66288ff4e3..42c21a4f86db 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -247,6 +247,7 @@ function updatePersonalDetails(personalDetails) { * @param {Object} [parameters.idologyAnswers] * @param {Boolean} [parameters.hasAcceptedTerms] */ +// TODO: this is no longer used, so I need to remove it, for now I need to understand how it works function activateWallet(currentStep, parameters) { let personalDetails; let idologyAnswers; @@ -257,6 +258,7 @@ function activateWallet(currentStep, parameters) { throw new Error('Invalid currentStep passed to activateWallet()'); } + // TODO: need to check what this does setWalletShouldShowFailedKYC(false); if (currentStep === CONST.WALLET.STEP.ONFIDO) { onfidoData = parameters.onfidoData; @@ -303,6 +305,7 @@ function activateWallet(currentStep, parameters) { return; } + // TODO: not sure if we need to do this, and if we do, is it this GH? if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) { if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) { setAdditionalDetailsQuestions(response.data.questions, response.data.idNumber); From b9c7d676997e4dc206bc3d47657f03eef3c594b7 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 16:25:05 +0200 Subject: [PATCH 16/59] adding some handling for what happens when the answers were correct. --- src/libs/actions/BankAccounts.js | 1 + src/libs/actions/Wallet.js | 1 + src/pages/EnablePayments/IdologyQuestions.js | 30 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 4d3b0b71b36c..3c7fc568ff72 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -31,6 +31,7 @@ export { answerQuestionsForWallet, verifyIdentity, acceptWalletTerms, + setAdditionalDetailsLoading, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 42c21a4f86db..223637d770ff 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -580,4 +580,5 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, + setAdditionalDetailsLoading, }; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 78ed9ac162a6..90ff21191a67 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -15,6 +15,8 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; +import CONST from "../../CONST"; +import * as PaymentMethods from "../../libs/actions/PaymentMethods"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -43,6 +45,16 @@ const propTypes = { /** What error do we need to handle */ errorCode: PropTypes.string, }), + + /** User wallet props */ + userWallet: PropTypes.shape({ + + /** The step in the wallet configuration we are in. */ + currentStep: PropTypes.string, + + /** What tier does the user has for their wallet */ + tierName: PropTypes.string, + }), }; const defaultProps = { @@ -53,6 +65,10 @@ const defaultProps = { errors: {}, errorCode: '', }, + userWallet: { + currentStep: '', + tierName: '', + }, }; class IdologyQuestions extends React.Component { @@ -75,6 +91,17 @@ class IdologyQuestions extends React.Component { }; } + componentDidUpdate(prevProps) { + if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { + return; + } + + if (this.props.walletAdditionalDetails.isLoading) { + PaymentMethods.continueSetup(); + BankAccounts.setAdditionalDetailsLoading(false); + } + } + /** * Put question answer in the state. * @param {Number} questionIndex @@ -190,4 +217,7 @@ export default compose(withLocalize(IdologyQuestions), withOnyx({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + } }))(IdologyQuestions); From d9153aa550562b435d2bb47bba83d5397d0fcbc6 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:01:50 +0200 Subject: [PATCH 17/59] fix jest hopefully --- src/pages/EnablePayments/IdologyQuestions.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 90ff21191a67..ae5bdc65240e 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -213,11 +213,14 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; -export default compose(withLocalize(IdologyQuestions), withOnyx({ - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - } -}))(IdologyQuestions); +export default compose( + withLocalize(IdologyQuestions), + withOnyx({ + walletAdditionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + }), +)(IdologyQuestions); From f0d04dbe88d275e74bb1959334bfbd4054620a71 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:21:02 +0200 Subject: [PATCH 18/59] adding some more logic --- src/pages/EnablePayments/IdologyQuestions.js | 31 ++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index ae5bdc65240e..1e24dc1c1c14 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -17,6 +17,12 @@ import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import CONST from "../../CONST"; import * as PaymentMethods from "../../libs/actions/PaymentMethods"; +import lodashGet from "lodash/get"; +import * as PersonalDetails from "../../libs/actions/PersonalDetails"; +import ScreenWrapper from "../../components/ScreenWrapper"; +import HeaderWithCloseButton from "../../components/HeaderWithCloseButton"; +import Navigation from "../../libs/Navigation/Navigation"; +import FailedKYC from "./FailedKYC"; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -85,9 +91,6 @@ class IdologyQuestions extends React.Component { /** Answers from the user */ answers: [], - - /** Any error message */ - errorMessage: '', }; } @@ -114,7 +117,6 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, - errorMessage: '', }; }); } @@ -127,6 +129,7 @@ class IdologyQuestions extends React.Component { // User must pick an answer if (!prevState.answers[prevState.questionNumber]) { return { + // TODO: I think this should be a merge of the Aditional Details, as an error, that would update everything errorMessage: this.props.translate('additionalDetailsStep.selectAnswer'), }; } @@ -174,6 +177,22 @@ class IdologyQuestions extends React.Component { value: answer, }; })); + const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); + const isErrorVisible = _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? '' : _.last(_.values(errors)); + + if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { + return ( + // TODO: not sure if this is the correct design + + Navigation.dismissModal()} + /> + + + ); + } return ( @@ -196,12 +215,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={_.isEmpty(this.props.walletAdditionalDetails.errors) ? _.find(this.props.walletAdditionalDetails.errors, error => error !== undefined) : this.state.errorMessage} + message={errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From de24b0031f2c5912c97bf250b8967f8b7126dc89 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:25:42 +0200 Subject: [PATCH 19/59] localize is not a mehtod --- src/pages/EnablePayments/IdologyQuestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 1e24dc1c1c14..161843345235 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -233,7 +233,7 @@ class IdologyQuestions extends React.Component { IdologyQuestions.propTypes = propTypes; IdologyQuestions.defaultProps = defaultProps; export default compose( - withLocalize(IdologyQuestions), + withLocalize, withOnyx({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, From 31b42d936e59f63925678a704c1c749334ed0238 Mon Sep 17 00:00:00 2001 From: Andre Fonseca Date: Tue, 30 Aug 2022 17:37:13 +0200 Subject: [PATCH 20/59] fixing linter --- src/pages/EnablePayments/IdologyQuestions.js | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 161843345235..13de3aac942c 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,6 +5,7 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -15,14 +16,11 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import CONST from "../../CONST"; -import * as PaymentMethods from "../../libs/actions/PaymentMethods"; -import lodashGet from "lodash/get"; -import * as PersonalDetails from "../../libs/actions/PersonalDetails"; -import ScreenWrapper from "../../components/ScreenWrapper"; -import HeaderWithCloseButton from "../../components/HeaderWithCloseButton"; -import Navigation from "../../libs/Navigation/Navigation"; -import FailedKYC from "./FailedKYC"; +import CONST from '../../CONST'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import Navigation from '../../libs/Navigation/Navigation'; +import FailedKYC from './FailedKYC'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -94,7 +92,7 @@ class IdologyQuestions extends React.Component { }; } - componentDidUpdate(prevProps) { + componentDidUpdate() { if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { return; } @@ -183,6 +181,7 @@ class IdologyQuestions extends React.Component { if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { return ( + // TODO: not sure if this is the correct design Date: Wed, 31 Aug 2022 18:14:53 +0400 Subject: [PATCH 21/59] Remove TODOs and cleanup --- src/libs/actions/BankAccounts.js | 1 - src/libs/actions/Wallet.js | 6 +----- src/pages/EnablePayments/EnablePaymentsPage.js | 10 +--------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 3c7fc568ff72..4d3b0b71b36c 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -31,7 +31,6 @@ export { answerQuestionsForWallet, verifyIdentity, acceptWalletTerms, - setAdditionalDetailsLoading, } from './Wallet'; function clearPersonalBankAccount() { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 223637d770ff..e8a838fc79c4 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -247,7 +247,6 @@ function updatePersonalDetails(personalDetails) { * @param {Object} [parameters.idologyAnswers] * @param {Boolean} [parameters.hasAcceptedTerms] */ -// TODO: this is no longer used, so I need to remove it, for now I need to understand how it works function activateWallet(currentStep, parameters) { let personalDetails; let idologyAnswers; @@ -258,7 +257,6 @@ function activateWallet(currentStep, parameters) { throw new Error('Invalid currentStep passed to activateWallet()'); } - // TODO: need to check what this does setWalletShouldShowFailedKYC(false); if (currentStep === CONST.WALLET.STEP.ONFIDO) { onfidoData = parameters.onfidoData; @@ -305,7 +303,6 @@ function activateWallet(currentStep, parameters) { return; } - // TODO: not sure if we need to do this, and if we do, is it this GH? if (currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS) { if (response.title === CONST.WALLET.ERROR.KBA_NEEDED) { setAdditionalDetailsQuestions(response.data.questions, response.data.idNumber); @@ -532,7 +529,7 @@ function updateCurrentStep(currentStep) { } /** - * @param {String} idologyAnswers + * @param {Object} idologyAnswers */ function answerQuestionsForWallet(idologyAnswers) { const answers = JSON.stringify(idologyAnswers); @@ -580,5 +577,4 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, - setAdditionalDetailsLoading, }; diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 0e098fa5a820..3647343f9e89 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -27,10 +27,6 @@ const propTypes = { /** Information about the network from Onyx */ network: networkPropTypes.isRequired, - additionalDetails: PropTypes.shape({ - errorCode: PropTypes.string, - }), - /** The user's wallet */ userWallet: PropTypes.objectOf(userWalletPropTypes), @@ -39,10 +35,6 @@ const propTypes = { const defaultProps = { userWallet: {}, - - additionalDetails: { - errorCode: '', - }, }; class EnablePaymentsPage extends React.Component { @@ -63,7 +55,7 @@ class EnablePaymentsPage extends React.Component { return ; } - if (this.props.additionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { + if (this.props.userWallet.errorCode === CONST.WALLET.ERROR.KYC) { return ( Date: Wed, 31 Aug 2022 18:36:05 +0400 Subject: [PATCH 22/59] Clean up props and remove un-needed logic --- .../EnablePayments/EnablePaymentsPage.js | 3 -- src/pages/EnablePayments/IdologyQuestions.js | 54 +++++-------------- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js index 3647343f9e89..314419df9a56 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.js @@ -101,9 +101,6 @@ export default compose( walletAdditionalDetailsDraft: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, - additionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, }), withNetwork(), )(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 13de3aac942c..af2dcf3853eb 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -21,6 +21,7 @@ import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; +import userWalletPropTypes from './userWalletPropTypes'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -51,28 +52,14 @@ const propTypes = { }), /** User wallet props */ - userWallet: PropTypes.shape({ - - /** The step in the wallet configuration we are in. */ - currentStep: PropTypes.string, - - /** What tier does the user has for their wallet */ - tierName: PropTypes.string, - }), + userWallet: PropTypes.shape(userWalletPropTypes), }; const defaultProps = { questions: [], idNumber: '', - walletAdditionalDetails: { - isLoading: false, - errors: {}, - errorCode: '', - }, - userWallet: { - currentStep: '', - tierName: '', - }, + walletAdditionalDetails: {}, + userWallet: {}, }; class IdologyQuestions extends React.Component { @@ -89,6 +76,9 @@ class IdologyQuestions extends React.Component { /** Answers from the user */ answers: [], + + /** Any error message */ + errorMessage: '', }; } @@ -96,11 +86,6 @@ class IdologyQuestions extends React.Component { if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { return; } - - if (this.props.walletAdditionalDetails.isLoading) { - PaymentMethods.continueSetup(); - BankAccounts.setAdditionalDetailsLoading(false); - } } /** @@ -115,6 +100,7 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, + errorMessage, }; }); } @@ -127,7 +113,6 @@ class IdologyQuestions extends React.Component { // User must pick an answer if (!prevState.answers[prevState.questionNumber]) { return { - // TODO: I think this should be a merge of the Aditional Details, as an error, that would update everything errorMessage: this.props.translate('additionalDetailsStep.selectAnswer'), }; } @@ -146,11 +131,10 @@ class IdologyQuestions extends React.Component { } } - const idologyAnswers = { + BankAccounts.answerQuestionsForWallet({ answers, idNumber: this.props.idNumber, - }; - BankAccounts.answerQuestionsForWallet(idologyAnswers); + }); return {answers}; } @@ -176,22 +160,8 @@ class IdologyQuestions extends React.Component { }; })); const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = _.size(this.getErrors()) > 0 || !_.isEmpty(errors); - const errorMessage = _.isEmpty(errors) ? '' : _.last(_.values(errors)); - - if (this.props.walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.KYC) { - return ( - - // TODO: not sure if this is the correct design - - Navigation.dismissModal()} - /> - - - ); - } + const isErrorVisible = this.state.errorMessage || _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( From f42ed2a20b95a4ebb24d720956232bcc4f0d6b7e Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 19:27:49 +0400 Subject: [PATCH 23/59] Fix command params for answerQuestionsForWallet --- src/libs/actions/Wallet.js | 7 +++--- src/pages/EnablePayments/IdologyQuestions.js | 24 ++++---------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index e8a838fc79c4..b8b883a67e28 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -531,11 +531,12 @@ function updateCurrentStep(currentStep) { /** * @param {Object} idologyAnswers */ -function answerQuestionsForWallet(idologyAnswers) { - const answers = JSON.stringify(idologyAnswers); +function answerQuestionsForWallet(answers, idNumber) { + const idologyAnswers = JSON.stringify(answers); API.write('AnswerQuestionsForWallet', { - answers, + idologyAnswers, + idNumber }, { optimisticData: [{ diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index af2dcf3853eb..5dc3beb5d697 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -21,7 +21,6 @@ import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; import FailedKYC from './FailedKYC'; -import userWalletPropTypes from './userWalletPropTypes'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -50,16 +49,12 @@ const propTypes = { /** What error do we need to handle */ errorCode: PropTypes.string, }), - - /** User wallet props */ - userWallet: PropTypes.shape(userWalletPropTypes), }; const defaultProps = { questions: [], idNumber: '', walletAdditionalDetails: {}, - userWallet: {}, }; class IdologyQuestions extends React.Component { @@ -82,12 +77,6 @@ class IdologyQuestions extends React.Component { }; } - componentDidUpdate() { - if (this.props.userWallet.tierName !== CONST.WALLET.TIER_NAME.GOLD) { - return; - } - } - /** * Put question answer in the state. * @param {Number} questionIndex @@ -100,7 +89,7 @@ class IdologyQuestions extends React.Component { answers[questionIndex] = {question: question.type, answer}; return { answers, - errorMessage, + errorMessage: '', }; }); } @@ -131,10 +120,7 @@ class IdologyQuestions extends React.Component { } } - BankAccounts.answerQuestionsForWallet({ - answers, - idNumber: this.props.idNumber, - }); + BankAccounts.answerQuestionsForWallet(answers, this.props.idNumber); return {answers}; } @@ -160,7 +146,7 @@ class IdologyQuestions extends React.Component { }; })); const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = this.state.errorMessage || _.size(this.getErrors()) > 0 || !_.isEmpty(errors); + const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( @@ -179,6 +165,7 @@ class IdologyQuestions extends React.Component { {question.prompt} this.chooseAnswer(questionIndex, answer)} /> @@ -207,8 +194,5 @@ export default compose( walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, }), )(IdologyQuestions); From 3595d65aedc9f4ff2a4531695389e078ed181b40 Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 21:39:42 +0400 Subject: [PATCH 24/59] Don't show redundant error --- src/pages/EnablePayments/IdologyQuestions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 5dc3beb5d697..b726b417cd06 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -145,9 +145,6 @@ class IdologyQuestions extends React.Component { value: answer, }; })); - const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); - const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); - const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); return ( @@ -171,12 +168,12 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={errorMessage} + message={this.state.errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> From f1f621ee99fafe8ff50a0175954da1cad780707e Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Wed, 31 Aug 2022 21:42:07 +0400 Subject: [PATCH 25/59] Fix lint errors --- src/libs/actions/Wallet.js | 5 +++-- src/pages/EnablePayments/IdologyQuestions.js | 6 ------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index b8b883a67e28..a4e75f49d010 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -529,14 +529,15 @@ function updateCurrentStep(currentStep) { } /** - * @param {Object} idologyAnswers + * @param {Object} answers + * @param {String} idNumber */ function answerQuestionsForWallet(answers, idNumber) { const idologyAnswers = JSON.stringify(answers); API.write('AnswerQuestionsForWallet', { idologyAnswers, - idNumber + idNumber, }, { optimisticData: [{ diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index b726b417cd06..6d3cf2677339 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,7 +5,6 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -16,11 +15,6 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import CONST from '../../CONST'; -import * as PaymentMethods from '../../libs/actions/PaymentMethods'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import Navigation from '../../libs/Navigation/Navigation'; -import FailedKYC from './FailedKYC'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; From bc6be2ddedfa075a8a816a1079fde5dc06ba16ce Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Thu, 1 Sep 2022 09:44:04 +0400 Subject: [PATCH 26/59] Add offline indicator and display error messages --- src/pages/EnablePayments/IdologyQuestions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.js index 6d3cf2677339..c4368c8cb9c6 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.js @@ -5,6 +5,7 @@ import { View, } from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import RadioButtons from '../../components/RadioButtons'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import styles from '../../styles/styles'; @@ -15,6 +16,7 @@ import FormScrollView from '../../components/FormScrollView'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; +import OfflineIndicator from '../../components/OfflineIndicator'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -140,6 +142,10 @@ class IdologyQuestions extends React.Component { }; })); + const errors = lodashGet(this.props, 'walletAdditionalDetails.errors', {}); + const isErrorVisible = this.state.errorMessage || !_.isEmpty(errors); + const errorMessage = _.isEmpty(errors) ? this.state.errorMessage : _.last(_.values(errors)); + return ( @@ -162,15 +168,16 @@ class IdologyQuestions extends React.Component { { this.form.scrollTo({y: 0, animated: true}); }} - message={this.state.errorMessage} + message={errorMessage} isLoading={this.props.walletAdditionalDetails.isLoading} buttonText={this.props.translate('common.saveAndContinue')} /> + ); From 41671c724443e00ec4b8e1de5be307525262ce7c Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 2 Sep 2022 10:52:37 +0200 Subject: [PATCH 27/59] Handle API call and change button text --- src/pages/SetPasswordPage.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index ae46a1c53c76..b7d15d9fd610 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -30,6 +30,9 @@ const propTypes = { /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, + + /** If account is validated or not */ + validated: PropTypes.bool, }), /** The credentials of the logged in person */ @@ -75,15 +78,6 @@ class SetPasswordPage extends Component { }; } - componentDidMount() { - const accountID = lodashGet(this.props.route.params, 'accountID', ''); - const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { - return; - } - Session.validateEmail(accountID, validateCode); - } - validateAndSubmitForm() { if (!this.state.isFormValid) { return; @@ -91,15 +85,16 @@ class SetPasswordPage extends Component { const accountID = lodashGet(this.props.route.params, 'accountID', ''); const validateCode = lodashGet(this.props.route.params, 'validateCode', ''); - if (this.props.credentials.authToken) { - Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); - } else { + if (this.props.account.validated) { Session.setPassword(this.state.password, validateCode, accountID); + } else { + Session.setPasswordForNewAccountAndSignin(accountID, validateCode, this.state.password); + // Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); } } render() { - const buttonText = !this.props.account.validated ? this.props.translate('setPasswordPage.validateAccount') : this.props.translate('setPasswordPage.setPassword'); + const buttonText = this.props.translate('setPasswordPage.setPassword'); const sessionError = this.props.session.error && this.props.translate(this.props.session.error); const error = sessionError || this.props.account.error; return ( From f4e191d0c84d1d6a81ec5770ef7190cea26cfd6a Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Fri, 2 Sep 2022 17:55:38 +0200 Subject: [PATCH 28/59] Update errors to error --- src/CONST.js | 2 +- src/libs/actions/Session/index.js | 39 ++++++++++++++++-------------- src/libs/actions/SignInRedirect.js | 3 ++- src/libs/actions/User.js | 3 ++- src/pages/SetPasswordPage.js | 12 ++++----- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 4b0bd6f4a502..59a80df5c244 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -408,7 +408,7 @@ const CONST = { FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {error: '', success: '', loading: false}, + DEFAULT_ACCOUNT_DATA: {errors: '', success: '', loading: false}, APP_STATE: { ACTIVE: 'active', BACKGROUND: 'background', diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index dd6d247e7b5f..8ec90f4a89b2 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -43,7 +43,7 @@ function setSuccessfulSignInData(data) { PushNotification.register(data.accountID); Onyx.merge(ONYXKEYS.SESSION, { shouldShowComposeInput: true, - error: null, + errors: null, ..._.pick(data, 'authToken', 'accountID', 'email', 'encryptedAuthToken'), }); } @@ -188,7 +188,7 @@ function createTemporaryLogin(authToken, email) { }) .then((createLoginResponse) => { if (createLoginResponse.jsonCode !== 200) { - Onyx.merge(ONYXKEYS.ACCOUNT, {error: createLoginResponse.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: createLoginResponse.message}); return createLoginResponse; } @@ -250,7 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(errorMessage), isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: Localize.translateLocal(errorMessage), isLoading: false}); return; } @@ -344,7 +344,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: response.message}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); @@ -386,7 +386,6 @@ function cleanupSession() { function clearAccountMessages() { Onyx.merge(ONYXKEYS.ACCOUNT, { - error: '', success: '', errors: [], isLoading: false, @@ -418,35 +417,35 @@ function changePasswordAndSignIn(authToken, password) { } if (responsePassword.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { // authToken has expired, and we have the account email, so we request a new magic link. - Onyx.merge(ONYXKEYS.ACCOUNT, {error: null}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: null}); resetPassword(); Navigation.navigate(ROUTES.HOME); return; } - Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.passwordNotSet'}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('setPasswordPage.passwordNotSet')}}); }); } /** * Validates new user login, sets a new password and authenticates them - * @param {String} authToken + * @param {Number} accountID + * @param {String} validateCode * @param {String} password */ -function setPasswordForNewUserAndSignin(authToken, password) { +function setPasswordForNewAccountAndSignin(accountID, validateCode, password) { const optimisticData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, value: { isLoading: true, - validateCodeExpired: false, }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: '', + errors: null, }, }, ]; @@ -457,13 +456,14 @@ function setPasswordForNewUserAndSignin(authToken, password) { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, + errors: null, }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: '', + errors: null, }, }, ]; @@ -474,18 +474,21 @@ function setPasswordForNewUserAndSignin(authToken, password) { key: ONYXKEYS.ACCOUNT, value: { isLoading: false, - error: 'Unable to set Password', }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.SESSION, value: { - error: 'setPasswordPage.passwordNotSet', + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('setPasswordPage.passwordNotSet'), + }, }, }, ]; - API.write('SetPasswordForNewAccountAndSignin', {authToken, password}, {optimisticData, successData, failureData}); + API.write('SetPasswordForNewAccountAndSignin', { + accountID, validateCode, password, + }, {optimisticData, successData, failureData}); } /** @@ -495,7 +498,7 @@ function setPasswordForNewUserAndSignin(authToken, password) { * @param {String} authToken */ function validateEmail(accountID, validateCode) { - Onyx.merge(ONYXKEYS.SESSION, {error: ''}); + Onyx.merge(ONYXKEYS.SESSION, {errors: null}); DeprecatedAPI.ValidateEmail({ accountID, validateCode, @@ -509,7 +512,7 @@ function validateEmail(accountID, validateCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true}); } if (responseValidate.jsonCode === 401) { - Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.setPasswordLinkInvalid'}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translate('setPasswordPage.setPasswordLinkInvalid')}}); } }); } @@ -582,7 +585,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) { export { beginSignIn, setPassword, - setPasswordForNewUserAndSignin, + setPasswordForNewAccountAndSignin, signIn, signInWithShortLivedToken, signOut, diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 597de6d2ecd8..4542d943c7d7 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; +import DateUtils from '../DateUtils'; let currentActiveClients; Onyx.connect({ @@ -44,7 +45,7 @@ function clearStorageAndRedirect(errorMessage) { } // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set`. - Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); }); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index a7b71d9fd3b5..bf67d7d42d29 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -18,6 +18,7 @@ import * as Link from './Link'; import getSkinToneEmojiFromIndex from '../../components/EmojiPicker/getSkinToneEmojiFromIndex'; import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; +import DateUtils from '../DateUtils'; let sessionAuthToken = ''; let currentUserAccountID = ''; @@ -233,7 +234,7 @@ function validateLogin(accountID, validateCode) { } } else { const error = lodashGet(response, 'message', 'Unable to validate login.'); - Onyx.merge(ONYXKEYS.ACCOUNT, {error}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: error}}); } }).finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index b7d15d9fd610..0f35f098e54e 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -19,6 +19,7 @@ import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import compose from '../libs/compose'; import NewPasswordForm from './settings/NewPasswordForm'; import FormAlertWithSubmitButton from '../components/FormAlertWithSubmitButton'; +import * as ErrorUtils from '../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -26,7 +27,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.string, /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, @@ -47,7 +48,7 @@ const propTypes = { /** Session object */ session: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.string, }), /** The accountID and validateCode are passed via the URL */ @@ -61,7 +62,7 @@ const defaultProps = { credentials: {}, route: validateLinkDefaultProps, session: { - error: '', + errors: '', authToken: '', }, }; @@ -89,14 +90,13 @@ class SetPasswordPage extends Component { Session.setPassword(this.state.password, validateCode, accountID); } else { Session.setPasswordForNewAccountAndSignin(accountID, validateCode, this.state.password); - // Session.changePasswordAndSignIn(this.props.credentials.authToken, this.state.password); } } render() { const buttonText = this.props.translate('setPasswordPage.setPassword'); - const sessionError = this.props.session.error && this.props.translate(this.props.session.error); - const error = sessionError || this.props.account.error; + const sessionError = this.props.session.errors && ErrorUtils.getLatestErrorMessage(this.props.session); + const error = sessionError || ErrorUtils.getLatestErrorMessage(this.props.account); return ( Date: Mon, 5 Sep 2022 12:27:06 +0100 Subject: [PATCH 29/59] Prevent setting empty error --- src/libs/actions/SignInRedirect.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 4542d943c7d7..4806242051dd 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -44,8 +44,10 @@ function clearStorageAndRedirect(errorMessage) { Onyx.set(ONYXKEYS.NETWORK, {isOffline}); } - // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set`. - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` + if (errorMessage) { + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + } }); } From 233aa13cc1d13aa8f23f41fd4c4a1513c840999d Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Tue, 6 Sep 2022 10:16:08 +0100 Subject: [PATCH 30/59] Update error props and displaying error --- src/pages/SetPasswordPage.js | 6 +++--- src/pages/settings/PasswordPage.js | 7 ++++--- src/pages/signin/LoginForm.js | 2 +- src/pages/signin/PasswordForm.js | 3 ++- src/pages/signin/SignInPage.js | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index 0f35f098e54e..82c7ca263b1c 100755 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -27,7 +27,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - errors: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Whether a sign on form is loading (being submitted) */ isLoading: PropTypes.bool, @@ -48,7 +48,7 @@ const propTypes = { /** Session object */ session: PropTypes.shape({ /** An error message to display to the user */ - errors: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), }), /** The accountID and validateCode are passed via the URL */ @@ -62,7 +62,7 @@ const defaultProps = { credentials: {}, route: validateLinkDefaultProps, session: { - errors: '', + errors: null, authToken: '', }, }; diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index 69270c2bacaf..590d12b7a550 100755 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -18,6 +18,7 @@ import FixedFooter from '../../components/FixedFooter'; import TextInput from '../../components/TextInput'; import * as Session from '../../libs/actions/Session'; import PasswordConfirmationScreen from './PasswordConfirmationScreen'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -25,7 +26,7 @@ const propTypes = { /** Holds information about the users account that is logging in */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Success message to display when necessary */ success: PropTypes.string, @@ -208,9 +209,9 @@ class PasswordPage extends Component { )} - {_.every(this.state.errors, error => !error) && !_.isEmpty(this.props.account.error) && ( + {_.every(this.state.errors, error => !error) && !_.isEmpty(this.props.account.errors) && ( - {this.props.account.error} + {ErrorUtils.getLatestErrorMessage(this.props.account)} )} diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js index 2490103b97e3..b6557ec413a3 100755 --- a/src/pages/signin/LoginForm.js +++ b/src/pages/signin/LoginForm.js @@ -32,7 +32,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** An error message to display to the user */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Success message to display when necessary */ success: PropTypes.string, diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index 2d3d0e418e53..c70f25633121 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -20,6 +20,7 @@ import * as ComponentUtils from '../../libs/ComponentUtils'; import * as ValidationUtils from '../../libs/ValidationUtils'; import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView'; import canFocusInputOnScreenFocus from '../../libs/canFocusInputOnScreenFocus'; +import * as ErrorUtils from '../../libs/ErrorUtils'; const propTypes = { /* Onyx Props */ @@ -179,7 +180,7 @@ class PasswordForm extends React.Component { {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.error) && ( - {this.props.account.error} + {ErrorUtils.getLatestErrorMessage(this.props.account)} )} diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index c19d8ba09fe9..065a3206ea94 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -20,7 +20,7 @@ const propTypes = { /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** Error to display when there is an account error returned */ - error: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), /** Whether the account is validated */ validated: PropTypes.bool, From 1e5a1ecacb2c361dc9ec51ba34c08a0954a27a9c Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 17:54:02 +0100 Subject: [PATCH 31/59] Use errors object in Onyx key --- src/CONST.js | 2 +- src/libs/actions/Session/index.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 6e73e7cb5ab8..5440e746ad3a 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -408,7 +408,7 @@ const CONST = { FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {errors: '', success: '', loading: false}, + DEFAULT_ACCOUNT_DATA: {errors: null, success: '', loading: false}, APP_STATE: { ACTIVE: 'active', BACKGROUND: 'background', diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 10d329fc64c9..ec2661a13a1b 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -188,7 +188,7 @@ function createTemporaryLogin(authToken, email) { }) .then((createLoginResponse) => { if (createLoginResponse.jsonCode !== 200) { - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: createLoginResponse.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translate('createLoginResponse.message')}}); return createLoginResponse; } @@ -250,7 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: Localize.translateLocal(errorMessage), isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); return; } @@ -348,7 +348,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: response.message}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds]: response.message}}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); From a48e38a34b7aeccf7d7265084eea2a66fb0f28e7 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 17:54:27 +0100 Subject: [PATCH 32/59] Suppress cyclic dependency error temporarily --- src/libs/Authentication.js | 1 + src/libs/DateUtils.js | 1 + src/libs/Middleware/Reauthentication.js | 1 + src/libs/Middleware/index.js | 1 + src/libs/actions/NameValuePair.js | 1 + src/libs/actions/PersonalDetails.js | 2 ++ src/libs/actions/SignInRedirect.js | 1 + src/libs/deprecatedAPI.js | 1 + 8 files changed, 9 insertions(+) diff --git a/src/libs/Authentication.js b/src/libs/Authentication.js index cd32c958eb25..85d1a4a2c573 100644 --- a/src/libs/Authentication.js +++ b/src/libs/Authentication.js @@ -3,6 +3,7 @@ import * as Network from './Network'; import * as NetworkStore from './Network/NetworkStore'; import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens'; import CONFIG from '../CONFIG'; +// eslint-disable-next-line import/no-cycle import redirectToSignIn from './actions/SignInRedirect'; import CONST from '../CONST'; import Log from './Log'; diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index 0c432888b3b8..13aef6919e59 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -9,6 +9,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import * as Localize from './Localize'; +// eslint-disable-next-line import/no-cycle import * as PersonalDetails from './actions/PersonalDetails'; import * as CurrentDate from './actions/CurrentDate'; diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.js index 4f346dda9563..79b76ca19f41 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.js @@ -2,6 +2,7 @@ import lodashGet from 'lodash/get'; import CONST from '../../CONST'; import * as NetworkStore from '../Network/NetworkStore'; import * as MainQueue from '../Network/MainQueue'; +// eslint-disable-next-line import/no-cycle import * as Authentication from '../Authentication'; import * as PersistedRequests from '../actions/PersistedRequests'; import * as Request from '../Request'; diff --git a/src/libs/Middleware/index.js b/src/libs/Middleware/index.js index 4e270b009c1d..62c5d6c1aaf9 100644 --- a/src/libs/Middleware/index.js +++ b/src/libs/Middleware/index.js @@ -1,4 +1,5 @@ import Logging from './Logging'; +// eslint-disable-next-line import/no-cycle import Reauthentication from './Reauthentication'; import RecheckConnection from './RecheckConnection'; import Retry from './Retry'; diff --git a/src/libs/actions/NameValuePair.js b/src/libs/actions/NameValuePair.js index 17563e8a8b27..8363b2b961c3 100644 --- a/src/libs/actions/NameValuePair.js +++ b/src/libs/actions/NameValuePair.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; +// eslint-disable-next-line import/no-cycle import * as DeprecatedAPI from '../deprecatedAPI'; /** diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 55bc10f84cd6..92a39ea9a505 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -6,7 +6,9 @@ import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../ONYXKEYS'; import CONST from '../../CONST'; import * as API from '../API'; +// eslint-disable-next-line import/no-cycle import * as DeprecatedAPI from '../deprecatedAPI'; +// eslint-disable-next-line import/no-cycle import NameValuePair from './NameValuePair'; import * as LoginUtils from '../LoginUtils'; import * as ReportUtils from '../ReportUtils'; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 4806242051dd..fc88042a8809 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; +// eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; let currentActiveClients; diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 26a25ff2c9e7..a1646f6cee23 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -3,6 +3,7 @@ import isViaExpensifyCashNative from './isViaExpensifyCashNative'; import requireParameters from './requireParameters'; import * as Request from './Request'; import * as Network from './Network'; +// eslint-disable-next-line import/no-cycle import * as Middleware from './Middleware'; import CONST from '../CONST'; From 7f9d0ca6ef5795118186e37f88c03f8c5723cd22 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 7 Sep 2022 18:51:00 +0100 Subject: [PATCH 33/59] Fix lint error by using multilingual error message --- src/libs/actions/SignInRedirect.js | 3 ++- src/libs/actions/User.js | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index fc88042a8809..6741fabfb4c8 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,6 +3,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; +import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ @@ -47,7 +48,7 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); } }); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index bf67d7d42d29..2ea286a56c98 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -233,8 +233,7 @@ function validateLogin(accountID, validateCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {success}); } } else { - const error = lodashGet(response, 'message', 'Unable to validate login.'); - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: error}}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('resendValidationForm.validationCodeFailedMessage')}}); } }).finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); From b199711695ef50814fc1ed80b68204b33ad7a410 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 9 Sep 2022 11:20:36 +0100 Subject: [PATCH 34/59] add new test files and mocks Add mock Fix UrbanAirship mock Get main drawer to render rename to navigation test Just render the sidebar links for now Fix tests and make ReportScreen render Add report action and verify the action renders Test for report-action-item visibility Mock disconnect for Pusher add way to set initial url for testing; Test for drawer status Test for the unread indicator Mock local notification and make sure only one notification appears upgrade test library dependencies and use act to get rid of weird warning undo test version bumps Fix tests Add comment Test behavior of unread indicator in report and sidebar Rename back to unread indicators Test new messages badge indicator refactor and improve Report screen visibility check Update mock for AppState Clear new line indicator when report page is in view and we return from background remove extra logs add JSDocs Check the LHN lastMessageText Fix ReportTest Check for pendingAction when deleting comment Fix deleted comments not showing strikethrough while offline Stop allowing deletion or editing of pendingAction add comments as users cannot be updated correctly via sequenceNumber Rollback some changes Use sequenceNumber for handledReportActions for now Remove log add doc --- .../@react-native-firebase/crashlytics.js | 1 + __mocks__/pusher-js/react-native.js | 8 +- __mocks__/react-native-safe-area-context.js | 44 ++ __mocks__/react-native.js | 66 +++ __mocks__/urbanairship-react-native.js | 12 +- jest/setup.js | 140 ++++++- package-lock.json | 2 +- package.json | 1 + src/components/Checkbox.js | 3 +- src/components/DisplayNames/index.native.js | 2 +- src/components/OptionRow.js | 9 +- src/components/OptionsList/BaseOptionsList.js | 1 + src/components/ReportWelcomeText.js | 3 +- src/components/Text.js | 4 + src/components/UnreadActionIndicator.js | 2 +- src/libs/ReportActionsUtils.js | 71 +++- src/libs/ReportUtils.js | 2 + src/libs/actions/Report.js | 69 ++- src/libs/actions/ReportActions.js | 101 ----- src/pages/home/HeaderView.js | 18 +- src/pages/home/ReportHeaderViewBackButton.js | 30 ++ src/pages/home/ReportScreen.js | 6 + .../FloatingMessageCounterContainer/index.js | 2 +- .../report/FloatingMessageCounter/index.js | 2 +- src/pages/home/report/ReportActionCompose.js | 3 +- src/pages/home/report/ReportActionItem.js | 4 +- .../home/report/ReportActionItemCreated.js | 13 +- .../home/report/ReportActionItemMessage.js | 45 +- src/pages/home/report/ReportActionsList.js | 12 +- src/pages/home/report/ReportActionsView.js | 11 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- src/pages/signin/LoginForm.js | 2 +- src/setup/platformSetup/index.native.js | 2 +- src/styles/styles.js | 4 - tests/actions/ReportTest.js | 18 +- tests/ui/UnreadIndicatorsTest.js | 395 ++++++++++++++++++ tests/unit/LHNOrderTest.js | 7 - tests/unit/loginTest.js | 12 - tests/utils/TestHelper.js | 78 +++- 39 files changed, 951 insertions(+), 256 deletions(-) create mode 100644 __mocks__/@react-native-firebase/crashlytics.js create mode 100644 __mocks__/react-native-safe-area-context.js create mode 100644 __mocks__/react-native.js create mode 100644 src/pages/home/ReportHeaderViewBackButton.js create mode 100644 tests/ui/UnreadIndicatorsTest.js diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/__mocks__/@react-native-firebase/crashlytics.js @@ -0,0 +1 @@ +export default {}; diff --git a/__mocks__/pusher-js/react-native.js b/__mocks__/pusher-js/react-native.js index 0cb6afb4bb1d..1edec34ffb14 100644 --- a/__mocks__/pusher-js/react-native.js +++ b/__mocks__/pusher-js/react-native.js @@ -1,3 +1,9 @@ import {PusherMock} from 'pusher-js-mock'; -export default PusherMock; +class PusherMockWithDisconnect extends PusherMock { + disconnect() { + return jest.fn(); + } +} + +export default PusherMockWithDisconnect; diff --git a/__mocks__/react-native-safe-area-context.js b/__mocks__/react-native-safe-area-context.js new file mode 100644 index 000000000000..82152de8de11 --- /dev/null +++ b/__mocks__/react-native-safe-area-context.js @@ -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 => ( + + ); + return forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )); +} + +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, +}; diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js new file mode 100644 index 000000000000..3e407252dc78 --- /dev/null +++ b/__mocks__/react-native.js @@ -0,0 +1,66 @@ +/* eslint-disable arrow-body-style */ +// eslint-disable-next-line no-restricted-imports +import * as ReactNative from 'react-native'; +import _ from 'underscore'; + +jest.doMock('react-native', () => { + let url = 'https://new.expensify.com/'; + const getInitialURL = () => { + return Promise.resolve(url); + }; + + let appState = 'active'; + let count = 0; + const changeListeners = {}; + 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: () => ({ + width: 300, height: 700, scale: 1, fontScale: 1, + }), + }, + }, + ReactNative, + ); +}); diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js index c4e8a3939325..efbb5d87f92b 100644 --- a/__mocks__/urbanairship-react-native.js +++ b/__mocks__/urbanairship-react-native.js @@ -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, }; diff --git a/jest/setup.js b/jest/setup.js index c380ebb56669..627d43466b74 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -1,15 +1,143 @@ +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/Libraries/Animated/NativeAnimatedHelper'); -// 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: () => {}, +jest.mock('react-native-blob-util', () => ({})); + +jest.mock('react-native-reanimated', () => { + const Reanimated = require('react-native-reanimated/mock'); + + // The mock for `call` immediately calls the callback which is incorrect + // So we override it with a no-op + Reanimated.default.call = () => {}; + return Reanimated; +}); + +// uses 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 +jest.mock('@react-native-firebase/crashlytics', () => () => ({ + log: jest.fn(), + recordError: jest.fn(), })); -jest.mock('react-native-blob-util', () => ({})); +jest.mock('../src/libs/BootSplash', () => ({ + hide: jest.fn(), + getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), +})); + +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}`, () => () => ''); + } + }); +} + +// Mock all images so that Icons and other assets cannot break tests +mockImages('images'); +mockImages('images/avatars'); +mockImages('images/bankicons'); +mockImages('images/product-illustrations'); +jest.mock('../src/components/Icon/Expensicons', () => ({ + ActiveRoomAvatar: () => '', + AdminRoomAvatar: () => '', + Android: () => '', + AnnounceRoomAvatar: () => '', + Apple: () => '', + ArrowRight: () => '', + BackArrow: () => '', + Bank: () => '', + Bill: () => '', + Bolt: () => '', + Briefcase: () => '', + Bug: () => '', + Building: () => '', + Camera: () => '', + Cash: () => '', + ChatBubble: () => '', + Checkmark: () => '', + CircleHourglass: () => '', + Clipboard: () => '', + Close: () => '', + ClosedSign: () => '', + Collapse: () => '', + Concierge: () => '', + Connect: () => '', + CreditCard: () => '', + DeletedRoomAvatar: () => '', + DomainRoomAvatar: () => '', + DotIndicator: () => '', + DownArrow: () => '', + Download: () => '', + Emoji: () => '', + Exclamation: () => '', + Exit: () => '', + ExpensifyCard: () => '', + Expand: () => '', + Eye: () => '', + EyeDisabled: () => '', + FallbackAvatar: () => '', + FallbackWorkspaceAvatar: () => '', + Gallery: () => '', + Gear: () => '', + Hashtag: () => '', + ImageCropMask: () => '', + Info: () => '', + Invoice: () => '', + Key: () => '', + Keyboard: () => '', + Link: () => '', + LinkCopy: () => '', + Lock: () => '', + Luggage: () => '', + MagnifyingGlass: () => '', + Mail: () => '', + MoneyBag: () => '', + MoneyCircle: () => '', + Monitor: () => '', + NewWindow: () => '', + NewWorkspace: () => '', + Offline: () => '', + OfflineCloud: () => '', + Paperclip: () => '', + PayPal: () => '', + Paycheck: () => '', + Pencil: () => '', + Phone: () => '', + Pin: () => '', + PinCircle: () => '', + Plus: () => '', + Printer: () => '', + Profile: () => '', + QuestionMark: () => '', + Receipt: () => '', + ReceiptSearch: () => '', + Rotate: () => '', + RotateLeft: () => '', + Send: () => '', + Sync: () => '', + ThreeDots: () => '', + Transfer: () => '', + Trashcan: () => '', + UpArrow: () => '', + Upload: () => '', + Users: () => '', + Wallet: () => '', + Workspace: () => '', + Zoom: () => '', +})); diff --git a/package-lock.json b/package-lock.json index b13378e833d2..1662a0358d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44475,7 +44475,7 @@ "@oguzhnatly/react-native-image-manipulator": { "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", - "from": "@oguzhnatly/react-native-image-manipulator@https://github.com/Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" + "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" }, "@onfido/active-video-capture": { "version": "0.0.1", diff --git a/package.json b/package.json index f9a67bfa8cee..272255fac56d 100644 --- a/package.json +++ b/package.json @@ -201,6 +201,7 @@ "/node_modules/" ], "testMatch": [ + "**/tests/ui/**/*.[jt]s?(x)", "**/tests/unit/**/*.[jt]s?(x)", "**/tests/actions/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js index ab5dde69f505..cc94b2e842a1 100644 --- a/src/components/Checkbox.js +++ b/src/components/Checkbox.js @@ -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, @@ -33,6 +33,7 @@ const propTypes = { }; const defaultProps = { + isChecked: false, hasError: false, disabled: false, style: [], diff --git a/src/components/DisplayNames/index.native.js b/src/components/DisplayNames/index.native.js index 6ccb53888fd6..91ed0406a0d7 100644 --- a/src/components/DisplayNames/index.native.js +++ b/src/components/DisplayNames/index.native.js @@ -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 => ( - + {props.fullTitle} ); diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index cadae24e801e..40471941aba4 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -128,7 +128,10 @@ const OptionRow = (props) => { touchableRef = el} onPress={(e) => { - e.preventDefault(); + if (e) { + e.preventDefault(); + } + props.onSelectRow(props.option, touchableRef); }} disabled={props.isDisabled} @@ -145,7 +148,7 @@ const OptionRow = (props) => { props.isDisabled && styles.cursorDisabled, ]} > - + { } { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 17ce25e26551..e5524c49dab5 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -223,6 +223,7 @@ class BaseOptionsList extends Component { ) : null} { diff --git a/src/components/Text.js b/src/components/Text.js index 84a78d3e37c5..47a037ae735f 100644 --- a/src/components/Text.js +++ b/src/components/Text.js @@ -26,6 +26,9 @@ const propTypes = { /** Any additional styles to apply */ // eslint-disable-next-line react/forbid-prop-types style: PropTypes.any, + + /** Optional testID for testing library */ + testID: PropTypes.string, }; const defaultProps = { color: themeColors.text, @@ -34,6 +37,7 @@ const defaultProps = { textAlign: 'left', children: null, style: {}, + testID: undefined, }; const Text = React.forwardRef(({ diff --git a/src/components/UnreadActionIndicator.js b/src/components/UnreadActionIndicator.js index 389413e4f38e..32a48b650fe3 100755 --- a/src/components/UnreadActionIndicator.js +++ b/src/components/UnreadActionIndicator.js @@ -5,7 +5,7 @@ import Text from './Text'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; const UnreadActionIndicator = props => ( - + {props.translate('common.new')} diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 29f22e0f68c0..a86027cb35ef 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -1,6 +1,25 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; +import lodashMerge from 'lodash/merge'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import Onyx from 'react-native-onyx'; +import * as CollectionUtils from './CollectionUtils'; import CONST from '../CONST'; +import ONYXKEYS from '../ONYXKEYS'; +import * as ReportUtils from './ReportUtils'; + +const allReportActions = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + callback: (actions, key) => { + if (!key || !actions) { + return; + } + + const reportID = CollectionUtils.extractCollectionItemID(key); + allReportActions[reportID] = actions; + }, +}); /** * @param {Object} reportAction @@ -22,7 +41,9 @@ function getSortedReportActions(reportActions) { return _.chain(reportActions) .sortBy('sequenceNumber') .filter(action => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU - || (action.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !isDeletedAction(action)) + + // All comment actions are shown unless they are deleted and non-pending + || (action.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && (!isDeletedAction(action) || !_.isEmpty(action.pendingAction))) || action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) .map((item, index) => ({action: item, index})) @@ -76,7 +97,55 @@ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { return currentAction.action.actorEmail === previousAction.action.actorEmail; } +/** + * Get the message text for the last action that was not deleted + * @param {Number} reportID + * @param {Object} [actionsToMerge] + * @return {String} + */ +function getLastVisibleMessageText(reportID, actionsToMerge = {}) { + const parser = new ExpensiMark(); + const actions = _.toArray(lodashMerge({}, allReportActions[reportID], actionsToMerge)); + const sortedActions = _.sortBy(actions, 'sequenceNumber'); + const lastMessageIndex = _.findLastIndex(sortedActions, action => ( + !isDeletedAction(action) + )); + const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); + const messageText = parser.htmlToText(htmlText); + return ReportUtils.formatReportLastMessageText(messageText); +} + +/** + * @param {Number} reportID + * @param {Object} [actionsToMerge] + * @param {Number} deletedSequenceNumber + * @param {Number} lastReadSequenceNumber + * @return {String} + */ +function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { + // If the action we are deleting is unread then just return the current last read sequence number + if (deletedSequenceNumber > lastReadSequenceNumber) { + return lastReadSequenceNumber; + } + + // Otherwise, we must find the first previous index of an action that is not deleted and less than the lastReadSequenceNumber + const actions = _.toArray(lodashMerge({}, allReportActions[reportID], actionsToMerge)); + const sortedActions = _.sortBy(actions, 'sequenceNumber'); + const lastMessageIndex = _.findLastIndex(sortedActions, action => ( + !isDeletedAction(action) && action.sequenceNumber <= lastReadSequenceNumber + )); + + // It's possible we won't find any and in that case the last read should be reset + if (lastMessageIndex < 0) { + return 0; + } + + return actions[lastMessageIndex].sequenceNumber; +} + export { + getNewLastReadSequenceNumberForDeletedAction, + getLastVisibleMessageText, getSortedReportActions, getMostRecentIOUReportSequenceNumber, isDeletedAction, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index e48939c5e7e9..1cc13e65c7f7 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -101,6 +101,7 @@ function canEditReportAction(reportAction) { && reportAction.reportActionID && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } @@ -116,6 +117,7 @@ function canDeleteReportAction(reportAction) { return reportAction.actorEmail === sessionEmail && reportAction.reportActionID && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3d9d75cda781..9d977290278d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -11,7 +11,6 @@ import PushNotification from '../Notification/PushNotification'; import * as PersonalDetails from './PersonalDetails'; import Navigation from '../Navigation/Navigation'; import * as ActiveClientManager from '../ActiveClientManager'; -import Visibility from '../Visibility'; import ROUTES from '../../ROUTES'; import Timing from './Timing'; import * as DeprecatedAPI from '../deprecatedAPI'; @@ -21,12 +20,12 @@ import CONST from '../../CONST'; import Log from '../Log'; import * as LoginUtils from '../LoginUtils'; import * as ReportUtils from '../ReportUtils'; -import * as ReportActions from './ReportActions'; import Growl from '../Growl'; import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as NumberUtils from '../NumberUtils'; +import Visibility from '../Visibility'; let currentUserEmail; let currentUserAccountID; @@ -802,10 +801,20 @@ function addActions(reportID, text = '', file) { const lastAction = attachmentAction || reportCommentAction; // We need a newSequenceNumber that is n larger than the current depending on how many actions we are adding. - const actionCount = text && file ? 2 : 1; const highestSequenceNumber = getMaxSequenceNumber(reportID); + const actionCount = text && file ? 2 : 1; const newSequenceNumber = highestSequenceNumber + actionCount; + // We guess at what these sequenceNumbers are to enable marking as unread while offline + if (text && file) { + reportCommentAction.sequenceNumber = highestSequenceNumber + 1; + attachmentAction.sequenceNumber = highestSequenceNumber + 2; + } else if (file) { + attachmentAction.sequenceNumber = highestSequenceNumber + 1; + } else { + reportCommentAction.sequenceNumber = highestSequenceNumber + 1; + } + // Update the report in Onyx to have the new sequence number const optimisticReport = { maxSequenceNumber: newSequenceNumber, @@ -815,13 +824,14 @@ function addActions(reportID, text = '', file) { lastReadSequenceNumber: newSequenceNumber, }; - // Optimistically add the new actions to the store before waiting to save them to the server + // Optimistically add the new actions to the store before waiting to save them to the server. We use the clientID + // so that we can later unset these messages via the server by sending {[clientID]: null} const optimisticReportActions = {}; if (text) { - optimisticReportActions[reportCommentAction.sequenceNumber] = reportCommentAction; + optimisticReportActions[reportCommentAction.clientID] = reportCommentAction; } if (file) { - optimisticReportActions[attachmentAction.sequenceNumber] = attachmentAction; + optimisticReportActions[attachmentAction.clientID] = attachmentAction; } const parameters = { @@ -1159,26 +1169,32 @@ Onyx.connect({ */ function deleteReportComment(reportID, reportAction) { const sequenceNumber = reportAction.sequenceNumber; - - // We are not updating the message content here so the message can re-appear as strike-throughed - // if the user goes offline. The API will update the message content to empty strings on success. + const deletedMessage = [{ + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + }]; const optimisticReportActions = { [sequenceNumber]: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + previousMessage: reportAction.message, + message: deletedMessage, }, }; - // If we are deleting the last visible message, let's find the previous visible one - // and update the lastMessageText in the chat preview. + // If we are deleting the last visible message, let's find the previous visible one and update the lastMessageText in the LHN. + // Similarly, we are deleting the last read comment will want to update the lastReadSequenceNumber to use the previous visible message. + const lastMessageText = ReportActionsUtils.getLastVisibleMessageText(reportID, optimisticReportActions); + const lastReadSequenceNumber = ReportActionsUtils.getNewLastReadSequenceNumberForDeletedAction( + reportID, + optimisticReportActions, + reportAction.sequenceNumber, + getLastReadSequenceNumber(reportID), + ); const optimisticReport = { - lastMessageText: ReportActions.getLastVisibleMessageText(reportID, { - [sequenceNumber]: { - message: [{ - html: '', - text: '', - }], - }, - }), + lastMessageText, + lastReadSequenceNumber, }; // If the API call fails we must show the original message again, so we revert the message content back to how it was @@ -1191,6 +1207,7 @@ function deleteReportComment(reportID, reportAction) { [sequenceNumber]: { message: reportAction.message, pendingAction: null, + previousMessage: null, }, }, }, @@ -1203,6 +1220,7 @@ function deleteReportComment(reportID, reportAction) { value: { [sequenceNumber]: { pendingAction: null, + previousMessage: null, }, }, }, @@ -1511,11 +1529,14 @@ function viewNewReportAction(reportID, action) { const lastReadSequenceNumber = getLastReadSequenceNumber(reportID); const updatedReportObject = {}; - // When handling an action from the current user we can assume that their last read actionID has been updated in the server, but not necessarily reflected - // locally so we will update the lastReadSequenceNumber to mark the report as read. + // When handling an action from the current user we can assume that their last read actionID has been updated in the server, + // but not necessarily reflected locally so we will update the lastReadSequenceNumber to mark the report as read. if (isFromCurrentUser) { updatedReportObject.lastVisitedTimestamp = Date.now(); updatedReportObject.lastReadSequenceNumber = action.pendingAction ? lastReadSequenceNumber : action.sequenceNumber; + updatedReportObject.maxSequenceNumber = action.sequenceNumber; + } else { + updatedReportObject.maxSequenceNumber = action.sequenceNumber; } Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, updatedReportObject); @@ -1589,7 +1610,9 @@ Onyx.connect({ return; } - if (action.isLoading) { + // We don't want to process any new actions that have a pendingAction field as this means they are "optimistic" and no notifications + // should be created for them + if (!_.isEmpty(action.pendingAction)) { return; } @@ -1599,6 +1622,8 @@ Onyx.connect({ // If we are past the deadline to notify for this comment don't do it if (moment.utc(action.timestamp * 1000).isBefore(moment.utc().subtract(10, 'seconds'))) { + handledReportActions[reportID] = handledReportActions[reportID] || {}; + handledReportActions[reportID][action.sequenceNumber] = true; return; } diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index 504e915ef76b..f1b7c3380acc 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -1,103 +1,5 @@ -import _ from 'underscore'; import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; -import lodashMerge from 'lodash/merge'; -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import ONYXKEYS from '../../ONYXKEYS'; -import * as CollectionUtils from '../CollectionUtils'; -import CONST from '../../CONST'; -import * as ReportUtils from '../ReportUtils'; -import * as ReportActionsUtils from '../ReportActionsUtils'; - -/** - * Map of the most recent non-loading sequenceNumber for a reportActions_* key in Onyx by reportID. - * - * What's the difference between reportMaxSequenceNumbers and reportActionsMaxSequenceNumbers? - * - * Knowing the maxSequenceNumber for a report does not necessarily mean we have stored the report actions for that - * report. To understand and optimize which reportActions we need to fetch we also keep track of the max sequenceNumber - * for the stored reportActions in reportActionsMaxSequenceNumbers. This allows us to initially download all - * reportActions when the app starts up and then only download the actions that we need when the app reconnects. - * - * This information should only be used in the correct contexts. In most cases, reportMaxSequenceNumbers should be - * referenced and not the locally stored reportAction's max sequenceNumber. - */ -const reportActionsMaxSequenceNumbers = {}; -const reportActions = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { - return; - } - - const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray = _.toArray(actions); - reportActions[reportID] = actionsArray; - const mostRecentNonLoadingActionIndex = _.findLastIndex(actionsArray, action => !action.isLoading); - const mostRecentAction = actionsArray[mostRecentNonLoadingActionIndex]; - if (!mostRecentAction || _.isUndefined(mostRecentAction.sequenceNumber)) { - return; - } - - reportActionsMaxSequenceNumbers[reportID] = mostRecentAction.sequenceNumber; - }, -}); - -/** - * Get the count of deleted messages after a sequence number of a report - * @param {Number|String} reportID - * @param {Number} sequenceNumber - * @return {Number} - */ -function getDeletedCommentsCount(reportID, sequenceNumber) { - if (!reportActions[reportID]) { - return 0; - } - - return _.reduce(reportActions[reportID], (numDeletedMessages, action) => { - if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT || action.sequenceNumber <= sequenceNumber) { - return numDeletedMessages; - } - - // Empty ADDCOMMENT actions typically mean they have been deleted - const message = _.first(lodashGet(action, 'message', null)); - const html = lodashGet(message, 'html', ''); - return _.isEmpty(html) ? numDeletedMessages + 1 : numDeletedMessages; - }, 0); -} - -/** - * Get the message text for the last action that was not deleted - * @param {Number} reportID - * @param {Object} [actionsToMerge] - * @return {String} - */ -function getLastVisibleMessageText(reportID, actionsToMerge = {}) { - const parser = new ExpensiMark(); - const existingReportActions = _.indexBy(reportActions[reportID], 'sequenceNumber'); - const actions = _.toArray(lodashMerge({}, existingReportActions, actionsToMerge)); - const lastMessageIndex = _.findLastIndex(actions, action => ( - !ReportActionsUtils.isDeletedAction(action) - )); - const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); - const messageText = parser.htmlToText(htmlText); - return ReportUtils.formatReportLastMessageText(messageText); -} - -/** - * @param {Number} reportID - * @param {Number} sequenceNumber - * @param {Number} currentUserAccountID - * @param {Object} [actionsToMerge] - * @returns {Boolean} - */ -function isFromCurrentUser(reportID, sequenceNumber, currentUserAccountID, actionsToMerge = {}) { - const existingReportActions = _.indexBy(reportActions[reportID], 'sequenceNumber'); - const action = lodashMerge({}, existingReportActions, actionsToMerge)[sequenceNumber]; - return action.actorAccountID === currentUserAccountID; -} /** * @param {Number} reportID @@ -122,9 +24,6 @@ function clearReportActionErrors(reportID, sequenceNumber) { } export { - getDeletedCommentsCount, - getLastVisibleMessageText, clearReportActionErrors, - isFromCurrentUser, deleteOptimisticReportAction, }; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 93cd78ea63c5..ea5db9406a7e 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -26,6 +26,7 @@ import Text from '../../components/Text'; import Tooltip from '../../components/Tooltip'; import variables from '../../styles/variables'; import colors from '../../styles/colors'; +import ReportHeaderViewBackButton from './ReportHeaderViewBackButton'; const propTypes = { /** Toggles the navigationMenu open and closed */ @@ -49,7 +50,7 @@ const propTypes = { policies: PropTypes.shape({ /** Name of the policy */ name: PropTypes.string, - }).isRequired, + }), /** Personal details of all the users */ personalDetails: PropTypes.objectOf(participantPropTypes), @@ -60,6 +61,7 @@ const propTypes = { const defaultProps = { personalDetails: {}, + policies: {}, report: null, }; @@ -89,17 +91,13 @@ const HeaderView = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; return ( - + {props.isSmallScreenWidth && ( - - - - - + )} {Boolean(props.report && title) && ( ( + + + + + +); + +ReportHeaderViewBackButton.displayName = 'ReportHeaderViewBackButton'; +ReportHeaderViewBackButton.propTypes = propTypes; +export default ReportHeaderViewBackButton; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f852dfb2626f..4e43bb06956c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -26,6 +26,7 @@ import addViewportResizeListener from '../../libs/VisualViewport'; import {withNetwork} from '../../components/OnyxProvider'; import compose from '../../libs/compose'; import networkPropTypes from '../../components/networkPropTypes'; +import withDrawerState, {withDrawerPropTypes} from '../../components/withDrawerState'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -80,6 +81,8 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, + + ...withDrawerPropTypes, }; const defaultProps = { @@ -234,6 +237,7 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} @@ -250,6 +254,7 @@ class ReportScreen extends React.Component { report={this.props.report} session={this.props.session} isComposerFullSize={this.props.isComposerFullSize} + isDrawerOpen={this.props.isDrawerOpen} /> )} {(isArchivedRoom || this.props.session.shouldShowComposeInput) && ( @@ -285,6 +290,7 @@ ReportScreen.propTypes = propTypes; ReportScreen.defaultProps = defaultProps; export default compose( + withDrawerState, withNetwork(), withOnyx({ isSidebarLoaded: { diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js index c5a9ed5078eb..a367eea5f602 100644 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js @@ -4,7 +4,7 @@ import styles from '../../../../../styles/styles'; import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; const FloatingMessageCounterContainer = props => ( - + {props.children} ); diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index 7025d5a67a4f..8af8266b0a87 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -62,7 +62,7 @@ class FloatingMessageCounter extends PureComponent { render() { return ( - + {hovered => ( - + {this.props.shouldDisplayNewIndicator && ( - + )} { const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); - return ( - ReportUtils.navigateToDetailsPage(props.report)}> diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index a066b2be34f8..d226ac9ced90 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -6,18 +6,12 @@ import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; -import {withNetwork} from '../../../components/OnyxProvider'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import compose from '../../../libs/compose'; -import networkPropTypes from '../../../components/networkPropTypes'; const propTypes = { /** The report action */ action: PropTypes.shape(reportActionPropTypes).isRequired, - /** Information about the network */ - network: networkPropTypes.isRequired, - /** Additional styles to add after local styles. */ style: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.object), @@ -32,31 +26,24 @@ const defaultProps = { style: [], }; -const ReportActionItemMessage = (props) => { - const isUnsent = props.network.isOffline && props.action.isLoading; - - return ( - - {_.map(_.compact(props.action.message), (fragment, index) => ( - - ))} - - ); -}; +const ReportActionItemMessage = props => ( + + {_.map(_.compact(props.action.previousMessage || props.action.message), (fragment, index) => ( + + ))} + +); ReportActionItemMessage.propTypes = propTypes; ReportActionItemMessage.defaultProps = defaultProps; ReportActionItemMessage.displayName = 'ReportActionItemMessage'; -export default compose( - withNetwork(), - withLocalize, -)(ReportActionItemMessage); +export default withLocalize(ReportActionItemMessage); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index bdfe34ce2d1c..77e54bd39162 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -106,14 +106,14 @@ class ReportActionsList extends React.Component { } /** - * Create a unique key for Each Action in the FlatList. - * We use the reportActionId that is a string representation of a random 64-bit int, which should be - * random enought to avoid colisions + * Create a unique key for each action in the FlatList. + * We use the reportActionID that is a string representation of a random 64-bit int, which should be + * random enough to avoid collisions * @param {Object} item * @return {String} */ keyExtractor(item) { - return `${item.action.reportActionID}${item.action.sequenceNumber}`; + return `${item.action.clientID}${item.action.reportActionID}${item.action.sequenceNumber}`; } /** @@ -135,7 +135,8 @@ class ReportActionsList extends React.Component { // When the new indicator should not be displayed we explicitly set it to 0. The marker should never be shown above the // created action (which will have sequenceNumber of 0) so we use 0 to indicate "hidden". const shouldDisplayNewIndicator = this.props.newMarkerSequenceNumber > 0 - && item.action.sequenceNumber === this.props.newMarkerSequenceNumber; + && item.action.sequenceNumber === this.props.newMarkerSequenceNumber + && !ReportActionsUtils.isDeletedAction(item.action); return ( { + this.appStateChangeListener = AppState.addEventListener('change', () => { if (!this.getIsReportFullyVisible()) { return; } // If the app user becomes active and they have no unread actions we clear the new marker to sync their device // e.g. they could have read these messages on another device and only just become active here - if (state === 'active' && !ReportUtils.isUnread(this.props.report)) { - this.setState({newMarkerSequenceNumber: 0}); - } - Report.openReport(this.props.report.reportID); + this.setState({newMarkerSequenceNumber: 0}); }); Report.subscribeToReportTypingEvents(this.props.report.reportID); @@ -383,7 +381,6 @@ ReportActionsView.defaultProps = defaultProps; export default compose( Performance.withRenderTrace({id: ' rendering'}), withWindowDimensions, - withDrawerState, withLocalize, withNetwork(), )(ReportActionsView); diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 7028b796a0ea..417c4030a11b 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -253,7 +253,7 @@ class SidebarLinks extends React.Component { }]; return ( - + - + this.input = el} label={this.props.translate('loginForm.phoneOrEmail')} diff --git a/src/setup/platformSetup/index.native.js b/src/setup/platformSetup/index.native.js index cd55b6a280a4..02a0629db1d4 100644 --- a/src/setup/platformSetup/index.native.js +++ b/src/setup/platformSetup/index.native.js @@ -23,7 +23,7 @@ export default function () { Report.subscribeToReportCommentPushNotifications(); // Setup Flipper plugins when on dev - if (__DEV__) { + if (__DEV__ && typeof jest === 'undefined') { require('flipper-plugin-bridgespy-client'); } diff --git a/src/styles/styles.js b/src/styles/styles.js index 467a5609a4bf..8ee0006b18fe 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1330,10 +1330,6 @@ const styles = { ...wordBreak.breakWord, }, - chatItemUnsentMessage: { - opacity: 0.6, - }, - chatItemMessageLink: { color: colors.blue, fontSize: variables.fontSizeNormal, diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index c8d95f9dcabb..cbf04d3adb58 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -74,7 +74,7 @@ describe('actions/Report', () => { callback: val => reportActions = val, }); - let sequenceNumber; + let clientID; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -92,8 +92,8 @@ describe('actions/Report', () => { .then(() => { const resultAction = _.first(_.values(reportActions)); - // Store the generated sequenceNumber so that we can send it with our mock Pusher update - sequenceNumber = resultAction.sequenceNumber; + // Store the generated clientID so that we can send it with our mock Pusher update + clientID = resultAction.clientID; expect(resultAction.message).toEqual(REPORT_ACTION.message); expect(resultAction.person).toEqual(REPORT_ACTION.person); expect(resultAction.pendingAction).toEqual(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -120,7 +120,7 @@ describe('actions/Report', () => { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, value: { - [sequenceNumber]: null, + [clientID]: null, [ACTION_ID]: actionWithoutLoading, }, }, @@ -348,9 +348,9 @@ describe('actions/Report', () => { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, value: { - [_.toArray(reportActions)[1].sequenceNumber]: null, - [_.toArray(reportActions)[2].sequenceNumber]: null, - [_.toArray(reportActions)[3].sequenceNumber]: null, + [_.toArray(reportActions)[1].clientID]: null, + [_.toArray(reportActions)[2].clientID]: null, + [_.toArray(reportActions)[3].clientID]: null, 2: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 1', text: 'Current User Comment 1'}], @@ -374,7 +374,7 @@ describe('actions/Report', () => { }) .then(() => { // If the user deletes a comment that is before the last read - Report.deleteReportComment(REPORT_ID, reportActions[2]); + Report.deleteReportComment(REPORT_ID, {...reportActions[2], sequenceNumber: 2, clientID: null}); return waitForPromisesToResolve(); }) .then(() => { @@ -392,7 +392,7 @@ describe('actions/Report', () => { expect(report.lastReadSequenceNumber).toBe(2); // If the user deletes the last comment after the last read the lastMessageText will reflect the new last comment - Report.deleteReportComment(REPORT_ID, reportActions[4]); + Report.deleteReportComment(REPORT_ID, {...reportActions[4], sequenceNumber: 4, clientID: null}); return waitForPromisesToResolve(); }) .then(() => { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js new file mode 100644 index 000000000000..d26b6bae09d5 --- /dev/null +++ b/tests/ui/UnreadIndicatorsTest.js @@ -0,0 +1,395 @@ +/* eslint-disable @lwc/lwc/no-async-await */ +import React from 'react'; +import Onyx from 'react-native-onyx'; +import {Linking, AppState} from 'react-native'; +import {fireEvent, render, act} from '@testing-library/react-native'; +import lodashGet from 'lodash/get'; +import moment from 'moment'; +import App from '../../src/App'; +import CONST from '../../src/CONST'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import * as TestHelper from '../utils/TestHelper'; +import appSetup from '../../src/setup'; +import fontWeightBold from '../../src/styles/fontWeight/bold'; +import * as AppActions from '../../src/libs/actions/App'; +import ReportHeaderViewBackButton from '../../src/pages/home/ReportHeaderViewBackButton'; +import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; +import * as NumberUtils from '../../src/libs/NumberUtils'; +import LocalNotification from '../../src/libs/Notification/LocalNotification'; +import * as Report from '../../src/libs/actions/Report'; + +beforeAll(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + + // We need a bit more time for this test in some places + jest.setTimeout(30000); + Linking.setInitialURL('https://new.expensify.com/r/1'); + appSetup(); +}); + +/** + * @param {RenderAPI} renderedApp + */ +function scrollUpToRevealNewMessagesBadge(renderedApp) { + fireEvent.scroll(renderedApp.getByTestId('report-actions-list'), { + nativeEvent: { + contentOffset: { + y: 250, + }, + contentSize: { + // Dimensions of the scrollable content + height: 500, + width: 100, + }, + layoutMeasurement: { + // Dimensions of the device + height: 700, + width: 300, + }, + }, + }); + + // We advance the timer since we must wait for the animation to end + // and the new style to be reflected + jest.advanceTimersByTime(100); +} + +/** + * @param {RenderAPI} renderedApp + * @return {Boolean} + */ +function isNewMessagesBadgeVisible(renderedApp) { + const badge = renderedApp.getByTestId('new-messages-badge'); + return badge.props.style.transform[0].translateY === 10; +} + +/** + * @param {RenderAPI} renderedApp + * @return {Promise} + */ +async function navigateToSidebar(renderedApp) { + const reportHeader = renderedApp.getByTestId('report-header'); + const reportHeaderBackButton = reportHeader.findByType(ReportHeaderViewBackButton); + fireEvent(reportHeaderBackButton, 'press'); + return waitForPromisesToResolve(); +} + +/** + * @param {RenderAPI} renderedApp + * @param {Number} index + * @return {Promise} + */ +async function navigateToSidebarOption(renderedApp, index) { + const optionRows = renderedApp.getAllByTestId('option-row'); + fireEvent(optionRows[index], 'press'); + await waitForPromisesToResolve(); +} + +/** + * @param {RenderAPI} renderedApp + * @return {Boolean} + */ +function isDrawerOpen(renderedApp) { + const reportScreen = renderedApp.getByTestId('report-screen'); + return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; +} + +test('Unread LHN Status, “New Messages” badge, and New Line Indicator', async () => { + const REPORT_ID = 1; + const USER_A_ACCOUNT_ID = 1; + const USER_A_EMAIL = 'user_a@test.com'; + const USER_B_ACCOUNT_ID = 2; + const USER_B_EMAIL = 'user_b@test.com'; + const USER_C_ACCOUNT_ID = 3; + const USER_C_EMAIL = 'user_c@test.com'; + + // Render the App and sign in as a test user. + const renderedApp = render(); + + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + await waitForPromisesToResolve(); + }); + + const loginForm = renderedApp.getByTestId('login-form'); + expect(loginForm).toBeTruthy(); + + TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + await waitForPromisesToResolve(); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), // Ten minutes ago + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + await waitForPromisesToResolve(); + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.getByTestId('sidebar-links'); + expect(sidebarLinks).toBeTruthy(); + + // And verify that the Report screen is rendered after manually setting the sidebar as loaded + // since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + await waitForPromisesToResolve(); + + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + let optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + await navigateToSidebarOption(renderedApp, 0); + + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByTestId('report-action-created'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByTestId('report-action-item'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the back button to return to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Navigate to the report again + await navigateToSidebarOption(renderedApp, 0); + + // Verify the unread indicator is no longer present + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + await waitForPromisesToResolve(); + + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + + // Navigate back to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the new report option appears in the LHN + optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(2); + + // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + await navigateToSidebarOption(renderedApp, 0); + + // Verify that all report options appear in a "read" state + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Tap the previous report between User A and User B + await navigateToSidebarOption(renderedApp, 1); + + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + await waitForPromisesToResolve(); + + // Verify the indicator appears above the last action + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the report is marked as unread in the sidebar + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebar(renderedApp); + + // Verify the report is now marked as read + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + await navigateToSidebarOption(renderedApp, 1); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Navigate to the LHN + await navigateToSidebar(renderedApp); + + // Simulate another new message on the report with User B + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + }); + await waitForPromisesToResolve(); + + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report again and verify the indicator exists + await navigateToSidebarOption(renderedApp, 1); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // As the current user add several comments + Report.addComment(REPORT_ID, 'Current User Comment 2'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 3'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 4'); + await waitForPromisesToResolve(); + + // Mark the last comment as "unread" and verify the unread indicator appears + Report.markCommentAsUnread(REPORT_ID, 14); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); +}); diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 3e411f7ca251..d705761a8c20 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -174,13 +174,6 @@ function getDefaultRenderedSidebarLinks() { )); } -// Icons need to be explicitly mocked. The testing library throws an error when trying to render them -jest.mock('../../src/components/Icon/Expensicons', () => ({ - MagnifyingGlass: () => '', - Pencil: () => '', - Pin: () => '', -})); - describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate diff --git a/tests/unit/loginTest.js b/tests/unit/loginTest.js index 830378294c30..c8bcc6c6f00f 100644 --- a/tests/unit/loginTest.js +++ b/tests/unit/loginTest.js @@ -9,18 +9,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; import App from '../../src/App'; -/* uses 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 */ -jest.mock('@react-native-firebase/crashlytics', () => () => ({ - log: jest.fn(), - recordError: jest.fn(), -})); - -jest.mock('../../src/libs/BootSplash', () => ({ - hide: jest.fn(), - getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), -})); - describe('AppComponent', () => { it('renders correctly', () => { renderer.create(); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 92af57ba2c2c..319732eca13e 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -5,18 +5,43 @@ import HttpUtils from '../../src/libs/HttpUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from './waitForPromisesToResolve'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import * as NumberUtils from '../../src/libs/NumberUtils'; + +/** + * @param {String} login + * @param {Number} accountID + * @param {String} [firstName] + * @returns {Promise} + */ +function buildPersonalDetails(login, accountID, firstName = 'Test') { + const avatar = ReportUtils.getDefaultAvatar(login); + return { + accountID, + login, + avatar, + avatarThumbnail: avatar, + displayName: `${firstName} User`, + firstName, + lastName: 'User', + pronouns: '', + timezone: CONST.DEFAULT_TIME_ZONE, + payPalMeAddress: '', + phoneNumber: '', + }; +} /** * Simulate signing in and make sure all API calls in this flow succeed. Every time we add * a mockImplementationOnce() we are altering what Network.post() will return. * - * @param {Number} accountID - * @param {String} login - * @param {String} password - * @param {String} authToken + * @param {Number} [accountID] + * @param {String} [login] + * @param {String} [password] + * @param {String} [authToken] + * @param {String} [firstName] * @return {Promise} */ -function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty') { +function signInWithTestUser(accountID = 1, login = 'test@user.com', password = 'Password1', authToken = 'asdfqwerty', firstName = 'Test') { const originalXhr = HttpUtils.xhr; HttpUtils.xhr = jest.fn(); HttpUtils.xhr.mockImplementation(() => Promise.resolve({ @@ -35,6 +60,13 @@ function signInWithTestUser(accountID = 1, login = 'test@user.com', password = ' validated: true, }, }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS, + value: { + [login]: buildPersonalDetails(login, accountID, firstName), + }, + }, ], jsonCode: 200, })); @@ -86,28 +118,34 @@ function getGlobalFetchMock() { * @returns {Promise} */ function setPersonalDetails(login, accountID) { - const avatar = ReportUtils.getDefaultAvatar(login); - const details = { - accountID, - login, - avatar, - avatarThumbnail: avatar, - displayName: 'Test User', - firstName: 'Test', - lastName: 'User', - pronouns: '', - timezone: CONST.DEFAULT_TIME_ZONE, - payPalMeAddress: '', - phoneNumber: '', - }; Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [login]: details, + [login]: buildPersonalDetails(login, accountID), }); return waitForPromisesToResolve(); } +/** + * @param {String} actorEmail + * @param {Number} sequenceNumber + * @param {Number} timestamp + * @returns {Object} + */ +function buildTestReportComment(actorEmail, sequenceNumber, timestamp) { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail, + person: [{type: 'TEXT', style: 'strong', text: 'User B'}], + sequenceNumber, + timestamp, + message: [{type: 'COMMENT', html: 'Comment 1', text: `Comment ${sequenceNumber}`}], + reportActionID: NumberUtils.rand64(), + }; +} + export { getGlobalFetchMock, signInWithTestUser, setPersonalDetails, + buildPersonalDetails, + buildTestReportComment, }; From f1dc53487835f580eb7e6c1c69a3082c5848e9ce Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 13 Sep 2022 12:08:41 -0400 Subject: [PATCH 35/59] remove unused import --- src/pages/home/report/ReportActionsView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 96808af88280..b60ac8acb975 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -30,7 +30,6 @@ import CopySelectionHelper from '../../../components/CopySelectionHelper'; import EmojiPicker from '../../../components/EmojiPicker/EmojiPicker'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import * as ReportUtils from '../../../libs/ReportUtils'; -import Log from '../../../libs/Log'; const propTypes = { /* Onyx Props */ From 3e90f5e54daca1ca60dd57bcca35624907e2ecc4 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 11:31:18 -0400 Subject: [PATCH 36/59] Make requested changes --- contributingGuides/STYLE.md | 2 +- jest/setup.js | 10 +- src/libs/ReportActionsUtils.js | 8 +- tests/ui/UnreadIndicatorsTest.js | 591 ++++++++++++++++--------------- tests/utils/TestHelper.js | 2 +- 5 files changed, 315 insertions(+), 298 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 62e64bede72c..6402d27e2e85 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -388,7 +388,7 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: -- **Async/Await** - Use the native `Promise` instead +- **Async/Await** - Use the native `Promise` instead. Async/Await is permitted in test files only. - **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead - **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable diff --git a/jest/setup.js b/jest/setup.js index 627d43466b74..d6911480710d 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -26,11 +26,16 @@ jest.mock('@react-native-firebase/crashlytics', () => () => ({ recordError: jest.fn(), })); +// 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). jest.mock('../src/libs/BootSplash', () => ({ hide: jest.fn(), getVisibilityStatus: jest.fn().mockResolvedValue('hidden'), })); +// 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(), })); @@ -48,7 +53,10 @@ function mockImages(imagePath) { }); } -// Mock all images so that Icons and other assets cannot break tests +// We are mock 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'); diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index a86027cb35ef..157891dd8aaa 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -110,7 +110,11 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { const lastMessageIndex = _.findLastIndex(sortedActions, action => ( !isDeletedAction(action) )); - const htmlText = lodashGet(actions, [lastMessageIndex, 'message', 0, 'html'], ''); + if (lastMessageIndex < 0) { + return ''; + } + + const htmlText = lodashGet(sortedActions, [lastMessageIndex, 'message', 0, 'html'], ''); const messageText = parser.htmlToText(htmlText); return ReportUtils.formatReportLastMessageText(messageText); } @@ -140,7 +144,7 @@ function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = return 0; } - return actions[lastMessageIndex].sequenceNumber; + return sortedActions[lastMessageIndex].sequenceNumber; } export { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index d26b6bae09d5..cc0defa51e62 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -20,6 +20,11 @@ import LocalNotification from '../../src/libs/Notification/LocalNotification'; import * as Report from '../../src/libs/actions/Report'; beforeAll(() => { + // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it + // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means + // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling + // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to + // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. global.fetch = TestHelper.getGlobalFetchMock(); // We need a bit more time for this test in some places @@ -70,7 +75,7 @@ function isNewMessagesBadgeVisible(renderedApp) { */ async function navigateToSidebar(renderedApp) { const reportHeader = renderedApp.getByTestId('report-header'); - const reportHeaderBackButton = reportHeader.findByType(ReportHeaderViewBackButton); + const reportHeaderBackButton = await reportHeader.findByType(ReportHeaderViewBackButton); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -80,10 +85,10 @@ async function navigateToSidebar(renderedApp) { * @param {Number} index * @return {Promise} */ -async function navigateToSidebarOption(renderedApp, index) { +function navigateToSidebarOption(renderedApp, index) { const optionRows = renderedApp.getAllByTestId('option-row'); fireEvent(optionRows[index], 'press'); - await waitForPromisesToResolve(); + return waitForPromisesToResolve(); } /** @@ -95,301 +100,301 @@ function isDrawerOpen(renderedApp) { return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; } -test('Unread LHN Status, “New Messages” badge, and New Line Indicator', async () => { - const REPORT_ID = 1; - const USER_A_ACCOUNT_ID = 1; - const USER_A_EMAIL = 'user_a@test.com'; - const USER_B_ACCOUNT_ID = 2; - const USER_B_EMAIL = 'user_b@test.com'; - const USER_C_ACCOUNT_ID = 3; - const USER_C_EMAIL = 'user_c@test.com'; - - // Render the App and sign in as a test user. - const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { +describe('Unread Indicators', () => { + it('Shows correct LHN Status, “New Messages” badge, and New Line Indicators', async () => { + const REPORT_ID = 1; + const USER_A_ACCOUNT_ID = 1; + const USER_A_EMAIL = 'user_a@test.com'; + const USER_B_ACCOUNT_ID = 2; + const USER_B_EMAIL = 'user_b@test.com'; + const USER_C_ACCOUNT_ID = 3; + const USER_C_EMAIL = 'user_c@test.com'; + + // Render the App and sign in as a test user. + const renderedApp = render(); + + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + }); + + const loginForm = renderedApp.queryAllByTestId('login-form'); + expect(loginForm.length).toBe(1); + + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); await waitForPromisesToResolve(); + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.queryAllByTestId('sidebar-links'); + expect(sidebarLinks.length).toBe(1); + + // And verify that the Report screen is rendered after manually setting the sidebar as loaded + // since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); await waitForPromisesToResolve(); - }); - const loginForm = renderedApp.getByTestId('login-form'); - expect(loginForm).toBeTruthy(); + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + let optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + await navigateToSidebarOption(renderedApp, 0); + + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByTestId('report-action-created'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByTestId('report-action-item'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the back button to return to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Navigate to the report again + await navigateToSidebarOption(renderedApp, 0); + + // Verify the unread indicator is no longer present + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + await waitForPromisesToResolve(); - TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - await waitForPromisesToResolve(); + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + // Navigate back to the sidebar + await navigateToSidebar(renderedApp); - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), // Ten minutes ago - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - await waitForPromisesToResolve(); - - // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); - - // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.getByTestId('sidebar-links'); - expect(sidebarLinks).toBeTruthy(); - - // And verify that the Report screen is rendered after manually setting the sidebar as loaded - // since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByTestId('option-row'); - expect(optionRows.length).toBe(1); - - // And that the text is bold - const displayNameText = renderedApp.getByTestId('option-row-display-name'); - expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - await navigateToSidebarOption(renderedApp, 0); - - // Verify that the report screen is rendered and the drawer is closed - expect(isDrawerOpen(renderedApp)).toBe(false); - - // That the report actions are visible along with the created action - const createdAction = renderedApp.getByTestId('report-action-created'); - expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByTestId('report-action-item'); - expect(reportComments.length).toBe(9); - - // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will - // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(2); - - // Scroll up and verify that the "New messages" badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - - // Tap on the back button to return to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the LHN is now open - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Navigate to the report again - await navigateToSidebarOption(renderedApp, 0); - - // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Scroll and verify that the new messages badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - - // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant - const NEW_REPORT_ID = 2; - const NEW_REPORT_CREATED_MOMENT = moment(); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { - reportID: NEW_REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 1, - lastReadSequenceNumber: 0, - lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), - lastMessageText: 'Comment 1', - participants: [USER_C_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: NEW_REPORT_CREATED_MOMENT.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorEmail: USER_C_EMAIL, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - sequenceNumber: 1, - timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: NumberUtils.rand64(), - }, - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), - }); - await waitForPromisesToResolve(); - - // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - - // Navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByTestId('option-row'); - expect(optionRows.length).toBe(2); - - // Verify the text for the new chat is bold and above the previous indicating it has not yet been read - let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - const firstReportOption = displayNameTexts[0]; - expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); - - const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); - - // Tap the new report option and navigate back to the sidebar again via the back button - await navigateToSidebarOption(renderedApp, 0); - - // Verify that all report options appear in a "read" state - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Tap the previous report between User A and User B - await navigateToSidebarOption(renderedApp, 1); - - // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then - // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly - Report.markCommentAsUnread(REPORT_ID, 3); - await waitForPromisesToResolve(); - - // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(3); - - // Scroll up and verify the new messages badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // Navigate to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 1); - await navigateToSidebar(renderedApp); - - // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 1); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // Scroll up and verify the badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Navigate to the LHN - await navigateToSidebar(renderedApp); - - // Simulate another new message on the report with User B - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + // Verify the new report option appears in the LHN + optionRows = renderedApp.getAllByTestId('option-row'); + expect(optionRows.length).toBe(2); + + // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + await navigateToSidebarOption(renderedApp, 0); + + // Verify that all report options appear in a "read" state + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Tap the previous report between User A and User B + await navigateToSidebarOption(renderedApp, 1); + + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + await waitForPromisesToResolve(); + + // Verify the indicator appears above the last action + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + await navigateToSidebar(renderedApp); + + // Verify the report is marked as unread in the sidebar + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebar(renderedApp); + + // Verify the report is now marked as read + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + await navigateToSidebarOption(renderedApp, 1); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Navigate to the LHN + await navigateToSidebar(renderedApp); + + // Simulate another new message on the report with User B + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), + }); + await waitForPromisesToResolve(); + + displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report again and verify the indicator exists + await navigateToSidebarOption(renderedApp, 1); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(0); + + // As the current user add several comments + Report.addComment(REPORT_ID, 'Current User Comment 2'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 3'); + await waitForPromisesToResolve(); + + Report.addComment(REPORT_ID, 'Current User Comment 4'); + await waitForPromisesToResolve(); + + // Mark the last comment as "unread" and verify the unread indicator appears + Report.markCommentAsUnread(REPORT_ID, 14); + await waitForPromisesToResolve(); + unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + expect(unreadIndicator.length).toBe(1); }); - await waitForPromisesToResolve(); - - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the report again and verify the indicator exists - await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - - // Leave a comment as the current user and verify the indicator is removed - Report.addComment(REPORT_ID, 'Current User Comment 1'); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // Mark a previous comment as unread and verify the unread action indicator returns - Report.markCommentAsUnread(REPORT_ID, 9); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); - - // Trigger the app going inactive and active again - AppState.emitCurrentTestState('background'); - AppState.emitCurrentTestState('active'); - - // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(0); - - // As the current user add several comments - Report.addComment(REPORT_ID, 'Current User Comment 2'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 3'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 4'); - await waitForPromisesToResolve(); - - // Mark the last comment as "unread" and verify the unread indicator appears - Report.markCommentAsUnread(REPORT_ID, 14); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); - expect(unreadIndicator.length).toBe(1); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 319732eca13e..eea6ae43461f 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -11,7 +11,7 @@ import * as NumberUtils from '../../src/libs/NumberUtils'; * @param {String} login * @param {Number} accountID * @param {String} [firstName] - * @returns {Promise} + * @returns {Object} */ function buildPersonalDetails(login, accountID, firstName = 'Test') { const avatar = ReportUtils.getDefaultAvatar(login); From 973de5c964596501d6adf4bcf900dbcb699817bb Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:02:02 -0400 Subject: [PATCH 37/59] Remove docs from react-navigation setup and link to react-navigation docs --- jest/setup.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index d6911480710d..5c84427e5205 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -5,16 +5,13 @@ 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/Libraries/Animated/NativeAnimatedHelper'); - 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'); - - // The mock for `call` immediately calls the callback which is incorrect - // So we override it with a no-op Reanimated.default.call = () => {}; return Reanimated; }); From 261c712e1b8118de955bd31e6cf5b070dd285b3d Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:38:59 -0400 Subject: [PATCH 38/59] Clean up all testID and use accessibilityHint or accessibilityLabel instead --- src/components/DisplayNames/index.native.js | 2 +- src/components/OptionRow.js | 5 +- src/components/OptionsList/BaseOptionsList.js | 2 +- src/components/Text.js | 4 -- src/components/UnreadActionIndicator.js | 2 +- src/pages/home/HeaderView.js | 2 +- src/pages/home/ReportHeaderViewBackButton.js | 1 + src/pages/home/ReportScreen.js | 2 +- .../FloatingMessageCounterContainer/index.js | 5 +- .../report/FloatingMessageCounter/index.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- .../home/report/ReportActionItemCreated.js | 2 +- src/pages/home/report/ReportActionsList.js | 3 +- src/pages/home/sidebar/SidebarLinks.js | 3 +- src/pages/signin/LoginForm.js | 2 +- tests/ui/UnreadIndicatorsTest.js | 56 +++++++++---------- 16 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/DisplayNames/index.native.js b/src/components/DisplayNames/index.native.js index 91ed0406a0d7..743a8036b965 100644 --- a/src/components/DisplayNames/index.native.js +++ b/src/components/DisplayNames/index.native.js @@ -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 => ( - + {props.fullTitle} ); diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index 40471941aba4..e408387b1a3f 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -148,7 +148,7 @@ const OptionRow = (props) => { props.isDisabled && styles.cursorDisabled, ]} > - + { } { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index e5524c49dab5..c9f983a9720f 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -166,6 +166,7 @@ class BaseOptionsList extends Component { renderItem({item, index, section}) { return ( ) : null} ( - + {props.translate('common.new')} diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index ea5db9406a7e..6e5b625e7491 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -91,7 +91,7 @@ const HeaderView = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; return ( - + {props.isSmallScreenWidth && ( ( diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4e43bb06956c..7ac2117071c0 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -237,7 +237,7 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js index a367eea5f602..91ad8203ce81 100644 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js +++ b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.js @@ -4,7 +4,10 @@ import styles from '../../../../../styles/styles'; import floatingMessageCounterContainerPropTypes from './floatingMessageCounterContainerPropTypes'; const FloatingMessageCounterContainer = props => ( - + {props.children} ); diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index 8af8266b0a87..9cfb9ba5818f 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -62,7 +62,7 @@ class FloatingMessageCounter extends PureComponent { render() { return ( - + {hovered => ( - + {this.props.shouldDisplayNewIndicator && ( )} diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 10b5fcd31f5e..93bc71a3eab6 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -39,7 +39,7 @@ const ReportActionItemCreated = (props) => { const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies); return ( + - + this.input = el} label={this.props.translate('loginForm.phoneOrEmail')} diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index cc0defa51e62..d13953880d91 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -13,7 +13,6 @@ import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; import * as AppActions from '../../src/libs/actions/App'; -import ReportHeaderViewBackButton from '../../src/pages/home/ReportHeaderViewBackButton'; import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; import * as NumberUtils from '../../src/libs/NumberUtils'; import LocalNotification from '../../src/libs/Notification/LocalNotification'; @@ -37,7 +36,7 @@ beforeAll(() => { * @param {RenderAPI} renderedApp */ function scrollUpToRevealNewMessagesBadge(renderedApp) { - fireEvent.scroll(renderedApp.getByTestId('report-actions-list'), { + fireEvent.scroll(renderedApp.getByA11yLabel('List of chat messages'), { nativeEvent: { contentOffset: { y: 250, @@ -65,7 +64,7 @@ function scrollUpToRevealNewMessagesBadge(renderedApp) { * @return {Boolean} */ function isNewMessagesBadgeVisible(renderedApp) { - const badge = renderedApp.getByTestId('new-messages-badge'); + const badge = renderedApp.getByA11yHint('Scroll to newest messages'); return badge.props.style.transform[0].translateY === 10; } @@ -74,8 +73,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @return {Promise} */ async function navigateToSidebar(renderedApp) { - const reportHeader = renderedApp.getByTestId('report-header'); - const reportHeaderBackButton = await reportHeader.findByType(ReportHeaderViewBackButton); + const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -86,7 +84,7 @@ async function navigateToSidebar(renderedApp) { * @return {Promise} */ function navigateToSidebarOption(renderedApp, index) { - const optionRows = renderedApp.getAllByTestId('option-row'); + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); fireEvent(optionRows[index], 'press'); return waitForPromisesToResolve(); } @@ -96,7 +94,7 @@ function navigateToSidebarOption(renderedApp, index) { * @return {Boolean} */ function isDrawerOpen(renderedApp) { - const reportScreen = renderedApp.getByTestId('report-screen'); + const reportScreen = renderedApp.getByA11yLabel('Main chat area'); return reportScreen.findByType(ReportActionsView).props.isDrawerOpen; } @@ -119,7 +117,7 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); }); - const loginForm = renderedApp.queryAllByTestId('login-form'); + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); expect(loginForm.length).toBe(1); await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); @@ -163,7 +161,7 @@ describe('Unread Indicators', () => { expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.queryAllByTestId('sidebar-links'); + const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); expect(sidebarLinks.length).toBe(1); // And verify that the Report screen is rendered after manually setting the sidebar as loaded @@ -174,11 +172,11 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByTestId('option-row'); + let optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(1); // And that the text is bold - const displayNameText = renderedApp.getByTestId('option-row-display-name'); + const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); await navigateToSidebarOption(renderedApp, 0); @@ -187,14 +185,14 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(false); // That the report actions are visible along with the created action - const createdAction = renderedApp.getByTestId('report-action-created'); + const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByTestId('report-action-item'); + const reportComments = renderedApp.getAllByA11yLabel('Chat message'); expect(reportComments.length).toBe(9); // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); @@ -204,7 +202,7 @@ describe('Unread Indicators', () => { expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByTestId('option-row-display-name'); + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); // Tap on the back button to return to the sidebar @@ -217,7 +215,7 @@ describe('Unread Indicators', () => { await navigateToSidebarOption(renderedApp, 0); // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); expect(isDrawerOpen(renderedApp)).toBe(false); @@ -267,11 +265,11 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByTestId('option-row'); + optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(2); // Verify the text for the new chat is bold and above the previous indicating it has not yet been read - let displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); const firstReportOption = displayNameTexts[0]; expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -285,7 +283,7 @@ describe('Unread Indicators', () => { await navigateToSidebarOption(renderedApp, 0); // Verify that all report options appear in a "read" state - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); @@ -301,7 +299,7 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); @@ -314,7 +312,7 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); @@ -324,7 +322,7 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); @@ -332,7 +330,7 @@ describe('Unread Indicators', () => { // Navigate to the report again and verify the new line indicator is missing await navigateToSidebarOption(renderedApp, 1); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // Scroll up and verify the badge is hidden @@ -349,7 +347,7 @@ describe('Unread Indicators', () => { }); await waitForPromisesToResolve(); - displayNameTexts = renderedApp.queryAllByTestId('option-row-display-name'); + displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); @@ -358,19 +356,19 @@ describe('Unread Indicators', () => { // Navigate to the report again and verify the indicator exists await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Leave a comment as the current user and verify the indicator is removed Report.addComment(REPORT_ID, 'Current User Comment 1'); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Trigger the app going inactive and active again @@ -378,7 +376,7 @@ describe('Unread Indicators', () => { AppState.emitCurrentTestState('active'); // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); // As the current user add several comments @@ -394,7 +392,7 @@ describe('Unread Indicators', () => { // Mark the last comment as "unread" and verify the unread indicator appears Report.markCommentAsUnread(REPORT_ID, 14); await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByTestId('unread-action-indicator'); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); }); }); From ac9fe71febeec7b2e95575a2c0b370d3d9e7de28 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:49:39 -0400 Subject: [PATCH 39/59] Use accessibilityElementsHidden --- src/pages/home/ReportScreen.js | 1 - src/pages/home/sidebar/SidebarLinks.js | 10 +++++++++- tests/ui/UnreadIndicatorsTest.js | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 7ac2117071c0..92b334ebabb6 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -237,7 +237,6 @@ class ReportScreen extends React.Component { /> this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index a3250b6d4f4f..c6c905a2030d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -26,6 +26,8 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import * as App from '../../../libs/actions/App'; import * as ReportUtils from '../../../libs/ReportUtils'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; +import withDrawerState from '../../../components/withDrawerState'; +import withWindowDimensions from '../../../components/withWindowDimensions'; const propTypes = { /** Toggles the navigation menu open and closed */ @@ -253,7 +255,11 @@ class SidebarLinks extends React.Component { }]; return ( - + { From 68fed0c20d29468cf248612bb2f3ef3e7882de19 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 14 Sep 2022 12:54:59 -0400 Subject: [PATCH 40/59] Undo unnecessary refactor of back button --- src/pages/home/HeaderView.js | 14 +++++---- src/pages/home/ReportHeaderViewBackButton.js | 31 -------------------- 2 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 src/pages/home/ReportHeaderViewBackButton.js diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 6e5b625e7491..69a8b524c263 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -26,7 +26,6 @@ import Text from '../../components/Text'; import Tooltip from '../../components/Tooltip'; import variables from '../../styles/variables'; import colors from '../../styles/colors'; -import ReportHeaderViewBackButton from './ReportHeaderViewBackButton'; const propTypes = { /** Toggles the navigationMenu open and closed */ @@ -94,10 +93,15 @@ const HeaderView = (props) => { {props.isSmallScreenWidth && ( - + + + + + )} {Boolean(props.report && title) && ( ( - - - - - -); - -ReportHeaderViewBackButton.displayName = 'ReportHeaderViewBackButton'; -ReportHeaderViewBackButton.propTypes = propTypes; -export default ReportHeaderViewBackButton; From 07df0b275e542ad4ff1044c0f757bd1ccea40258 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 14 Sep 2022 23:42:00 +0530 Subject: [PATCH 41/59] Address review - Fix wrong code --- src/libs/actions/Session/index.js | 2 +- src/pages/signin/PasswordForm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index ec2661a13a1b..3ee2a8afc3aa 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -348,7 +348,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds]: response.message}}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: response.message}}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index c70f25633121..fd6400031f74 100755 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -178,7 +178,7 @@ class PasswordForm extends React.Component { )} - {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.error) && ( + {!this.state.formError && this.props.account && !_.isEmpty(this.props.account.errors) && ( {ErrorUtils.getLatestErrorMessage(this.props.account)} From a8bc73441f747f054bab73309777d5639295cffc Mon Sep 17 00:00:00 2001 From: Maria D'Costa Date: Thu, 15 Sep 2022 15:09:53 +0100 Subject: [PATCH 42/59] Update param type --- src/libs/actions/Wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index a4e75f49d010..262eee68e9f9 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -529,7 +529,7 @@ function updateCurrentStep(currentStep) { } /** - * @param {Object} answers + * @param {Array} answers * @param {String} idNumber */ function answerQuestionsForWallet(answers, idNumber) { From efb1d62e1874e77e4deb6bad46a449e343a1f934 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 13:23:50 -0400 Subject: [PATCH 43/59] Remove jest.setTimeout() by breaking test up into smaller chunks --- tests/ui/UnreadIndicatorsTest.js | 280 +++++++++++++++++-------------- 1 file changed, 152 insertions(+), 128 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 32013bcb1b38..b05d2b77ff14 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -13,7 +13,6 @@ import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; import * as AppActions from '../../src/libs/actions/App'; -import ReportActionsView from '../../src/pages/home/report/ReportActionsView'; import * as NumberUtils from '../../src/libs/NumberUtils'; import LocalNotification from '../../src/libs/Notification/LocalNotification'; import * as Report from '../../src/libs/actions/Report'; @@ -27,7 +26,6 @@ beforeAll(() => { global.fetch = TestHelper.getGlobalFetchMock(); // We need a bit more time for this test in some places - jest.setTimeout(30000); Linking.setInitialURL('https://new.expensify.com/r/1'); appSetup(); }); @@ -98,64 +96,77 @@ function isDrawerOpen(renderedApp) { return !lodashGet(sidebarLinks, [0, 'props', 'accessibilityElementsHidden']); } -describe('Unread Indicators', () => { - it('Shows correct LHN Status, “New Messages” badge, and New Line Indicators', async () => { - const REPORT_ID = 1; - const USER_A_ACCOUNT_ID = 1; - const USER_A_EMAIL = 'user_a@test.com'; - const USER_B_ACCOUNT_ID = 2; - const USER_B_EMAIL = 'user_b@test.com'; - const USER_C_ACCOUNT_ID = 3; - const USER_C_EMAIL = 'user_c@test.com'; - - // Render the App and sign in as a test user. - const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { - await waitForPromisesToResolve(); - }); +const REPORT_ID = 1; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; +const USER_C_ACCOUNT_ID = 3; +const USER_C_EMAIL = 'user_c@test.com'; - const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); +/** + * @returns {RenderAPI} + */ +async function signInAndGetAppWithUnreadChat() { + // Render the App and sign in as a test user. + const renderedApp = render(); - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some + // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + await act(async () => { + await waitForPromisesToResolve(); + }); - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); + expect(loginForm.length).toBe(1); - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - await waitForPromisesToResolve(); + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + await waitForPromisesToResolve(); + return renderedApp; +} + +describe('Unread Indicators', () => { + afterEach(Onyx.clear); + + it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); // Verify no notifications are created for these older messages expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); @@ -163,16 +174,10 @@ describe('Unread Indicators', () => { // Verify the sidebar links are rendered const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); expect(sidebarLinks.length).toBe(1); - - // And verify that the Report screen is rendered after manually setting the sidebar as loaded - // since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - let optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(1); // And that the text is bold @@ -192,36 +197,50 @@ describe('Unread Indicators', () => { // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); - let sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); // Scroll up and verify that the "New messages" badge appears scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + }); - // And that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + it('Clear the new line indicator and bold when we navigate away from a chat that is now read', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Navigate to the unread chat from the sidebar + await navigateToSidebarOption(renderedApp, 0); + expect(isDrawerOpen(renderedApp)).toBe(false); - // Tap on the back button to return to the sidebar + // Then navigate back to the sidebar await navigateToSidebar(renderedApp); // Verify the LHN is now open expect(isDrawerOpen(renderedApp)).toBe(true); - // Navigate to the report again + // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the chat again await navigateToSidebarOption(renderedApp, 0); - // Verify the unread indicator is no longer present - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + // Verify the unread indicator is not present + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); expect(isDrawerOpen(renderedApp)).toBe(false); - // Scroll and verify that the new messages badge is hidden + // Scroll and verify that the new messages badge is also hidden scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); + + it('Shows a browser notification and bold text when a new message arrives for a chat that is read', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Read the chat by navigating to it then return to the LHN // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant const NEW_REPORT_ID = 2; @@ -261,14 +280,14 @@ describe('Unread Indicators', () => { // Verify notification was created as the new message that has arrived is very recent expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - // Navigate back to the sidebar + // // Navigate back to the sidebar await navigateToSidebar(renderedApp); - // Verify the new report option appears in the LHN - optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + // // Verify the new report option appears in the LHN + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); expect(optionRows.length).toBe(2); - // Verify the text for the new chat is bold and above the previous indicating it has not yet been read + // Verify the text for both chats are bold indicating that nothing has not yet been read let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); const firstReportOption = displayNameTexts[0]; @@ -276,22 +295,26 @@ describe('Unread Indicators', () => { expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); // Tap the new report option and navigate back to the sidebar again via the back button await navigateToSidebarOption(renderedApp, 0); - // Verify that all report options appear in a "read" state + // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); expect(displayNameTexts.length).toBe(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + }); - // Tap the previous report between User A and User B - await navigateToSidebarOption(renderedApp, 1); + it('Manually marking a chat message as read shows the new line indicator and updates the LHN', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Navigate to the unread report + await navigateToSidebarOption(renderedApp, 0); // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly @@ -299,9 +322,9 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); // Verify the indicator appears above the last action - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); - sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); // Scroll up and verify the new messages badge appears @@ -312,51 +335,45 @@ describe('Unread Indicators', () => { await navigateToSidebar(renderedApp); // Verify the report is marked as unread in the sidebar - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 1); + await navigateToSidebarOption(renderedApp, 0); await navigateToSidebar(renderedApp); // Verify the report is now marked as read displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 1); - await waitForPromisesToResolve(); + await navigateToSidebarOption(renderedApp, 0); unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); - // Scroll up and verify the badge is hidden + // Scroll up and verify the "New messages" badge is hidden scrollUpToRevealNewMessagesBadge(renderedApp); expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); - expect(isDrawerOpen(renderedApp)).toBe(false); + }); - // Navigate to the LHN - await navigateToSidebar(renderedApp); + it('Removes the new line indicator when a new message is created by the current user', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); - // Simulate another new message on the report with User B - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 10: TestHelper.buildTestReportComment(USER_B_EMAIL, 10, moment().unix()), - }); - await waitForPromisesToResolve(); + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - // Navigate to the report again and verify the indicator exists - await navigateToSidebarOption(renderedApp, 1); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + // Navigate to the report and verify the indicator is present + await navigateToSidebarOption(renderedApp, 0); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(1); // Leave a comment as the current user and verify the indicator is removed @@ -364,6 +381,29 @@ describe('Unread Indicators', () => { await waitForPromisesToResolve(); unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); + }); + + it('Clears the new line indicator when the user moves the App to the background', async () => { + const renderedApp = await signInAndGetAppWithUnreadChat(); + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the chat and verify the new line indicator is present + await navigateToSidebarOption(renderedApp, 0); + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared + await navigateToSidebar(renderedApp); + await navigateToSidebarOption(renderedApp, 0); + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); @@ -378,21 +418,5 @@ describe('Unread Indicators', () => { // Verify the new line is cleared unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); expect(unreadIndicator.length).toBe(0); - - // As the current user add several comments - Report.addComment(REPORT_ID, 'Current User Comment 2'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 3'); - await waitForPromisesToResolve(); - - Report.addComment(REPORT_ID, 'Current User Comment 4'); - await waitForPromisesToResolve(); - - // Mark the last comment as "unread" and verify the unread indicator appears - Report.markCommentAsUnread(REPORT_ID, 14); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); }); }); From 18367421f260003630e554647d02481d021f7415 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 13:56:52 -0400 Subject: [PATCH 44/59] Extract the one part of tests that actually does need async/await to work. Use regular promises in the actual tests themselves --- contributingGuides/STYLE.md | 2 +- tests/ui/UnreadIndicatorsTest.js | 639 ++++++++++-------- .../utils/waitForPromisesToResolveWithAct.js | 21 + 3 files changed, 364 insertions(+), 298 deletions(-) create mode 100644 tests/utils/waitForPromisesToResolveWithAct.js diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 6402d27e2e85..62e64bede72c 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -388,7 +388,7 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: -- **Async/Await** - Use the native `Promise` instead. Async/Await is permitted in test files only. +- **Async/Await** - Use the native `Promise` instead - **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead - **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index b05d2b77ff14..45172e574f40 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -1,14 +1,14 @@ -/* eslint-disable @lwc/lwc/no-async-await */ import React from 'react'; import Onyx from 'react-native-onyx'; import {Linking, AppState} from 'react-native'; -import {fireEvent, render, act} from '@testing-library/react-native'; +import {fireEvent, render} from '@testing-library/react-native'; import lodashGet from 'lodash/get'; import moment from 'moment'; import App from '../../src/App'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import waitForPromisesToResolveWithAct from '../utils/waitForPromisesToResolveWithAct'; import * as TestHelper from '../utils/TestHelper'; import appSetup from '../../src/setup'; import fontWeightBold from '../../src/styles/fontWeight/bold'; @@ -70,7 +70,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @param {RenderAPI} renderedApp * @return {Promise} */ -async function navigateToSidebar(renderedApp) { +function navigateToSidebar(renderedApp) { const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); @@ -105,318 +105,363 @@ const USER_C_ACCOUNT_ID = 3; const USER_C_EMAIL = 'user_c@test.com'; /** + * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. + * * @returns {RenderAPI} */ -async function signInAndGetAppWithUnreadChat() { +function signInAndGetAppWithUnreadChat() { // Render the App and sign in as a test user. const renderedApp = render(); - - // Note: act() is necessary since react-navigation's NavigationContainer has an internal state update that will throw some - // warnings related to async code. See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act - await act(async () => { - await waitForPromisesToResolve(); - }); - - const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); - - await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - - const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); - - // Simulate setting an unread report and personal details - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 9, - lastReadSequenceNumber: 1, - lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), - lastMessageText: 'Test', - participants: [USER_B_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: MOMENT_TEN_MINUTES_AGO.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), - 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), - 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), - 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), - 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), - 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), - 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), - 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), - 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), - }); - - // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); - await waitForPromisesToResolve(); - return renderedApp; + return waitForPromisesToResolveWithAct() + .then(() => { + const loginForm = renderedApp.queryAllByA11yLabel('Login form'); + expect(loginForm.length).toBe(1); + + return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }) + .then(() => { + const MOMENT_TEN_MINUTES_AGO = moment().subtract(10, 'minutes'); + + // Simulate setting an unread report and personal details + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 9, + lastReadSequenceNumber: 1, + lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc(), + lastMessageText: 'Test', + participants: [USER_B_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: MOMENT_TEN_MINUTES_AGO.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: TestHelper.buildTestReportComment(USER_B_EMAIL, 1, MOMENT_TEN_MINUTES_AGO.add(10, 'seconds').unix()), + 2: TestHelper.buildTestReportComment(USER_B_EMAIL, 2, MOMENT_TEN_MINUTES_AGO.add(20, 'seconds').unix()), + 3: TestHelper.buildTestReportComment(USER_B_EMAIL, 3, MOMENT_TEN_MINUTES_AGO.add(30, 'seconds').unix()), + 4: TestHelper.buildTestReportComment(USER_B_EMAIL, 4, MOMENT_TEN_MINUTES_AGO.add(40, 'seconds').unix()), + 5: TestHelper.buildTestReportComment(USER_B_EMAIL, 5, MOMENT_TEN_MINUTES_AGO.add(50, 'seconds').unix()), + 6: TestHelper.buildTestReportComment(USER_B_EMAIL, 6, MOMENT_TEN_MINUTES_AGO.add(60, 'seconds').unix()), + 7: TestHelper.buildTestReportComment(USER_B_EMAIL, 7, MOMENT_TEN_MINUTES_AGO.add(70, 'seconds').unix()), + 8: TestHelper.buildTestReportComment(USER_B_EMAIL, 8, MOMENT_TEN_MINUTES_AGO.add(80, 'seconds').unix()), + 9: TestHelper.buildTestReportComment(USER_B_EMAIL, 9, MOMENT_TEN_MINUTES_AGO.add(90, 'seconds').unix()), + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_B_EMAIL]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(true); + return waitForPromisesToResolve(); + }) + .then(() => renderedApp); } describe('Unread Indicators', () => { afterEach(Onyx.clear); - it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); - - // Verify the sidebar links are rendered - const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); - expect(sidebarLinks.length).toBe(1); - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify there is only one option in the sidebar - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(1); - - // And that the text is bold - const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - await navigateToSidebarOption(renderedApp, 0); - - // Verify that the report screen is rendered and the drawer is closed - expect(isDrawerOpen(renderedApp)).toBe(false); - - // That the report actions are visible along with the created action - const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); - expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByA11yLabel('Chat message'); - expect(reportComments.length).toBe(9); - - // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will - // have a sequenceNumber of 2 - const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(2); - - // Scroll up and verify that the "New messages" badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify no notifications are created for these older messages + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + + // Verify the sidebar links are rendered + const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); + expect(sidebarLinks.length).toBe(1); + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify there is only one option in the sidebar + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + expect(optionRows.length).toBe(1); + + // And that the text is bold + const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify that the report screen is rendered and the drawer is closed + expect(isDrawerOpen(renderedApp)).toBe(false); + + // That the report actions are visible along with the created action + const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); + expect(createdAction).toBeTruthy(); + const reportComments = renderedApp.getAllByA11yLabel('Chat message'); + expect(reportComments.length).toBe(9); + + // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will + // have a sequenceNumber of 2 + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(2); + + // Scroll up and verify that the "New messages" badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + }); }); - it('Clear the new line indicator and bold when we navigate away from a chat that is now read', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Navigate to the unread chat from the sidebar - await navigateToSidebarOption(renderedApp, 0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Then navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the LHN is now open - expect(isDrawerOpen(renderedApp)).toBe(true); - - // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); - expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - - // Tap on the chat again - await navigateToSidebarOption(renderedApp, 0); - - // Verify the unread indicator is not present - const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - expect(isDrawerOpen(renderedApp)).toBe(false); - - // Scroll and verify that the new messages badge is also hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + it('Clear the new line indicator and bold when we navigate away from a chat that is now read', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the unread chat from the sidebar + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Then navigate back to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // Verify the LHN is now open + expect(isDrawerOpen(renderedApp)).toBe(true); + + // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) + const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + + // Tap on the chat again + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify the unread indicator is not present + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + expect(isDrawerOpen(renderedApp)).toBe(false); + + // Scroll and verify that the new messages badge is also hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); }); - it('Shows a browser notification and bold text when a new message arrives for a chat that is read', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Read the chat by navigating to it then return to the LHN - - // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant - const NEW_REPORT_ID = 2; - const NEW_REPORT_CREATED_MOMENT = moment(); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { - reportID: NEW_REPORT_ID, - reportName: 'Chat Report', - maxSequenceNumber: 1, - lastReadSequenceNumber: 0, - lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), - lastMessageText: 'Comment 1', - participants: [USER_C_EMAIL], - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { - 0: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - sequenceNumber: 0, - timestamp: NEW_REPORT_CREATED_MOMENT.unix(), - reportActionID: NumberUtils.rand64(), - }, - 1: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorEmail: USER_C_EMAIL, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - sequenceNumber: 1, - timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: NumberUtils.rand64(), - }, - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { - [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), - }); - await waitForPromisesToResolve(); - - // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); - - // // Navigate back to the sidebar - await navigateToSidebar(renderedApp); - - // // Verify the new report option appears in the LHN - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(2); - - // Verify the text for both chats are bold indicating that nothing has not yet been read - let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - const firstReportOption = displayNameTexts[0]; - expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); - - const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); - - // Tap the new report option and navigate back to the sidebar again via the back button - await navigateToSidebarOption(renderedApp, 0); - - // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + it('Shows a browser notification and bold text when a new message arrives for a chat that is read', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Simulate a new report arriving via Pusher along with reportActions and personalDetails for the other participant + const NEW_REPORT_ID = 2; + const NEW_REPORT_CREATED_MOMENT = moment(); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, { + reportID: NEW_REPORT_ID, + reportName: 'Chat Report', + maxSequenceNumber: 1, + lastReadSequenceNumber: 0, + lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc(), + lastMessageText: 'Comment 1', + participants: [USER_C_EMAIL], + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, { + 0: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + sequenceNumber: 0, + timestamp: NEW_REPORT_CREATED_MOMENT.unix(), + reportActionID: NumberUtils.rand64(), + }, + 1: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorEmail: USER_C_EMAIL, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + sequenceNumber: 1, + timestamp: NEW_REPORT_CREATED_MOMENT.add(5, 'seconds').unix(), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: NumberUtils.rand64(), + }, + }); + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, { + [USER_C_EMAIL]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify notification was created as the new message that has arrived is very recent + expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + + // // Navigate back to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // // Verify the new report option appears in the LHN + const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + expect(optionRows.length).toBe(2); + + // Verify the text for both chats are bold indicating that nothing has not yet been read + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(2); + const firstReportOption = displayNameTexts[0]; + expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); + + const secondReportOption = displayNameTexts[1]; + expect(lodashGet(secondReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(secondReportOption, ['props', 'children'])).toBe('B User'); + + // Tap the new report option and navigate back to the sidebar again via the back button + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(2); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); + expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[1], ['props', 'children'])).toBe('B User'); + }); }); - it('Manually marking a chat message as read shows the new line indicator and updates the LHN', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Navigate to the unread report - await navigateToSidebarOption(renderedApp, 0); - - // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then - // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly - Report.markCommentAsUnread(REPORT_ID, 3); - await waitForPromisesToResolve(); - - // Verify the indicator appears above the last action - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); - expect(sequenceNumber).toBe(3); - - // Scroll up and verify the new messages badge appears - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); - - // Navigate to the sidebar - await navigateToSidebar(renderedApp); - - // Verify the report is marked as unread in the sidebar - let displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and back to the sidebar - await navigateToSidebarOption(renderedApp, 0); - await navigateToSidebar(renderedApp); - - // Verify the report is now marked as read - displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - - // Navigate to the report again and verify the new line indicator is missing - await navigateToSidebarOption(renderedApp, 0); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - - // Scroll up and verify the "New messages" badge is hidden - scrollUpToRevealNewMessagesBadge(renderedApp); - expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + it('Manually marking a chat message as read shows the new line indicator and updates the LHN', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the unread report + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // It's difficult to trigger marking a report comment as unread since we would have to mock the long press event and then + // another press on the context menu item so we will do it via the action directly and then test if the UI has updated properly + Report.markCommentAsUnread(REPORT_ID, 3); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify the indicator appears above the last action + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); + expect(sequenceNumber).toBe(3); + + // Scroll up and verify the new messages badge appears + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(true); + + // Navigate to the sidebar + return navigateToSidebar(renderedApp); + }) + .then(() => { + // Verify the report is marked as unread in the sidebar + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and back to the sidebar + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => navigateToSidebar(renderedApp)) + .then(() => { + // Verify the report is now marked as read + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + + // Navigate to the report again and verify the new line indicator is missing + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + + // Scroll up and verify the "New messages" badge is hidden + scrollUpToRevealNewMessagesBadge(renderedApp); + expect(isNewMessagesBadgeVisible(renderedApp)).toBe(false); + }); }); - it('Removes the new line indicator when a new message is created by the current user', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(isDrawerOpen(renderedApp)).toBe(true); - - const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the report and verify the indicator is present - await navigateToSidebarOption(renderedApp, 0); - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Leave a comment as the current user and verify the indicator is removed - Report.addComment(REPORT_ID, 'Current User Comment 1'); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + it('Removes the new line indicator when a new message is created by the current user', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the report and verify the indicator is present + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Leave a comment as the current user and verify the indicator is removed + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForPromisesToResolve(); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + }); }); - it('Clears the new line indicator when the user moves the App to the background', async () => { - const renderedApp = await signInAndGetAppWithUnreadChat(); - - // Verify we are on the LHN and that the chat shows as unread in the LHN - expect(isDrawerOpen(renderedApp)).toBe(true); - - const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); - expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); - - // Navigate to the chat and verify the new line indicator is present - await navigateToSidebarOption(renderedApp, 0); - let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared - await navigateToSidebar(renderedApp); - await navigateToSidebarOption(renderedApp, 0); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); - - // Mark a previous comment as unread and verify the unread action indicator returns - Report.markCommentAsUnread(REPORT_ID, 9); - await waitForPromisesToResolve(); - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); - - // Trigger the app going inactive and active again - AppState.emitCurrentTestState('background'); - AppState.emitCurrentTestState('active'); - - // Verify the new line is cleared - unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + it('Clears the new line indicator when the user moves the App to the background', () => { + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Verify we are on the LHN and that the chat shows as unread in the LHN + expect(isDrawerOpen(renderedApp)).toBe(true); + + const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); + expect(displayNameTexts.length).toBe(1); + expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); + expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); + + // Navigate to the chat and verify the new line indicator is present + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared + return navigateToSidebar(renderedApp); + }) + .then(() => navigateToSidebarOption(renderedApp, 0)) + .then(() => { + const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + + // Mark a previous comment as unread and verify the unread action indicator returns + Report.markCommentAsUnread(REPORT_ID, 9); + return waitForPromisesToResolve(); + }) + .then(() => { + let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(1); + + // Trigger the app going inactive and active again + AppState.emitCurrentTestState('background'); + AppState.emitCurrentTestState('active'); + + // Verify the new line is cleared + unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); + expect(unreadIndicator.length).toBe(0); + }); }); }); diff --git a/tests/utils/waitForPromisesToResolveWithAct.js b/tests/utils/waitForPromisesToResolveWithAct.js new file mode 100644 index 000000000000..37d5ccb041ce --- /dev/null +++ b/tests/utils/waitForPromisesToResolveWithAct.js @@ -0,0 +1,21 @@ +import {act} from '@testing-library/react-native'; +import waitForPromisesToResolve from './waitForPromisesToResolve'; + +/** + * This method is necessary because react-navigation's NavigationContainer makes an internal state update when parsing the + * linkingConfig to get the initialState. This throws some warnings related to async code if we do not wrap this call in an act(). + * + * See: https://callstack.github.io/react-native-testing-library/docs/understanding-act/#asynchronous-act + * + * This apparently will not work unless we use async/await because of some kind of voodoo magic inside the react-test-renderer + * so we are suppressing our lint rule and avoiding async/await everywhere else in our tests. + * + * @returns {Promise} + */ +// eslint-disable-next-line @lwc/lwc/no-async-await +export default async function waitForPromisesToResolveWithAct() { + // eslint-disable-next-line @lwc/lwc/no-async-await + await act(async () => { + await waitForPromisesToResolve(); + }); +} From c350685dfb652e51827207874a9e7175265ffe83 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 14:02:27 -0400 Subject: [PATCH 45/59] Cleanup Expensicons mock --- jest/setup.js | 93 +++------------------------------------------------ 1 file changed, 5 insertions(+), 88 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 5c84427e5205..9a0f864ab4b6 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -58,91 +58,8 @@ mockImages('images'); mockImages('images/avatars'); mockImages('images/bankicons'); mockImages('images/product-illustrations'); -jest.mock('../src/components/Icon/Expensicons', () => ({ - ActiveRoomAvatar: () => '', - AdminRoomAvatar: () => '', - Android: () => '', - AnnounceRoomAvatar: () => '', - Apple: () => '', - ArrowRight: () => '', - BackArrow: () => '', - Bank: () => '', - Bill: () => '', - Bolt: () => '', - Briefcase: () => '', - Bug: () => '', - Building: () => '', - Camera: () => '', - Cash: () => '', - ChatBubble: () => '', - Checkmark: () => '', - CircleHourglass: () => '', - Clipboard: () => '', - Close: () => '', - ClosedSign: () => '', - Collapse: () => '', - Concierge: () => '', - Connect: () => '', - CreditCard: () => '', - DeletedRoomAvatar: () => '', - DomainRoomAvatar: () => '', - DotIndicator: () => '', - DownArrow: () => '', - Download: () => '', - Emoji: () => '', - Exclamation: () => '', - Exit: () => '', - ExpensifyCard: () => '', - Expand: () => '', - Eye: () => '', - EyeDisabled: () => '', - FallbackAvatar: () => '', - FallbackWorkspaceAvatar: () => '', - Gallery: () => '', - Gear: () => '', - Hashtag: () => '', - ImageCropMask: () => '', - Info: () => '', - Invoice: () => '', - Key: () => '', - Keyboard: () => '', - Link: () => '', - LinkCopy: () => '', - Lock: () => '', - Luggage: () => '', - MagnifyingGlass: () => '', - Mail: () => '', - MoneyBag: () => '', - MoneyCircle: () => '', - Monitor: () => '', - NewWindow: () => '', - NewWorkspace: () => '', - Offline: () => '', - OfflineCloud: () => '', - Paperclip: () => '', - PayPal: () => '', - Paycheck: () => '', - Pencil: () => '', - Phone: () => '', - Pin: () => '', - PinCircle: () => '', - Plus: () => '', - Printer: () => '', - Profile: () => '', - QuestionMark: () => '', - Receipt: () => '', - ReceiptSearch: () => '', - Rotate: () => '', - RotateLeft: () => '', - Send: () => '', - Sync: () => '', - ThreeDots: () => '', - Transfer: () => '', - Trashcan: () => '', - UpArrow: () => '', - Upload: () => '', - Users: () => '', - Wallet: () => '', - Workspace: () => '', - Zoom: () => '', -})); +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) => ({...prev, [key]: () => ''}), {}); +}); From f51ed16f150e23fc80a48e40bc59bf7870e03af3 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 14:06:49 -0400 Subject: [PATCH 46/59] Clean up function names --- __mocks__/react-native.js | 6 ++---- src/libs/ReportActionsUtils.js | 4 ++-- src/libs/actions/Report.js | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 3e407252dc78..e2630c1ef632 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -1,13 +1,10 @@ -/* eslint-disable arrow-body-style */ // eslint-disable-next-line no-restricted-imports import * as ReactNative from 'react-native'; import _ from 'underscore'; jest.doMock('react-native', () => { let url = 'https://new.expensify.com/'; - const getInitialURL = () => { - return Promise.resolve(url); - }; + const getInitialURL = () => Promise.resolve(url); let appState = 'active'; let count = 0; @@ -57,6 +54,7 @@ jest.doMock('react-native', () => { ...ReactNative.Dimensions, addEventListener: jest.fn(), get: () => ({ + // Tests will run with the app in a typical small screen size width: 300, height: 700, scale: 1, fontScale: 1, }), }, diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 157891dd8aaa..1b49c722e642 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -126,7 +126,7 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { * @param {Number} lastReadSequenceNumber * @return {String} */ -function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { +function getOptimisticLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { // If the action we are deleting is unread then just return the current last read sequence number if (deletedSequenceNumber > lastReadSequenceNumber) { return lastReadSequenceNumber; @@ -148,7 +148,7 @@ function getNewLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = } export { - getNewLastReadSequenceNumberForDeletedAction, + getOptimisticLastReadSequenceNumberForDeletedAction, getLastVisibleMessageText, getSortedReportActions, getMostRecentIOUReportSequenceNumber, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9d977290278d..4897d56ed02f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1186,7 +1186,7 @@ function deleteReportComment(reportID, reportAction) { // If we are deleting the last visible message, let's find the previous visible one and update the lastMessageText in the LHN. // Similarly, we are deleting the last read comment will want to update the lastReadSequenceNumber to use the previous visible message. const lastMessageText = ReportActionsUtils.getLastVisibleMessageText(reportID, optimisticReportActions); - const lastReadSequenceNumber = ReportActionsUtils.getNewLastReadSequenceNumberForDeletedAction( + const lastReadSequenceNumber = ReportActionsUtils.getOptimisticLastReadSequenceNumberForDeletedAction( reportID, optimisticReportActions, reportAction.sequenceNumber, From 947a4fc6950e5a17bbe14143cdc5eb7b1555a36d Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 22:32:31 +0000 Subject: [PATCH 47/59] Update version to 1.2.0-7 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4f708c805d62..2494c07a08e7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020006 - versionName "1.2.0-6" + versionCode 1001020007 + versionName "1.2.0-7" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index b61ebf609f94..c5ab77c495f8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.6 + 1.2.0.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3ebed1a25b84..4552d4e4c02d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.6 + 1.2.0.7 diff --git a/package-lock.json b/package-lock.json index 0d14d4ef4171..54086977ed84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7b4887d093e9..cfbca0601256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-6", + "version": "1.2.0-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 193b8a342927c62d7cfc9b13afea66acd1400c9f Mon Sep 17 00:00:00 2001 From: Francois Laithier Date: Thu, 15 Sep 2022 15:55:54 -0700 Subject: [PATCH 48/59] Only use personal detail login as `alternateText` if we're not making the option for a report --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index cc8a2b38b994..9f86d55add8f 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -329,6 +329,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { } } else { result.keyForList = personalDetail.login; + result.alternateText = Str.removeSMSDomain(personalDetail.login); } if (result.hasOutstandingIOU) { @@ -343,7 +344,6 @@ function createOption(logins, personalDetails, report, reportActions = {}, { result.login = personalDetail.login; result.phoneNumber = personalDetail.phoneNumber; result.payPalMeAddress = personalDetail.payPalMeAddress; - result.alternateText = Str.removeSMSDomain(personalDetail.login); } const reportName = ReportUtils.getReportName(report, personalDetailMap, policies); From e1efe56208b5185cd9f97d12ace1ba872f149f24 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 15 Sep 2022 23:19:28 +0000 Subject: [PATCH 49/59] Update version to 1.2.0-8 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2494c07a08e7..77885e4bc00e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020007 - versionName "1.2.0-7" + versionCode 1001020008 + versionName "1.2.0-8" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c5ab77c495f8..9ab467740b11 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.7 + 1.2.0.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4552d4e4c02d..92ecd597d886 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.2.0.7 + 1.2.0.8 diff --git a/package-lock.json b/package-lock.json index 54086977ed84..150485a684ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cfbca0601256..cc68c11ce720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-7", + "version": "1.2.0-8", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 8abe3dd36d813505185ed71ee777adde4924f695 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 16 Sep 2022 01:28:51 +0000 Subject: [PATCH 50/59] Update version to 1.2.1-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 77885e4bc00e..4ee66d3fb79d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001020008 - versionName "1.2.0-8" + versionCode 1001020100 + versionName "1.2.1-0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9ab467740b11..99e91a3df77c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.0 + 1.2.1 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.0.8 + 1.2.1.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 92ecd597d886..272bb944f4ff 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.0 + 1.2.1 CFBundleSignature ???? CFBundleVersion - 1.2.0.8 + 1.2.1.0 diff --git a/package-lock.json b/package-lock.json index 150485a684ea..50f91287d1fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cc68c11ce720..fb1a983ae489 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.0-8", + "version": "1.2.1-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From ff196b198e5567b4e65102e80f1b0ebe2df5304b Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 23:48:25 -0400 Subject: [PATCH 51/59] Add test for deleting comment --- src/components/OptionRow.js | 1 + src/components/OptionsList/BaseOptionsList.js | 1 + src/pages/home/sidebar/SidebarLinks.js | 1 + tests/ui/UnreadIndicatorsTest.js | 54 +++++++++++++++++++ tests/utils/TestHelper.js | 2 +- 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index e408387b1a3f..6ac45e98709d 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -196,6 +196,7 @@ const OptionRow = (props) => { /> {props.option.alternateText ? ( diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index c9f983a9720f..c8337b87acab 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -166,6 +166,7 @@ class BaseOptionsList extends Component { renderItem({item, index, section}) { return ( { // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it @@ -464,4 +465,57 @@ describe('Unread Indicators', () => { expect(unreadIndicator.length).toBe(0); }); }); + + it('Displays the correct chat message preview in the LHN when a comment is added then deleted', () => { + let reportActions; + let lastReportAction; + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: val => reportActions = val, + }); + let renderedApp; + return signInAndGetAppWithUnreadChat() + .then((testInstance) => { + renderedApp = testInstance; + + // Navigate to the chat and simulate leaving a comment from the current user + return navigateToSidebarOption(renderedApp, 0); + }) + .then(() => { + // Leave a comment as the current user + Report.addComment(REPORT_ID, 'Current User Comment 1'); + return waitForPromisesToResolve(); + }) + .then(() => { + // Simulate the response from the server so that the comment can be deleted in this test + lastReportAction = {...CollectionUtils.lastItem(reportActions)}; + delete lastReportAction[lastReportAction.clientID]; + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + lastMessageText: lastReportAction.message[0].text, + lastMessageTimestamp: lastReportAction.timestamp, + lastActorEmail: lastReportAction.actorEmail, + maxSequenceNumber: lastReportAction.sequenceNumber, + reportID: REPORT_ID, + }); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [lastReportAction.clientID]: null, + 10: lastReportAction, + }); + return waitForPromisesToResolve(); + }) + .then(() => { + // Verify the chat preview text matches the last comment from the current user + const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); + expect(alternateText.length).toBe(1); + expect(alternateText[0].props.children).toBe('Current User Comment 1'); + + Report.deleteReportComment(REPORT_ID, lastReportAction); + return waitForPromisesToResolve(); + }) + .then(() => { + const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); + expect(alternateText.length).toBe(1); + expect(alternateText[0].props.children).toBe('Comment 9'); + }); + }); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index eea6ae43461f..032f97fe2b1e 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -137,7 +137,7 @@ function buildTestReportComment(actorEmail, sequenceNumber, timestamp) { person: [{type: 'TEXT', style: 'strong', text: 'User B'}], sequenceNumber, timestamp, - message: [{type: 'COMMENT', html: 'Comment 1', text: `Comment ${sequenceNumber}`}], + message: [{type: 'COMMENT', html: `Comment ${sequenceNumber}`, text: `Comment ${sequenceNumber}`}], reportActionID: NumberUtils.rand64(), }; } From 7b76284a011aa08a4ad24aab7b867eaed725b395 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 18:34:02 -1000 Subject: [PATCH 52/59] improve a couple of comments and fix some incorrect changes --- jest/setup.js | 2 +- src/libs/actions/Report.js | 4 ++-- src/pages/home/HeaderView.js | 2 +- src/pages/home/report/FloatingMessageCounter/index.js | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 9a0f864ab4b6..9016683290d1 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -50,7 +50,7 @@ function mockImages(imagePath) { }); } -// We are mock all images so that Icons and other assets cannot break tests. In the testing environment, importing things like .svg +// 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. diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 4897d56ed02f..82a23a115fd0 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -11,6 +11,7 @@ import PushNotification from '../Notification/PushNotification'; import * as PersonalDetails from './PersonalDetails'; import Navigation from '../Navigation/Navigation'; import * as ActiveClientManager from '../ActiveClientManager'; +import Visibility from '../Visibility'; import ROUTES from '../../ROUTES'; import Timing from './Timing'; import * as DeprecatedAPI from '../deprecatedAPI'; @@ -25,7 +26,6 @@ import * as Localize from '../Localize'; import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as NumberUtils from '../NumberUtils'; -import Visibility from '../Visibility'; let currentUserEmail; let currentUserAccountID; @@ -805,7 +805,7 @@ function addActions(reportID, text = '', file) { const actionCount = text && file ? 2 : 1; const newSequenceNumber = highestSequenceNumber + actionCount; - // We guess at what these sequenceNumbers are to enable marking as unread while offline + // We're giving our best guess at what these sequenceNumbers are to enable marking as unread while offline. if (text && file) { reportCommentAction.sequenceNumber = highestSequenceNumber + 1; attachmentAction.sequenceNumber = highestSequenceNumber + 2; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 69a8b524c263..967523fe235a 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -93,7 +93,7 @@ const HeaderView = (props) => { {props.isSmallScreenWidth && ( - + + Date: Thu, 15 Sep 2022 19:43:40 -1000 Subject: [PATCH 53/59] Must mock useDrawerStatus in LHNOrderTest --- tests/unit/LHNOrderTest.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/LHNOrderTest.js b/tests/unit/LHNOrderTest.js index 68944b326a9d..5fbc395b6f93 100644 --- a/tests/unit/LHNOrderTest.js +++ b/tests/unit/LHNOrderTest.js @@ -198,6 +198,16 @@ function getDefaultRenderedSidebarLinks() { )); } +jest.mock('@react-navigation/drawer', () => { + const drawer = jest.requireActual('@react-navigation/drawer'); + return { + ...drawer, + useDrawerStatus() { + return 'open'; + }, + }; +}); + describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate From b6d9615d0d25d26addef67ab6a89c709d3c2c117 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 19:58:59 -1000 Subject: [PATCH 54/59] Fix icons and withDrawerState for LHNOrderTest --- jest/setup.js | 9 ++++++++- src/pages/home/sidebar/SidebarLinks.js | 2 -- .../home/sidebar/SidebarScreen/BaseSidebarScreen.js | 4 +++- tests/unit/LHNOrderTest.js | 10 ---------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 9016683290d1..07609b943522 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -61,5 +61,12 @@ 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) => ({...prev, [key]: () => ''}), {}); + return reduce(Expensicons, (prev, _curr, key) => { + const fn = () => ''; + Object.defineProperty(fn, 'name', { + value: key, + configurable: true, + }); + return {...prev, [key]: fn}; + }, {}); }); diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 563f4ea8ca7c..98ef2aa5f97d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -25,7 +25,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import * as App from '../../../libs/actions/App'; import * as ReportUtils from '../../../libs/ReportUtils'; import withCurrentUserPersonalDetails from '../../../components/withCurrentUserPersonalDetails'; -import withDrawerState from '../../../components/withDrawerState'; import withWindowDimensions from '../../../components/withWindowDimensions'; import Timing from '../../../libs/actions/Timing'; import reportActionPropTypes from '../report/reportActionPropTypes'; @@ -222,7 +221,6 @@ SidebarLinks.defaultProps = defaultProps; export default compose( withLocalize, withCurrentUserPersonalDetails, - withDrawerState, withWindowDimensions, withOnyx({ reports: { diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 42eac4ea43c2..ff3b97c3f778 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -18,6 +18,7 @@ import * as Policy from '../../../../libs/actions/Policy'; import Performance from '../../../../libs/Performance'; import * as Welcome from '../../../../libs/actions/Welcome'; import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; +import withDrawerState from '../../../../components/withDrawerState'; const propTypes = { @@ -110,6 +111,7 @@ class BaseSidebarScreen extends Component { insets={insets} onAvatarClick={this.navigateToSettings} isSmallScreenWidth={this.props.isSmallScreenWidth} + isDrawerOpen={this.props.isDrawerOpen} /> { - const drawer = jest.requireActual('@react-navigation/drawer'); - return { - ...drawer, - useDrawerStatus() { - return 'open'; - }, - }; -}); - describe('Sidebar', () => { describe('in default mode', () => { // Clear out Onyx after each test so that each test starts with a clean slate From b3ab1ba3c7c0a99d9604f1df0562f7684f468222 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 15 Sep 2022 20:03:49 -1000 Subject: [PATCH 55/59] improve comment and add explanation for icon name setting --- jest/setup.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 07609b943522..722e67007f13 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -62,11 +62,10 @@ 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, - configurable: true, - }); + Object.defineProperty(fn, 'name', {value: key}); return {...prev, [key]: fn}; }, {}); }); From 6b8536a5eea2a474e880dc0f99258cc8bd22db08 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 16 Sep 2022 10:38:37 -1000 Subject: [PATCH 56/59] Make some requested changes and improve comments further --- .../@react-native-firebase/crashlytics.js | 7 +- __mocks__/react-native.js | 16 +++-- jest/setup.js | 7 -- src/CONST.js | 7 ++ src/libs/ReportActionsUtils.js | 2 +- tests/ui/UnreadIndicatorsTest.js | 72 +++++++++---------- .../utils/waitForPromisesToResolveWithAct.js | 12 ++++ 7 files changed, 73 insertions(+), 50 deletions(-) diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js index ff8b4c56321a..0cd842b4765d 100644 --- a/__mocks__/@react-native-firebase/crashlytics.js +++ b/__mocks__/@react-native-firebase/crashlytics.js @@ -1 +1,6 @@ -export default {}; +// uses 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(), +}; diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index e2630c1ef632..495745fa8c66 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -1,6 +1,7 @@ // 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/'; @@ -9,6 +10,13 @@ jest.doMock('react-native', () => { 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: { @@ -53,10 +61,10 @@ jest.doMock('react-native', () => { Dimensions: { ...ReactNative.Dimensions, addEventListener: jest.fn(), - get: () => ({ - // Tests will run with the app in a typical small screen size - width: 300, height: 700, scale: 1, fontScale: 1, - }), + get: () => dimensions, + set: (newDimensions) => { + dimensions = newDimensions; + }, }, }, ReactNative, diff --git a/jest/setup.js b/jest/setup.js index 722e67007f13..d62bf1e6468e 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -16,13 +16,6 @@ jest.mock('react-native-reanimated', () => { return Reanimated; }); -// uses 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 -jest.mock('@react-native-firebase/crashlytics', () => () => ({ - log: jest.fn(), - recordError: jest.fn(), -})); - // 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). jest.mock('../src/libs/BootSplash', () => ({ diff --git a/src/CONST.js b/src/CONST.js index bc443e5e3fba..c5f954527670 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -827,6 +827,13 @@ const CONST = { INCORRECT_PASSWORD: 2, }, }, + TESTING: { + SCREEN_SIZE: { + SMALL: { + width: 300, height: 700, scale: 1, fontScale: 1, + }, + }, + }, }; export default CONST; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 1b49c722e642..86fbbfca799d 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -124,7 +124,7 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { * @param {Object} [actionsToMerge] * @param {Number} deletedSequenceNumber * @param {Number} lastReadSequenceNumber - * @return {String} + * @return {Number} */ function getOptimisticLastReadSequenceNumberForDeletedAction(reportID, actionsToMerge = {}, deletedSequenceNumber, lastReadSequenceNumber) { // If the action we are deleting is unread then just return the current last read sequence number diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 93630d4f661d..10adbab78db5 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -25,8 +25,6 @@ beforeAll(() => { // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. global.fetch = TestHelper.getGlobalFetchMock(); - - // We need a bit more time for this test in some places Linking.setInitialURL('https://new.expensify.com/r/1'); appSetup(); }); @@ -35,7 +33,7 @@ beforeAll(() => { * @param {RenderAPI} renderedApp */ function scrollUpToRevealNewMessagesBadge(renderedApp) { - fireEvent.scroll(renderedApp.getByA11yLabel('List of chat messages'), { + fireEvent.scroll(renderedApp.queryByA11yLabel('List of chat messages'), { nativeEvent: { contentOffset: { y: 250, @@ -63,7 +61,7 @@ function scrollUpToRevealNewMessagesBadge(renderedApp) { * @return {Boolean} */ function isNewMessagesBadgeVisible(renderedApp) { - const badge = renderedApp.getByA11yHint('Scroll to newest messages'); + const badge = renderedApp.queryByA11yHint('Scroll to newest messages'); return badge.props.style.transform[0].translateY === 10; } @@ -72,7 +70,7 @@ function isNewMessagesBadgeVisible(renderedApp) { * @return {Promise} */ function navigateToSidebar(renderedApp) { - const reportHeaderBackButton = renderedApp.getByA11yHint('Navigate back to chats list'); + const reportHeaderBackButton = renderedApp.queryByA11yHint('Navigate back to chats list'); fireEvent(reportHeaderBackButton, 'press'); return waitForPromisesToResolve(); } @@ -83,7 +81,7 @@ function navigateToSidebar(renderedApp) { * @return {Promise} */ function navigateToSidebarOption(renderedApp, index) { - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); fireEvent(optionRows[index], 'press'); return waitForPromisesToResolve(); } @@ -116,7 +114,7 @@ function signInAndGetAppWithUnreadChat() { return waitForPromisesToResolveWithAct() .then(() => { const loginForm = renderedApp.queryAllByA11yLabel('Login form'); - expect(loginForm.length).toBe(1); + expect(loginForm).toHaveLength(1); return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); }) @@ -172,19 +170,19 @@ describe('Unread Indicators', () => { renderedApp = testInstance; // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(0); + expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(0); // Verify the sidebar links are rendered const sidebarLinks = renderedApp.queryAllByA11yLabel('List of chats'); - expect(sidebarLinks.length).toBe(1); + expect(sidebarLinks).toHaveLength(1); expect(isDrawerOpen(renderedApp)).toBe(true); // Verify there is only one option in the sidebar - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(1); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); + expect(optionRows).toHaveLength(1); // And that the text is bold - const displayNameText = renderedApp.getByA11yLabel('Chat user display names'); + const displayNameText = renderedApp.queryByA11yLabel('Chat user display names'); expect(lodashGet(displayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); return navigateToSidebarOption(renderedApp, 0); @@ -194,15 +192,15 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(false); // That the report actions are visible along with the created action - const createdAction = renderedApp.getByA11yLabel('Chat welcome message'); + const createdAction = renderedApp.queryByA11yLabel('Chat welcome message'); expect(createdAction).toBeTruthy(); - const reportComments = renderedApp.getAllByA11yLabel('Chat message'); - expect(reportComments.length).toBe(9); + const reportComments = renderedApp.queryAllByA11yLabel('Chat message'); + expect(reportComments).toHaveLength(9); // Since the last read sequenceNumber is 1 we should have an unread indicator above the next "unread" action which will // have a sequenceNumber of 2 const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(2); @@ -232,7 +230,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); // Verify that the option row in the LHN is no longer bold (since OpenReport marked it as read) - const updatedDisplayNameText = renderedApp.getByA11yLabel('Chat user display names'); + const updatedDisplayNameText = renderedApp.queryByA11yLabel('Chat user display names'); expect(lodashGet(updatedDisplayNameText, ['props', 'style', 0, 'fontWeight'])).toBe(undefined); // Tap on the chat again @@ -241,7 +239,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the unread indicator is not present const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); expect(isDrawerOpen(renderedApp)).toBe(false); // Scroll and verify that the new messages badge is also hidden @@ -293,19 +291,19 @@ describe('Unread Indicators', () => { }) .then(() => { // Verify notification was created as the new message that has arrived is very recent - expect(LocalNotification.showCommentNotification.mock.calls.length).toBe(1); + expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(1); // // Navigate back to the sidebar return navigateToSidebar(renderedApp); }) .then(() => { // // Verify the new report option appears in the LHN - const optionRows = renderedApp.getAllByA11yHint('Navigates to a chat'); - expect(optionRows.length).toBe(2); + const optionRows = renderedApp.queryAllByA11yHint('Navigates to a chat'); + expect(optionRows).toHaveLength(2); // Verify the text for both chats are bold indicating that nothing has not yet been read const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); + expect(displayNameTexts).toHaveLength(2); const firstReportOption = displayNameTexts[0]; expect(lodashGet(firstReportOption, ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(firstReportOption, ['props', 'children'])).toBe('C User'); @@ -320,7 +318,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(2); + expect(displayNameTexts).toHaveLength(2); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('C User'); expect(lodashGet(displayNameTexts[1], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -346,7 +344,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the indicator appears above the last action const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); const sequenceNumber = lodashGet(unreadIndicator, [0, 'props', 'data-sequence-number']); expect(sequenceNumber).toBe(3); @@ -360,7 +358,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the report is marked as unread in the sidebar const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); @@ -371,7 +369,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the report is now marked as read const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(undefined); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); @@ -380,7 +378,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); // Scroll up and verify the "New messages" badge is hidden scrollUpToRevealNewMessagesBadge(renderedApp); @@ -398,7 +396,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -407,7 +405,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Leave a comment as the current user and verify the indicator is removed Report.addComment(REPORT_ID, 'Current User Comment 1'); @@ -415,7 +413,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); }); }); @@ -429,7 +427,7 @@ describe('Unread Indicators', () => { expect(isDrawerOpen(renderedApp)).toBe(true); const displayNameTexts = renderedApp.queryAllByA11yLabel('Chat user display names'); - expect(displayNameTexts.length).toBe(1); + expect(displayNameTexts).toHaveLength(1); expect(lodashGet(displayNameTexts[0], ['props', 'children'])).toBe('B User'); expect(lodashGet(displayNameTexts[0], ['props', 'style', 0, 'fontWeight'])).toBe(fontWeightBold); @@ -438,7 +436,7 @@ describe('Unread Indicators', () => { }) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Then back to the LHN - then back to the chat again and verify the new line indicator has cleared return navigateToSidebar(renderedApp); @@ -446,7 +444,7 @@ describe('Unread Indicators', () => { .then(() => navigateToSidebarOption(renderedApp, 0)) .then(() => { const unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); // Mark a previous comment as unread and verify the unread action indicator returns Report.markCommentAsUnread(REPORT_ID, 9); @@ -454,7 +452,7 @@ describe('Unread Indicators', () => { }) .then(() => { let unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(1); + expect(unreadIndicator).toHaveLength(1); // Trigger the app going inactive and active again AppState.emitCurrentTestState('background'); @@ -462,7 +460,7 @@ describe('Unread Indicators', () => { // Verify the new line is cleared unreadIndicator = renderedApp.queryAllByA11yLabel('New message line indicator'); - expect(unreadIndicator.length).toBe(0); + expect(unreadIndicator).toHaveLength(0); }); }); @@ -506,7 +504,7 @@ describe('Unread Indicators', () => { .then(() => { // Verify the chat preview text matches the last comment from the current user const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); - expect(alternateText.length).toBe(1); + expect(alternateText).toHaveLength(1); expect(alternateText[0].props.children).toBe('Current User Comment 1'); Report.deleteReportComment(REPORT_ID, lastReportAction); @@ -514,7 +512,7 @@ describe('Unread Indicators', () => { }) .then(() => { const alternateText = renderedApp.queryAllByA11yLabel('Last chat message preview'); - expect(alternateText.length).toBe(1); + expect(alternateText).toHaveLength(1); expect(alternateText[0].props.children).toBe('Comment 9'); }); }); diff --git a/tests/utils/waitForPromisesToResolveWithAct.js b/tests/utils/waitForPromisesToResolveWithAct.js index 37d5ccb041ce..eaef0f3b1a9d 100644 --- a/tests/utils/waitForPromisesToResolveWithAct.js +++ b/tests/utils/waitForPromisesToResolveWithAct.js @@ -10,6 +10,18 @@ import waitForPromisesToResolve from './waitForPromisesToResolve'; * This apparently will not work unless we use async/await because of some kind of voodoo magic inside the react-test-renderer * so we are suppressing our lint rule and avoiding async/await everywhere else in our tests. * + * When to use this: + * + * - If you see the jest output complaining about needing to use act() AND the callback inside the act() returns a promise. We can't + * call .then() on act() and the test renderer wants us to use async/await so use this function instead. + * + * + * When not to use this: + * + * - You're not rendering any react components at all in your tests, but have some async logic you need to wait for e.g. Onyx.merge(). Use waitForPromisesToResolve(). + * - You're writing UI tests but don't see any errors or warnings related to using act(). You probably don't need this in that case and should use waitForPromisesToResolve(). + * - You're writing UI test and do see a warning about using act(), but there's no asynchronous code that needs to run inside act(). + * * @returns {Promise} */ // eslint-disable-next-line @lwc/lwc/no-async-await From e961332cc29d847968c816b50ced538dfe5b278c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:36:39 -0600 Subject: [PATCH 57/59] fix lint errors --- src/libs/actions/Session/index.js | 5 ++++- src/libs/actions/SignInRedirect.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 3ee2a8afc3aa..696642d31ff2 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -250,7 +250,9 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); + + // eslint-disable-next-line rulesdir/prefer-localization + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: errorMessage}, isLoading: false}); return; } @@ -348,6 +350,7 @@ function setPassword(password, validateCode, accountID) { } // This request can fail if the password is not complex enough + // eslint-disable-next-line rulesdir/prefer-localization Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: response.message}}); }) .finally(() => { diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 6741fabfb4c8..c0388ef8d430 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -48,7 +48,8 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); + // eslint-disable-next-line rulesdir/prefer-localization + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); } }); } From 37bfdfcc45730d464b12310f18675af0154e6451 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:43:18 -0600 Subject: [PATCH 58/59] rm unused import --- src/libs/actions/SignInRedirect.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index c0388ef8d430..997470023ef6 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,7 +3,6 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; -import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ From d36b1753a99f091880075a2425f3e491d908977b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 16 Sep 2022 16:55:57 -0600 Subject: [PATCH 59/59] revert changes --- src/libs/actions/Session/index.js | 4 +--- src/libs/actions/SignInRedirect.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 696642d31ff2..86e2a0089cab 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -250,9 +250,7 @@ function signIn(password, twoFactorAuthCode) { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, isLoading: false}); return; } - - // eslint-disable-next-line rulesdir/prefer-localization - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: errorMessage}, isLoading: false}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}, isLoading: false}); return; } diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 997470023ef6..6741fabfb4c8 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -3,6 +3,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as MainQueue from '../Network/MainQueue'; // eslint-disable-next-line import/no-cycle import DateUtils from '../DateUtils'; +import * as Localize from '../Localize'; let currentActiveClients; Onyx.connect({ @@ -47,8 +48,7 @@ function clearStorageAndRedirect(errorMessage) { // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` if (errorMessage) { - // eslint-disable-next-line rulesdir/prefer-localization - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: errorMessage}}); + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); } }); }