diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index edadad1fdca4..7c11b1448a19 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -7,7 +7,7 @@ on:
description: Pull Request number for correct placement of apps
required: true
pull_request_target:
- types: [opened, synchronize]
+ types: [opened, synchronize, labeled]
branches: ['*ci-test/**']
env:
@@ -17,19 +17,32 @@ jobs:
validateActor:
runs-on: ubuntu-latest
outputs:
- IS_TEAM_MEMBER: ${{ fromJSON(steps.isUserDeployer.outputs.isTeamMember) }}
+ READY_TO_BUILD: ${{steps.readyToBuild.outputs.READY_TO_BUILD}}
steps:
- - id: isUserDeployer
+ - id: isUserTeamMember
uses: tspascoal/get-user-teams-membership@baf2e6adf4c3b897bd65a7e3184305c165aec872
with:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
username: ${{ github.actor }}
- team: mobile-deployers
+ team: expensify-expensify
+ - name: Remove label if it was added by an unauthorized user
+ if: ${{ !fromJSON(steps.isUserTeamMember.outputs.isTeamMember) && github.event.label.name == 'Ready To Build' }}
+ uses: actions-ecosystem/action-remove-labels@v1
+ with:
+ labels: 'Ready To Build'
+ - name: Throw exception if label was added by an unauthorized user
+ if: ${{ !fromJSON(steps.isUserTeamMember.outputs.isTeamMember) && github.event.label.name == 'Ready To Build' }}
+ run: |
+ echo "The 'Ready To Build' label was added by an unauthorized user"
+ exit 1
+ - id: readyToBuild
+ name: Set READY_TO_BUILD flag
+ run: echo "READY_TO_BUILD=${{ fromJSON(steps.isUserTeamMember.outputs.isTeamMember) || contains(github.event.pull_request.labels.*.name, 'Ready To Build') }}" >> "$GITHUB_OUTPUT"
getBranchRef:
runs-on: ubuntu-latest
needs: validateActor
- if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }}
+ if: ${{ needs.validateActor.outputs.READY_TO_BUILD == 'true' }}
outputs:
REF: ${{steps.getHeadRef.outputs.REF}}
steps:
@@ -49,7 +62,7 @@ jobs:
android:
name: Build and deploy Android for testing
needs: [validateActor, getBranchRef]
- if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }}
+ if: ${{ needs.validateActor.outputs.READY_TO_BUILD == 'true' }}
runs-on: ubuntu-latest
env:
PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }}
@@ -99,7 +112,7 @@ jobs:
iOS:
name: Build and deploy iOS for testing
needs: [validateActor, getBranchRef]
- if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }}
+ if: ${{ needs.validateActor.outputs.READY_TO_BUILD == 'true' }}
env:
PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }}
runs-on: macos-12
@@ -155,7 +168,7 @@ jobs:
desktop:
name: Build and deploy Desktop for testing
needs: [validateActor, getBranchRef]
- if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }}
+ if: ${{ needs.validateActor.outputs.READY_TO_BUILD == 'true' }}
env:
PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }}
runs-on: macos-12
@@ -192,7 +205,7 @@ jobs:
web:
name: Build and deploy Web
needs: [validateActor, getBranchRef]
- if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }}
+ if: ${{ needs.validateActor.outputs.READY_TO_BUILD == 'true' }}
env:
PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }}
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 768ab38507e3..8265d5fd272b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,4 +93,4 @@ storybook-static
.jest-cache
# E2E test reports
-tests/e2e/.results/
+tests/e2e/results/
diff --git a/android/app/build.gradle b/android/app/build.gradle
index bddf3a7d18b7..d9113769a1db 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -156,8 +156,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001024301
- versionName "1.2.43-1"
+ versionCode 1001024600
+ versionName "1.2.46-0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
index 7c1f4a245cd2..9b8f64d51a53 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
@@ -124,6 +124,9 @@ private void createAndRegisterNotificationChannel(@NonNull Context context) {
* @param bitmap The bitmap image to modify.
*/
public Bitmap getCroppedBitmap(Bitmap bitmap) {
+ // Convert hardware bitmap to software bitmap so it can be drawn on the canvas
+ bitmap = bitmap.copy(Config.ARGB_8888, true);
+
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(output);
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index dc18ff815bb3..7f282a292bd5 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.2.43
+ 1.2.46
CFBundleSignature
????
CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.2.43.1
+ 1.2.46.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 7af9e83e7dea..96a597740774 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.2.43
+ 1.2.46
CFBundleSignature
????
CFBundleVersion
- 1.2.43.1
+ 1.2.46.0
diff --git a/package-lock.json b/package-lock.json
index 54b0c3234ef7..a3ac89f9a689 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.2.43-1",
+ "version": "1.2.46-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.2.43-1",
+ "version": "1.2.46-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -73,7 +73,7 @@
"react-native-pdf": "^6.6.2",
"react-native-performance": "^2.0.0",
"react-native-permissions": "^3.0.1",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#77cc9d42c474a693755941b10ee4c2d6f50e5346",
"react-native-plaid-link-sdk": "^7.2.0",
"react-native-reanimated": "3.0.0-rc.6",
"react-native-render-html": "6.3.1",
@@ -35528,8 +35528,8 @@
},
"node_modules/react-native-picker-select": {
"version": "8.0.4",
- "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10",
- "integrity": "sha512-fKuK7NBPYmf0rfQIPcOK1OrM31DIlQVEtdEBANWxudBXwj+okAakY9hIXPXkCd1Ow7gj5P3Z2XNle2ak6NtYPg==",
+ "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#77cc9d42c474a693755941b10ee4c2d6f50e5346",
+ "integrity": "sha512-KhadZYEWeoTQv/dj2tXpCRQvoY3L9tMGcVnopiYNSzlPdbnDzJUdvdDwf2bVdR3zQXrmHjzsYUVUJx3FFu6LAA==",
"license": "MIT",
"dependencies": {
"lodash.isequal": "^4.5.0"
@@ -69867,9 +69867,9 @@
"requires": {}
},
"react-native-picker-select": {
- "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10",
- "integrity": "sha512-fKuK7NBPYmf0rfQIPcOK1OrM31DIlQVEtdEBANWxudBXwj+okAakY9hIXPXkCd1Ow7gj5P3Z2XNle2ak6NtYPg==",
- "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10",
+ "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#77cc9d42c474a693755941b10ee4c2d6f50e5346",
+ "integrity": "sha512-KhadZYEWeoTQv/dj2tXpCRQvoY3L9tMGcVnopiYNSzlPdbnDzJUdvdDwf2bVdR3zQXrmHjzsYUVUJx3FFu6LAA==",
+ "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#77cc9d42c474a693755941b10ee4c2d6f50e5346",
"requires": {
"lodash.isequal": "^4.5.0"
}
diff --git a/package.json b/package.json
index 47e984a63f4e..c589aa337ed0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.2.43-1",
+ "version": "1.2.46-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.",
@@ -38,7 +38,7 @@
"analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production",
"symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map",
"symbolicate:ios": "npx metro-symbolicate main.jsbundle.map",
- "test:e2e": "node tests/e2e/testRunner.js"
+ "test:e2e": "node tests/e2e/testRunner.js --development"
},
"dependencies": {
"@expensify/react-native-web": "0.18.9",
@@ -104,7 +104,7 @@
"react-native-pdf": "^6.6.2",
"react-native-performance": "^2.0.0",
"react-native-permissions": "^3.0.1",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#77cc9d42c474a693755941b10ee4c2d6f50e5346",
"react-native-plaid-link-sdk": "^7.2.0",
"react-native-reanimated": "3.0.0-rc.6",
"react-native-render-html": "6.3.1",
diff --git a/scripts/android-repackage-app-bundle-and-sign.sh b/scripts/android-repackage-app-bundle-and-sign.sh
new file mode 100755
index 000000000000..fe4ee1e4b8fc
--- /dev/null
+++ b/scripts/android-repackage-app-bundle-and-sign.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+###
+# Takes an android app that has been built with the debug keystore,
+# and re-packages it with an alternative JS bundle to run.
+# It then signs the APK again, so you can simply install the app on a device.
+# This is useful if you quickly want to test changes to the JS code with a
+# release app, without having to rebuild the whole app.
+#
+# There are many outdated resources on how to re-sign an app. The main
+# flow and commands have been taken from:
+# - https://gist.github.com/floyd-fuh/7f7408b560672ece3ea78348559d47b6#file-repackage_apk_for_burp-py-L276-L319
+#
+# This script uses `apktool` instead of manually unzipping and zipping the app.
+# Only with apktool it worked without any errors, so you need to install it.
+###
+
+BUILD_TOOLS=$ANDROID_SDK_ROOT/build-tools/31.0.0
+APK=$1
+NEW_BUNDLE_FILE=$2
+OUTPUT_APK=$3
+
+### Helper function to use echo but print text in bold
+echo_bold() {
+ echo -e "\033[1m$*\033[0m"
+}
+
+### Validating inputs
+
+if [ -z "$APK" ] || [ -z "$NEW_BUNDLE_FILE" ] || [ -z "$OUTPUT_APK" ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+APK=$(realpath "$APK")
+if [ ! -f "$APK" ]; then
+ echo "APK not found: $APK"
+ exit 1
+fi
+NEW_BUNDLE_FILE=$(realpath "$NEW_BUNDLE_FILE")
+if [ ! -f "$NEW_BUNDLE_FILE" ]; then
+ echo "Bundle file not found: $NEW_BUNDLE_FILE"
+ exit 1
+fi
+OUTPUT_APK=$(realpath "$OUTPUT_APK")
+# check if "apktool" command is available
+if ! command -v apktool &> /dev/null
+then
+ echo "apktool could not be found. Please install it."
+ exit 1
+fi
+# check if "jarsigner" command is available
+if ! command -v jarsigner &> /dev/null
+then
+ echo "jarsigner could not be found. Please install it."
+ exit 1
+fi
+
+KEYSTORE="$(realpath ./android/app/debug.keystore)"
+ORIGINAL_WD=$(pwd)
+
+### Copy apk to a temp dir
+
+TMP_DIR=$(mktemp -d)
+cp "$APK" "$TMP_DIR"
+cd "$TMP_DIR" || exit
+
+### Dissemble app
+
+echo_bold "Dissembling app..."
+apktool d "$APK" -o app > /dev/null
+
+### Copy new bundle into assets
+
+echo_bold "Copying new bundle into assets..."
+rm app/assets/index.android.bundle
+cp "$NEW_BUNDLE_FILE" app/assets/index.android.bundle
+
+### Reassemble app
+
+echo_bold "Reassembling app..."
+apktool b app -o app.apk > /dev/null
+
+### Do jarsigner
+
+echo_bold "Signing app..."
+jarsigner -verbose -keystore "$KEYSTORE" -storepass android -keypass android app.apk androiddebugkey
+
+### Do zipalign
+
+echo_bold "Zipaligning app..."
+"$BUILD_TOOLS"/zipalign -p -v 4 app.apk app-aligned.apk
+
+### Do apksigner
+
+echo_bold "Signing app with apksigner..."
+"$BUILD_TOOLS"/apksigner sign --v4-signing-enabled true --ks "$KEYSTORE" --ks-pass pass:android --ks-key-alias androiddebugkey --key-pass pass:android app-aligned.apk
+
+### Copy back to original location
+
+echo_bold "Copying back to original location..."
+cp app-aligned.apk "$OUTPUT_APK"
+echo "Done. Repacked app is at $OUTPUT_APK"
+rm -rf "$TMP_DIR"
+cd "$ORIGINAL_WD" || exit
+
diff --git a/src/CONST.js b/src/CONST.js
index 77cdb683c3e2..ae375f27ae5a 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -496,11 +496,11 @@ const CONST = {
ADD_PAYMENT_MENU_POSITION_X: 356,
EMOJI_PICKER_SIZE: {
WIDTH: 320,
- HEIGHT: 400,
+ HEIGHT: 390,
},
- NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 298,
+ NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 288,
EMOJI_PICKER_ITEM_HEIGHT: 32,
- EMOJI_PICKER_HEADER_HEIGHT: 38,
+ EMOJI_PICKER_HEADER_HEIGHT: 32,
COMPOSER_MAX_HEIGHT: 125,
CHAT_FOOTER_MIN_HEIGHT: 65,
CHAT_SKELETON_VIEW: {
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index 3aa31b8e7d31..29d76102b4b6 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -63,6 +63,7 @@ const defaultProps = {
bankAccounts: [],
isLoading: false,
error: '',
+ errors: {},
},
selectedPlaidAccountID: '',
plaidLinkToken: '',
@@ -112,6 +113,7 @@ class AddPlaidBankAccount extends React.Component {
label: `${account.addressName} ${account.mask}`,
}));
const {icon, iconSize} = getBankIcon();
+ const plaidDataErrorMessage = !_.isEmpty(this.props.plaidData.errors) ? _.chain(this.props.plaidData.errors).values().first().value() : this.props.plaidData.error;
// Plaid Link view
if (!plaidBankAccounts.length) {
@@ -123,9 +125,9 @@ class AddPlaidBankAccount extends React.Component {
)}
- {Boolean(this.props.plaidData.error) && (
+ {Boolean(plaidDataErrorMessage) && (
- {this.props.plaidData.error}
+ {plaidDataErrorMessage}
)}
{Boolean(token) && (
diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js
index e034af725c17..ec1f23f70bd6 100644
--- a/src/components/DatePicker/index.android.js
+++ b/src/components/DatePicker/index.android.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import TextInput from '../TextInput';
import CONST from '../../CONST';
import {propTypes, defaultProps} from './datepickerPropTypes';
+import styles from '../../styles/styles';
class DatePicker extends React.Component {
constructor(props) {
@@ -49,6 +50,7 @@ class DatePicker extends React.Component {
placeholder={this.props.placeholder}
errorText={this.props.errorText}
containerStyles={this.props.containerStyles}
+ textInputContainerStyles={this.state.isPickerVisible ? [styles.borderColorFocus] : []}
onPress={this.showPicker}
editable={false}
disabled={this.props.disabled}
diff --git a/src/components/DatePicker/index.ios.js b/src/components/DatePicker/index.ios.js
index 062134ea468d..c9d076b7836b 100644
--- a/src/components/DatePicker/index.ios.js
+++ b/src/components/DatePicker/index.ios.js
@@ -75,6 +75,7 @@ class DatePicker extends React.Component {
placeholder={this.props.placeholder}
errorText={this.props.errorText}
containerStyles={this.props.containerStyles}
+ textInputContainerStyles={this.state.isPickerVisible ? [styles.borderColorFocus] : []}
onPress={this.showPicker}
editable={false}
disabled={this.props.disabled}
diff --git a/src/components/DragAndDrop/index.js b/src/components/DragAndDrop/index.js
index b35c43126c98..09ce3c07dd1d 100644
--- a/src/components/DragAndDrop/index.js
+++ b/src/components/DragAndDrop/index.js
@@ -17,6 +17,9 @@ const propTypes = {
/** Guard for accepting drops in drop zone. Drag event is passed to this function as first parameter. This prop is necessary to be inlined to satisfy the linter */
shouldAcceptDrop: PropTypes.func,
+ /** Whether drag & drop should be disabled */
+ disabled: PropTypes.bool,
+
/** Rendered child component */
children: PropTypes.node.isRequired,
};
@@ -33,6 +36,7 @@ const defaultProps = {
}
return false;
},
+ disabled: false,
};
export default class DragAndDrop extends React.Component {
@@ -52,6 +56,32 @@ export default class DragAndDrop extends React.Component {
}
componentDidMount() {
+ if (this.props.disabled) {
+ return;
+ }
+ this.addEventListeners();
+ }
+
+ componentDidUpdate(prevProps) {
+ const isDisabled = this.props.disabled;
+ if (isDisabled === prevProps.disabled) {
+ return;
+ }
+ if (isDisabled) {
+ this.removeEventListeners();
+ } else {
+ this.addEventListeners();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.disabled) {
+ return;
+ }
+ this.removeEventListeners();
+ }
+
+ addEventListeners() {
this.dropZone = document.getElementById(this.props.dropZoneId);
this.dropZoneRect = this.calculateDropZoneClientReact();
document.addEventListener('dragover', this.dropZoneDragListener);
@@ -61,7 +91,7 @@ export default class DragAndDrop extends React.Component {
window.addEventListener('resize', this.throttledDragNDropWindowResizeListener);
}
- componentWillUnmount() {
+ removeEventListeners() {
document.removeEventListener('dragover', this.dropZoneDragListener);
document.removeEventListener('dragenter', this.dropZoneDragListener);
document.removeEventListener('dragleave', this.dropZoneDragListener);
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index ab68b51802c1..87e3356b7c72 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -87,6 +87,7 @@ class EmojiPickerMenu extends Component {
this.onSelectionChange = this.onSelectionChange.bind(this);
this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
this.setFirstNonHeaderIndex = this.setFirstNonHeaderIndex.bind(this);
+ this.getItemLayout = this.getItemLayout.bind(this);
this.currentScrollOffset = 0;
this.firstNonHeaderIndex = 0;
@@ -190,6 +191,20 @@ class EmojiPickerMenu extends Component {
document.addEventListener('mousemove', this.mouseMoveHandler);
}
+ /**
+ * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping
+ * the measurement of dynamic content if we know the size (height or width) of items ahead of time.
+ * Generate and return an object with properties length(height of each individual row),
+ * offset(distance of the current row from the top of the FlatList), index(current row index)
+ *
+ * @param {*} data FlatList item
+ * @param {Number} index row index
+ * @returns {Object}
+ */
+ getItemLayout(data, index) {
+ return {length: CONST.EMOJI_PICKER_ITEM_HEIGHT, offset: CONST.EMOJI_PICKER_ITEM_HEIGHT * index, index};
+ }
+
/**
* Cleanup all mouse/keydown event listeners that we've set up
*/
@@ -513,6 +528,7 @@ class EmojiPickerMenu extends Component {
}
stickyHeaderIndices={this.state.headerIndices}
onScroll={e => this.currentScrollOffset = e.nativeEvent.contentOffset.y}
+ getItemLayout={this.getItemLayout}
/>
)}
-
)}
-
+
>
);
}
diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js
index 62cb9c2b427e..5fa626751984 100644
--- a/src/components/Picker/index.js
+++ b/src/components/Picker/index.js
@@ -10,6 +10,7 @@ import Text from '../Text';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import pickerStyles from './pickerStyles';
+import {ScrollContext} from '../ScrollViewWithContext';
const propTypes = {
/** Picker label */
@@ -149,6 +150,7 @@ class Picker extends PureComponent {
render() {
const hasError = !_.isEmpty(this.props.errorText);
+
return (
<>
@@ -201,6 +205,7 @@ class Picker extends PureComponent {
Picker.propTypes = propTypes;
Picker.defaultProps = defaultProps;
+Picker.contextType = ScrollContext;
// eslint-disable-next-line react/jsx-props-no-spreading
export default React.forwardRef((props, ref) => );
diff --git a/src/components/ReportActionItem/IOUAction.js b/src/components/ReportActionItem/IOUAction.js
index affa1035eb64..486f3d10fdae 100644
--- a/src/components/ReportActionItem/IOUAction.js
+++ b/src/components/ReportActionItem/IOUAction.js
@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
-import lodashGet from 'lodash/get';
import ONYXKEYS from '../../ONYXKEYS';
import IOUQuote from './IOUQuote';
import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
@@ -61,7 +60,6 @@ const IOUAction = (props) => {
/>
{shouldShowIOUPreview && (
(
{/* Get first word of IOU message */}
{Str.htmlDecode(fragment.text.split(' ')[0])}
-
+
{/* Get remainder of IOU message */}
{Str.htmlDecode(fragment.text.substring(fragment.text.indexOf(' ')))}
diff --git a/src/components/ScrollViewWithContext.js b/src/components/ScrollViewWithContext.js
new file mode 100644
index 000000000000..6d6c74c33245
--- /dev/null
+++ b/src/components/ScrollViewWithContext.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import {ScrollView} from 'react-native';
+
+const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16;
+
+const ScrollContext = React.createContext();
+
+// eslint-disable-next-line react/forbid-foreign-prop-types
+const propTypes = ScrollView.propTypes;
+
+/*
+* is a wrapper around that provides a ref to the .
+* can be used as a direct replacement for
+* if it contains one or more / components.
+* Using this wrapper will automatically handle scrolling to the picker's
+* when the picker modal is opened
+*/
+class ScrollViewWithContext extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ contentOffsetY: 0,
+ };
+ this.scrollViewRef = React.createRef(null);
+
+ this.setContextScrollPosition = this.setContextScrollPosition.bind(this);
+ }
+
+ setContextScrollPosition(event) {
+ if (this.props.onScroll) {
+ this.props.onScroll(event);
+ }
+ this.setState({contentOffsetY: event.nativeEvent.contentOffset.y});
+ }
+
+ render() {
+ return (
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+ScrollViewWithContext.propTypes = propTypes;
+
+export default ScrollViewWithContext;
+export {
+ ScrollContext,
+};
diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
index ae15b2443888..9630c047f476 100755
--- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
+++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
@@ -122,9 +122,7 @@ class BaseVideoChatButtonAndMenu extends Component {
>
diff --git a/src/libs/actions/CloseAccount.js b/src/libs/actions/CloseAccount.js
index 525398446862..ea45ae86c76f 100644
--- a/src/libs/actions/CloseAccount.js
+++ b/src/libs/actions/CloseAccount.js
@@ -6,7 +6,7 @@ import CONST from '../../CONST';
* Clear CloseAccount error message to hide modal
*/
function clearError() {
- Onyx.merge(ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, {error: ''});
+ Onyx.merge(ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, {error: '', errors: null});
}
/**
diff --git a/src/libs/actions/Plaid.js b/src/libs/actions/Plaid.js
index f6691b8f4a3c..393b5bbc9961 100644
--- a/src/libs/actions/Plaid.js
+++ b/src/libs/actions/Plaid.js
@@ -1,7 +1,6 @@
import getPlaidLinkTokenParameters from '../getPlaidLinkTokenParameters';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
-import * as Localize from '../Localize';
import CONST from '../../CONST';
/**
@@ -33,6 +32,7 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) {
value: {
isLoading: true,
error: '',
+ errors: null,
bankName,
},
}],
@@ -42,6 +42,7 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) {
value: {
isLoading: false,
error: '',
+ errors: null,
},
}],
failureData: [{
@@ -49,7 +50,6 @@ function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) {
key: ONYXKEYS.PLAID_DATA,
value: {
isLoading: false,
- error: Localize.translateLocal('bankAccount.error.noBankAccountAvailable'),
},
}],
});
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index eb55a8996e08..5b665c525b38 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -236,7 +236,10 @@ class ReportScreen extends React.Component {
placeholder={(
<>
-
+
+
+
+
>
)}
>
@@ -312,9 +315,10 @@ class ReportScreen extends React.Component {
{/* Note: The report should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
{(!this.isReportReadyForDisplay() || isLoadingInitialReportActions) && (
-
+ <>
+
+
+ >
)}
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index 05074699003c..271132da0047 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -88,7 +88,10 @@ const propTypes = {
isFocused: PropTypes.bool.isRequired,
/** Is the composer full size */
- isComposerFullSize: PropTypes.bool.isRequired,
+ isComposerFullSize: PropTypes.bool,
+
+ /** Whether user interactions should be disabled */
+ disabled: PropTypes.bool,
// The NVP describing a user's block status
blockedFromConcierge: PropTypes.shape({
@@ -539,7 +542,7 @@ class ReportActionCompose extends React.Component {
{shouldShowReportRecipientLocalTime
&& }
e.preventDefault()}
style={styles.composerSizeButton}
- disabled={isBlockedFromConcierge}
+ disabled={isBlockedFromConcierge || this.props.disabled}
>
@@ -591,7 +594,7 @@ class ReportActionCompose extends React.Component {
// Keep focus on the composer when Expand button is clicked.
onMouseDown={e => e.preventDefault()}
style={styles.composerSizeButton}
- disabled={isBlockedFromConcierge}
+ disabled={isBlockedFromConcierge || this.props.disabled}
>
@@ -608,7 +611,7 @@ class ReportActionCompose extends React.Component {
this.setMenuVisibility(true);
}}
style={styles.chatItemAttachButton}
- disabled={isBlockedFromConcierge}
+ disabled={isBlockedFromConcierge || this.props.disabled}
>
@@ -654,6 +657,7 @@ class ReportActionCompose extends React.Component {
this.setState({isDraggingOver: false});
}}
+ disabled={this.props.disabled}
>
this.setTextInputShouldClear(false)}
- isDisabled={isComposeDisabled || isBlockedFromConcierge}
+ isDisabled={isComposeDisabled || isBlockedFromConcierge || this.props.disabled}
selection={this.state.selection}
onSelectionChange={this.onSelectionChange}
isFullComposerAvailable={this.state.isFullComposerAvailable}
@@ -686,7 +690,7 @@ class ReportActionCompose extends React.Component {
{canUseTouchScreen() && this.props.isMediumScreenWidth ? null : (
this.focus(true)}
onEmojiSelected={this.addEmojiToTextBox}
/>
@@ -702,7 +706,7 @@ class ReportActionCompose extends React.Component {
// Keep focus on the composer when Send message is clicked.
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onMouseDown={e => e.preventDefault()}
- disabled={this.state.isCommentEmpty || isBlockedFromConcierge || hasExceededMaxCommentLength}
+ disabled={this.state.isCommentEmpty || isBlockedFromConcierge || this.props.disabled || hasExceededMaxCommentLength}
hitSlop={{
top: 3, right: 3, bottom: 3, left: 3,
}}
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 915b5b354745..bccb119da3ac 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -236,10 +236,6 @@ class ReportActionsView extends React.Component {
}
componentWillUnmount() {
- if (this.keyboardEvent) {
- this.keyboardEvent.remove();
- }
-
if (this.unsubscribeVisibilityListener) {
this.unsubscribeVisibilityListener();
}
diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js
index 779337e489bc..29fea5e2ed19 100644
--- a/src/pages/home/report/ReportFooter.js
+++ b/src/pages/home/report/ReportFooter.js
@@ -21,16 +21,16 @@ import reportPropTypes from '../../reportPropTypes';
const propTypes = {
/** Report object for the current report */
- report: reportPropTypes.isRequired,
+ report: reportPropTypes,
/** Report actions for the current report */
- reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)).isRequired,
+ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
/** Offline status */
isOffline: PropTypes.bool.isRequired,
/** Callback fired when the comment is submitted */
- onSubmitComment: PropTypes.func.isRequired,
+ onSubmitComment: PropTypes.func,
/** Any errors associated with an attempt to create a chat */
// eslint-disable-next-line react/forbid-prop-types
@@ -42,13 +42,20 @@ const propTypes = {
/** Whether the composer input should be shown */
shouldShowComposeInput: PropTypes.bool,
+ /** Whether user interactions should be disabled */
+ shouldDisableCompose: PropTypes.bool,
+
...windowDimensionsPropTypes,
};
const defaultProps = {
- shouldShowComposeInput: true,
+ report: {reportID: '0'},
+ reportActions: {},
+ onSubmitComment: () => {},
errors: {},
pendingAction: null,
+ shouldShowComposeInput: true,
+ shouldDisableCompose: false,
};
class ReportFooter extends React.Component {
@@ -99,6 +106,7 @@ class ReportFooter extends React.Component {
reportActions={this.props.reportActions}
report={this.props.report}
isComposerFullSize={this.props.isComposerFullSize}
+ disabled={this.props.shouldDisableCompose}
/>
diff --git a/src/pages/iou/IOUDetailsModal.js b/src/pages/iou/IOUDetailsModal.js
index e4c253e9fa77..e45d24a95d28 100644
--- a/src/pages/iou/IOUDetailsModal.js
+++ b/src/pages/iou/IOUDetailsModal.js
@@ -22,6 +22,7 @@ import SettlementButton from '../../components/SettlementButton';
import ROUTES from '../../ROUTES';
import FixedFooter from '../../components/FixedFooter';
import networkPropTypes from '../../components/networkPropTypes';
+import reportActionPropTypes from '../home/report/reportActionPropTypes';
const propTypes = {
/** URL Route params */
@@ -67,6 +68,9 @@ const propTypes = {
email: PropTypes.string,
}).isRequired,
+ /** Actions from the ChatReport */
+ reportActions: PropTypes.shape(reportActionPropTypes),
+
/** Information about the network */
network: networkPropTypes.isRequired,
@@ -75,6 +79,7 @@ const propTypes = {
const defaultProps = {
iou: {},
+ reportActions: {},
iouReport: undefined,
};
@@ -132,9 +137,17 @@ class IOUDetailsModal extends Component {
}
}
+ // Finds if there is a reportAction pending for this IOU
+ findPendingAction() {
+ return _.find(this.props.reportActions, reportAction => reportAction.originalMessage
+ && Number(reportAction.originalMessage.IOUReportID) === Number(this.props.route.params.iouReportID)
+ && !_.isEmpty(reportAction.pendingAction));
+ }
+
render() {
const sessionEmail = lodashGet(this.props.session, 'email', null);
const reportIsLoading = _.isUndefined(this.props.iouReport);
+ const pendingAction = this.findPendingAction();
return (
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.chatReportID}`,
+ canEvict: false,
+ },
}),
)(IOUDetailsModal);
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index 92ac6559f895..65e7d556c575 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -257,7 +257,7 @@ class WorkspaceMembersPage extends React.Component {
this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}>
this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}>
this.toggleUser(item.login, item.pendingAction)}
activeOpacity={0.7}
>
diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js
index 0816075726f3..cfe5cc40aa4b 100644
--- a/src/pages/workspace/WorkspacePageWithSections.js
+++ b/src/pages/workspace/WorkspacePageWithSections.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {View, ScrollView} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import _ from 'underscore';
@@ -20,6 +20,7 @@ import withPolicy from './withPolicy';
import {withNetwork} from '../../components/OnyxProvider';
import networkPropTypes from '../../components/networkPropTypes';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import ScrollViewWithContext from '../../components/ScrollViewWithContext';
const propTypes = {
shouldSkipVBBACall: PropTypes.bool,
@@ -121,7 +122,7 @@ class WorkspacePageWithSections extends React.Component {
/>
{this.props.shouldUseScrollView
? (
-
@@ -130,7 +131,7 @@ class WorkspacePageWithSections extends React.Component {
{this.props.children(hasVBA, policyID, isUsingECard)}
-
+
)
: this.props.children(hasVBA, policyID, isUsingECard)}
{this.props.footer}
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 37811e82c43a..58cfdc62df24 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1444,7 +1444,7 @@ const styles = {
},
emojiPickerList: {
- height: 300,
+ height: 288,
width: '100%',
...spacing.ph4,
},
@@ -1455,7 +1455,9 @@ const styles = {
emojiHeaderStyle: {
backgroundColor: themeColors.componentBG,
width: '100%',
- ...spacing.pv3,
+ height: 32,
+ display: 'flex',
+ alignItems: 'center',
fontFamily: fontFamily.EXP_NEUE_BOLD,
fontWeight: fontWeightBold,
color: themeColors.heading,
diff --git a/tests/e2e/ADDING_TESTS.md b/tests/e2e/ADDING_TESTS.md
index 43deeaf2f0b1..39cdb97ebed0 100644
--- a/tests/e2e/ADDING_TESTS.md
+++ b/tests/e2e/ADDING_TESTS.md
@@ -94,3 +94,9 @@ test file:
Done! When you now start the test runner, your new test will be executed as well.
+## Quickly test your test
+
+To check your new test you can simply run `npm run test:e2e`, which uses the
+`--development` flag. This will run the tests on the branch you are currently on
+and will do fewer iterations.
+
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index be45d677bae7..3262770bc55f 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -14,9 +14,36 @@ To run the e2e tests:
2. Make sure Fastlane was initialized by running `bundle install`
3. Run the tests with `npm run test:e2e`.
+ > š” Tip: To run the tests locally faster, and you are only making changes to JS, it's recommended to
+ build the app once with `npm run android-build-e2e` and from then on run the tests with
+ `npm run test:e2e -- --buildMode js-only`. This will only rebuild the JS code, and not the
+ whole native app!
Ideally you want to run these tests on your branch before you want to merge your new feature to `main`.
+## Available CLI options
+
+The tests can be run with the following CLI options:
+
+- `--config`: Extend/Overwrite the default config with your values, e.g. `--config config.local.js`
+- `--includes`: Expects a string/regexp to filter the tests to run, e.g. `--includes "login|signup"`
+- `--skipInstallDeps`: Skips the `npm install` step, useful during development
+- `--development`: Applies some default configurations:
+ - Sets the config to `config.local.js`, which executes the tests with fewer iterations
+ - Runs the tests only on the current branch
+- `--buildMode`: There are three build modes, the default is `full`:
+ 1. **full**: rebuilds the full native app in (e2e) release mode
+ 2. **js-only**: only rebuilds the js bundle, and then re-packages
+ the existing native app with the new package. If there
+ is no existing native app, it will fallback to mode "full"
+ 3. **skip**: does not rebuild anything, and just runs the existing native app
+
+## Available environment variables
+
+The tests can be run with the following environment variables:
+
+- `baseline`: Change the baseline to run the tests again (default is `main`).
+
## Performance regression testing
The output of the tests is a set of performance metrics (see video above).
diff --git a/tests/e2e/config.js b/tests/e2e/config.js
index aad6b742b582..34f81467866d 100644
--- a/tests/e2e/config.js
+++ b/tests/e2e/config.js
@@ -1,4 +1,4 @@
-const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './results';
+const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results';
/**
* @typedef TestConfig
@@ -10,9 +10,25 @@ const TEST_NAMES = {
AppStartTime: 'App start time',
};
+/**
+ * Default config, used by CI by default.
+ * You can modify these values for your test run by creating a
+ * separate config file and pass it to the test runner like this:
+ *
+ * ```bash
+ * npm run test:e2e -- --config ./path/to/your/config.js
+ * ```
+ */
module.exports = {
APP_PACKAGE: 'com.expensify.chat',
+ APP_PATHS: {
+ baseline: './app-e2eRelease-baseline.apk',
+ compare: './app-e2eRelease-compare.apk',
+ },
+
+ ENTRY_FILE: 'src/libs/E2E/reactNativeLaunchingTest.js',
+
// The port of the testing server that communicates with the app
SERVER_PORT: 4723,
@@ -21,9 +37,6 @@ module.exports = {
DEFAULT_BASELINE_BRANCH: 'main',
- // The amount of outliers to remove from a dataset before calculating the average
- DROP_WORST: 8,
-
// The amount of runs that should happen without counting test results
WARM_UP_RUNS: 3,
diff --git a/tests/e2e/config.local.js b/tests/e2e/config.local.js
new file mode 100644
index 000000000000..cd0b04d7c3cf
--- /dev/null
+++ b/tests/e2e/config.local.js
@@ -0,0 +1,8 @@
+module.exports = {
+ WARM_UP_RUNS: 1,
+ RUNS: 8,
+ APP_PATHS: {
+ baseline: './android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk',
+ compare: './android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk',
+ },
+};
diff --git a/tests/e2e/testRunner.js b/tests/e2e/testRunner.js
index a5165db1ac67..a82dc3a9212a 100644
--- a/tests/e2e/testRunner.js
+++ b/tests/e2e/testRunner.js
@@ -9,14 +9,7 @@
/* eslint-disable @lwc/lwc/no-async-await,no-restricted-syntax,no-await-in-loop */
const fs = require('fs');
const _ = require('underscore');
-const {
- DEFAULT_BASELINE_BRANCH,
- OUTPUT_DIR,
- LOG_FILE,
- RUNS,
- WARM_UP_RUNS,
- TESTS_CONFIG,
-} = require('./config');
+const defaultConfig = require('./config');
const compare = require('./compare/compare');
const Logger = require('./utils/logger');
const execAsync = require('./utils/execAsync');
@@ -27,20 +20,57 @@ const installApp = require('./utils/installApp');
const math = require('./measure/math');
const writeTestStats = require('./measure/writeTestStats');
const withFailTimeout = require('./utils/withFailTimeout');
+const reversePort = require('./utils/androidReversePort');
+const getCurrentBranchName = require('./utils/getCurrentBranchName');
const args = process.argv.slice(2);
-const baselineBranch = process.env.baseline || DEFAULT_BASELINE_BRANCH;
+let config = defaultConfig;
+const setConfigPath = (configPathParam) => {
+ let configPath = configPathParam;
+ if (!configPath.startsWith('.')) {
+ configPath = `./${configPath}`;
+ }
+ const customConfig = require(configPath);
+ config = _.extend(defaultConfig, customConfig);
+};
+
+let baselineBranch = process.env.baseline || config.DEFAULT_BASELINE_BRANCH;
+
+// There are three build modes:
+// 1. full: rebuilds the full native app in (e2e) release mode
+// 2. js-only: only rebuilds the js bundle, and then re-packages
+// the existing native app with the new package. If there
+// is no existing native app, it will fallback to mode "full"
+// 3. skip: does not rebuild anything, and just runs the existing native app
+let buildMode = 'full';
+
+// When we are in dev mode we want to apply certain default params and configs
+const isDevMode = args.includes('--development');
+if (isDevMode) {
+ setConfigPath('config.local.js');
+ baselineBranch = getCurrentBranchName();
+ buildMode = 'js-only';
+}
+
+if (args.includes('--config')) {
+ const configPath = args[args.indexOf('--config') + 1];
+ setConfigPath(configPath);
+}
// Clear all files from previous jobs
try {
- fs.rmSync(OUTPUT_DIR, {recursive: true, force: true});
- fs.mkdirSync(OUTPUT_DIR);
+ fs.rmSync(config.OUTPUT_DIR, {recursive: true, force: true});
+ fs.mkdirSync(config.OUTPUT_DIR);
} catch (error) {
// Do nothing
console.error(error);
}
+if (isDevMode) {
+ Logger.note(`Running in development mode. Set baseline branch to same as current ${baselineBranch}`);
+}
+
const restartApp = async () => {
Logger.log('Killing app ā¦');
await killApp('android');
@@ -49,26 +79,52 @@ const restartApp = async () => {
};
const runTestsOnBranch = async (branch, baselineOrCompare) => {
- if (!args.includes('--skipInstallDeps') && !args.includes('--skipBuild')) {
- // Switch branch and install dependencies
- Logger.log(`Preparing ${baselineOrCompare} tests on branch '${branch}'`);
- await execAsync(`git checkout ${branch}`);
+ if (args.includes('--buildMode')) {
+ buildMode = args[args.indexOf('--buildMode') + 1];
+ }
+ let appPath = baselineOrCompare === 'baseline' ? config.APP_PATHS.baseline : config.APP_PATHS.compare;
+
+ // check if using buildMode "js-only" or "none" is possible
+ if (buildMode !== 'full') {
+ const appExists = fs.existsSync(appPath);
+ if (!appExists) {
+ Logger.warn(`Build mode "${buildMode}" is not possible, because the app does not exist. Falling back to build mode "full".`);
+ buildMode = 'full';
+ }
}
+ // Switch branch
+ Logger.log(`Preparing ${baselineOrCompare} tests on branch '${branch}'`);
+ await execAsync(`git checkout ${branch}`);
+
if (!args.includes('--skipInstallDeps')) {
Logger.log(`Preparing ${baselineOrCompare} tests on branch '${branch}' - npm install`);
await execAsync('npm i');
}
// Build app
- if (!args.includes('--skipBuild')) {
+ if (buildMode === 'full') {
Logger.log(`Preparing ${baselineOrCompare} tests on branch '${branch}' - building app`);
await execAsync('npm run android-build-e2e');
+ } else if (buildMode === 'js-only') {
+ Logger.log(`Preparing ${baselineOrCompare} tests on branch '${branch}' - building js bundle`);
+
+ // Build a new JS bundle
+ const tempDir = `${config.OUTPUT_DIR}/temp`;
+ const tempBundlePath = `${tempDir}/index.android.bundle`;
+ await execAsync(`rm -rf ${tempDir} && mkdir ${tempDir}`);
+ await execAsync(`npx react-native bundle --platform android --dev false --entry-file ${config.ENTRY_FILE} --bundle-output ${tempBundlePath}`);
+
+ // Repackage the existing native app with the new bundle
+ const tempApkPath = `${tempDir}/app-release.apk`;
+ await execAsync(`./scripts/android-repackage-app-bundle-and-sign.sh ${appPath} ${tempBundlePath} ${tempApkPath}`);
+ appPath = tempApkPath;
}
- // Install app
- let progressLog = Logger.progressInfo('Installing app');
- await installApp('android', baselineOrCompare);
+ // Install app and reverse port
+ let progressLog = Logger.progressInfo('Installing app and reversing port');
+ await installApp('android', appPath);
+ await reversePort();
progressLog.done();
// Start the HTTP server
@@ -92,14 +148,26 @@ const runTestsOnBranch = async (branch, baselineOrCompare) => {
});
// Run the tests
- const numOfTests = _.values(TESTS_CONFIG).length;
+ const numOfTests = _.values(config.TESTS_CONFIG).length;
for (let testIndex = 0; testIndex < numOfTests; testIndex++) {
- const config = _.values(TESTS_CONFIG)[testIndex];
- server.setTestConfig(config);
+ const testConfig = _.values(config.TESTS_CONFIG)[testIndex];
+
+ // check if we want to skip the text
+ if (args.includes('--includes')) {
+ const includes = args[args.indexOf('--includes') + 1];
- const warmupLogs = Logger.progressInfo(`Running test '${config.name}'`);
- for (let warmUpRuns = 0; warmUpRuns < WARM_UP_RUNS; warmUpRuns++) {
- const progressText = `(${testIndex + 1}/${numOfTests}) Warmup for test '${config.name}' (iteration ${warmUpRuns + 1}/${WARM_UP_RUNS})`;
+ // assume that "includes" is a regexp
+ if (!testConfig.name.match(includes)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+ }
+
+ server.setTestConfig(testConfig);
+
+ const warmupLogs = Logger.progressInfo(`Running test '${testConfig.name}'`);
+ for (let warmUpRuns = 0; warmUpRuns < config.WARM_UP_RUNS; warmUpRuns++) {
+ const progressText = `(${testIndex + 1}/${numOfTests}) Warmup for test '${testConfig.name}' (iteration ${warmUpRuns + 1}/${config.WARM_UP_RUNS})`;
warmupLogs.updateText(progressText);
await restartApp();
@@ -116,8 +184,8 @@ const runTestsOnBranch = async (branch, baselineOrCompare) => {
// We run each test multiple time to average out the results
const testLog = Logger.progressInfo('');
- for (let i = 0; i < RUNS; i++) {
- const progressText = `(${testIndex + 1}/${numOfTests}) Running test '${config.name}' (iteration ${i + 1}/${RUNS})`;
+ for (let i = 0; i < config.RUNS; i++) {
+ const progressText = `(${testIndex + 1}/${numOfTests}) Running test '${testConfig.name}' (iteration ${i + 1}/${config.RUNS})`;
testLog.updateText(progressText);
await restartApp();
@@ -142,7 +210,7 @@ const runTestsOnBranch = async (branch, baselineOrCompare) => {
// Calculate statistics and write them to our work file
progressLog = Logger.progressInfo('Calculating statics and writing results');
- const outputFileName = `${OUTPUT_DIR}/${baselineOrCompare}.json`;
+ const outputFileName = `${config.OUTPUT_DIR}/${baselineOrCompare}.json`;
for (const testName of _.keys(durationsByTestName)) {
const stats = math.getStats(durationsByTestName[testName]);
await writeTestStats(
@@ -175,12 +243,17 @@ const runTests = async () => {
Logger.info('\n\nE2E test suite failed due to error:', e, '\nPrinting full logs:\n\n');
// Write logcat, meminfo, emulator info to file as well:
- require('node:child_process').execSync(`adb logcat -d > ${OUTPUT_DIR}/logcat.txt`);
- require('node:child_process').execSync(`adb shell "cat /proc/meminfo" > ${OUTPUT_DIR}/meminfo.txt`);
- require('node:child_process').execSync(`cat ~/.android/avd/${process.env.AVD_NAME || 'test'}.avd/config.ini > ${OUTPUT_DIR}/emulator-config.ini`);
- require('node:child_process').execSync(`adb shell "getprop" > ${OUTPUT_DIR}/emulator-properties.txt`);
+ require('node:child_process').execSync(`adb logcat -d > ${config.OUTPUT_DIR}/logcat.txt`);
+ require('node:child_process').execSync(`adb shell "cat /proc/meminfo" > ${config.OUTPUT_DIR}/meminfo.txt`);
+ require('node:child_process').execSync(`adb shell "getprop" > ${config.OUTPUT_DIR}/emulator-properties.txt`);
- require('node:child_process').execSync(`cat ${LOG_FILE}`);
+ require('node:child_process').execSync(`cat ${config.LOG_FILE}`);
+ try {
+ require('node:child_process').execSync(`cat ~/.android/avd/${process.env.AVD_NAME || 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`);
+ } catch (ignoredError) {
+ // the error is ignored, as the file might not exist if the test
+ // run wasn't started with an emulator
+ }
process.exit(1);
}
};
diff --git a/tests/e2e/utils/androidReversePort.js b/tests/e2e/utils/androidReversePort.js
new file mode 100644
index 000000000000..b644ca1538dd
--- /dev/null
+++ b/tests/e2e/utils/androidReversePort.js
@@ -0,0 +1,6 @@
+const {SERVER_PORT} = require('../config');
+const execAsync = require('./execAsync');
+
+module.exports = function () {
+ return execAsync(`adb reverse tcp:${SERVER_PORT} tcp:${SERVER_PORT}`);
+};
diff --git a/tests/e2e/utils/getCurrentBranchName.js b/tests/e2e/utils/getCurrentBranchName.js
new file mode 100644
index 000000000000..3380bd23ef15
--- /dev/null
+++ b/tests/e2e/utils/getCurrentBranchName.js
@@ -0,0 +1,10 @@
+const {execSync} = require('node:child_process');
+
+const getCurrentBranchName = () => {
+ const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
+ encoding: 'utf8',
+ });
+ return stdout.trim();
+};
+
+module.exports = getCurrentBranchName;
diff --git a/tests/e2e/utils/installApp.js b/tests/e2e/utils/installApp.js
index 32c2f44c9533..48246bb67c5e 100644
--- a/tests/e2e/utils/installApp.js
+++ b/tests/e2e/utils/installApp.js
@@ -2,27 +2,22 @@ const {APP_PACKAGE} = require('../config');
const execAsync = require('./execAsync');
const Logger = require('./logger');
-const BASELINE_APP_PATH_FROM_ROOT = './app-e2eRelease-baseline.apk';
-const COMPARE_APP_PATH_FROM_ROOT = './app-e2eRelease-compare.apk';
-
/**
* Installs the app on the currently connected device for the given platform.
* It removes the app first if it already exists, so it's a clean installation.
*
* @param {String} platform
- * @param {String} baselineOrCompare
+ * @param {String} path
* @returns {Promise}
*/
-module.exports = function (platform = 'android', baselineOrCompare = 'baseline') {
+module.exports = function (platform = 'android', path) {
if (platform !== 'android') {
throw new Error(`installApp() missing implementation for platform: ${platform}`);
}
- const apk = baselineOrCompare === 'baseline' ? BASELINE_APP_PATH_FROM_ROOT : COMPARE_APP_PATH_FROM_ROOT;
-
// Uninstall first, then install
return execAsync(`adb uninstall ${APP_PACKAGE}`).catch((e) => {
// Ignore errors
Logger.warn('Failed to uninstall app:', e);
- }).finally(() => execAsync(`adb install ${apk}`));
+ }).finally(() => execAsync(`adb install ${path}`));
};
diff --git a/tests/e2e/utils/logger.js b/tests/e2e/utils/logger.js
index a12fecd1efad..aa198aec3004 100644
--- a/tests/e2e/utils/logger.js
+++ b/tests/e2e/utils/logger.js
@@ -71,10 +71,17 @@ const warn = (...args) => {
log(...lines);
};
+const note = (...args) => {
+ const lines = [`\nš”${COLOR_DIM}`, ...args, `${COLOR_RESET}\n`];
+ console.debug(...lines);
+ log(...lines);
+};
+
module.exports = {
log,
info,
warn,
+ note,
progressInfo,
setLogLevelVerbose,
};