Skip to content

Commit

Permalink
feat: e2e testing payments flow (GaloyMoney#667)
Browse files Browse the repository at this point in the history
* feat: send payments flow

* feat: send payment testProps

* feat: add amount test

* feat: receive flow

* feat: auto grant perms, appium-doctor

* chore: mermaid diagram

* chore: update docs

* fix: android selector swipe

* chore: grapqhl lookup walletId

* chore: temp circle ci test

* chore: temp circle ci e2e-testing-payments

* feat: ci with browserstack

* fix: iphone device os version

* fix: ios timing bugs and device id

* chore: update BROWSERSTACK_APP_ID in Fastfile

* fix: ios selector SecureTextField

* feat: share link invoice for ios

* fix: set specific ios version

* chore: rename browserstack CircleCI tests

* chore: remove circleci test for this branch

* feat: trigger e2e on main commit

* chore: final code cleanup

* chore: cleanup code

* chore: ios fastlane cleanup

Co-authored-by: daviroo <david.borthwick@gmail.com>

* chore: android fastlane cleanup

Co-authored-by: daviroo <david.borthwick@gmail.com>

* chore: fix code check

* fix: save changes screen

* feat: add scroll helper

* fix: save twice

* fix: ios allow notification

* feat: add env vars for BUILD_VERSION

* revert: circleci config

* revert: hold off on running e2e browserstack

Co-authored-by: daviroo <david.borthwick@gmail.com>
  • Loading branch information
ntheile and daviroo authored Dec 7, 2022
1 parent e89159c commit 0e11797
Show file tree
Hide file tree
Showing 28 changed files with 1,020 additions and 200 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ ios/GoogleService-Info.plist

.yalc
yalc.lock

*.log
6 changes: 0 additions & 6 deletions android/browserstack.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,19 @@ exports.config = {
},

onPrepare: (config, capabilities) => {
console.log("Connecting local");
return new Promise((resolve, reject) => {
exports.bs_local = new browserstack.Local();
exports.bs_local.start({ 'key': exports.config.key }, (error) => {
if (error) return reject(error);
console.log('Connected. Now testing...');

resolve();
});
});
},

onComplete: (capabilties, specs) => {
console.log("Closing local tunnel");
return new Promise((resolve, reject) => {
exports.bs_local.stop((error) => {
if (error) return reject(error);
console.log("Stopped BrowserStackLocal");

resolve();
});
});
Expand Down
2 changes: 1 addition & 1 deletion android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ platform :android do
browserstack_access_key: ENV["BROWSERSTACK_ACCESS_KEY"],
file_path: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
)
sh("npx wdio ../browserstack.js")
# sh("GALOY_TOKEN=$GALOY_TOKEN && GALOY_TOKEN_2=$GALOY_TOKEN_2 && yarn test:browserstack:android")
end

desc "Build for end to end testing"
Expand Down
2 changes: 2 additions & 0 deletions app/components/stablesats-modal/stablesats-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EStyleSheet from "react-native-extended-stylesheet"
import Modal from "react-native-modal"
import StableSatsImage from "../../assets/images/stable-sats.png"
import { useI18nContext } from "@app/i18n/i18n-react"
import { testProps } from "../../../utils/testProps"

const styles = EStyleSheet.create({
imageContainer: {
Expand Down Expand Up @@ -98,6 +99,7 @@ export const StableSatsModal: React.FC = () => {
</View>
<View style={styles.cardActionsContainer}>
<Button
{...testProps(LL.common.backHome())}
title={LL.common.backHome()}
onPress={acknowledgeModal}
buttonStyle={styles.homeButton}
Expand Down
1 change: 1 addition & 0 deletions app/screens/move-money-screen/move-money-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export const MoveMoneyScreen: ScreenType = ({
item && (
<>
<LargeButton
{...testProps(item.title)}
title={item.title}
icon={item.icon}
onPress={() => onMenuClick(item.target)}
Expand Down
3 changes: 2 additions & 1 deletion app/screens/receive-bitcoin-screen/qr-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "../../utils/wallet"

import successLottie from "../send-bitcoin-screen/success_lottie.json"
import { testProps } from "../../../utils/testProps"

const configByType = {
[TYPE_LIGHTNING_BTC]: {
Expand Down Expand Up @@ -79,7 +80,7 @@ export const QRView = ({
const renderSuccessView = useMemo(() => {
if (completed) {
return (
<View style={styles.container}>
<View {...testProps("Success Icon")} style={styles.container}>
<LottieView
source={successLottie}
loop={false}
Expand Down
11 changes: 9 additions & 2 deletions app/screens/receive-bitcoin-screen/receive-btc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { copyPaymentInfoToClipboard } from "@app/utils/clipboard"
import { useI18nContext } from "@app/i18n/i18n-react"
import { logGeneratePaymentRequest } from "@app/utils/analytics"
import { WalletCurrency } from "@app/types/amounts"
import { testProps } from "../../../utils/testProps"

const styles = EStyleSheet.create({
container: {
Expand Down Expand Up @@ -526,7 +527,10 @@ const ReceiveBtc = () => {
{invoiceReady ? (
<>
<View style={styles.copyInvoiceContainer}>
<Pressable onPress={copyToClipboard}>
<Pressable
{...testProps(LL.ReceiveBitcoinScreen.copyInvoice())}
onPress={copyToClipboard}
>
<Text style={styles.infoText}>
<Icon style={styles.infoText} name="copy-outline" />
<Text> </Text>
Expand All @@ -537,7 +541,10 @@ const ReceiveBtc = () => {
</Pressable>
</View>
<View style={styles.shareInvoiceContainer}>
<Pressable onPress={share}>
<Pressable
{...testProps(LL.ReceiveBitcoinScreen.shareInvoice())}
onPress={share}
>
<Text style={styles.infoText}>
<Icon style={styles.infoText} name="share-outline" />
<Text> </Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CommonActions } from "@react-navigation/native"
import useMainQuery from "@app/hooks/use-main-query"
import { useI18nContext } from "@app/i18n/i18n-react"
import { logPaymentAttempt, logPaymentResult } from "@app/utils/analytics"
import { testProps } from "../../../utils/testProps"
import crashlytics from "@react-native-firebase/crashlytics"

const styles = StyleSheet.create({
Expand Down Expand Up @@ -567,6 +568,7 @@ const SendBitcoinConfirmationScreen = ({
) : null}
<View style={styles.buttonContainer}>
<Button
{...testProps(LL.SendBitcoinConfirmationScreen.title())}
loading={isLoading}
title={LL.SendBitcoinConfirmationScreen.title()}
buttonStyle={styles.button}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
InvalidOnchainDestinationReason,
} from "@galoymoney/client/dist/parsing-v2"
import { logPaymentDestinationAccepted } from "@app/utils/analytics"
import { testProps } from "../../../utils/testProps"

const Styles = StyleSheet.create({
scrollView: {
Expand Down Expand Up @@ -513,6 +514,7 @@ const SendBitcoinDestinationScreen = ({

<View style={[Styles.fieldBackground, inputContainerStyle]}>
<TextInput
{...testProps(LL.SendBitcoinScreen.input())}
style={Styles.input}
placeholder={LL.SendBitcoinScreen.input()}
onChangeText={handleChangeText}
Expand All @@ -533,6 +535,7 @@ const SendBitcoinDestinationScreen = ({
<DestinationInformation destinationState={destinationState} />
<View style={Styles.buttonContainer}>
<Button
{...testProps(LL.common.next())}
title={
destinationState.unparsedDestination
? LL.common.next()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Icon from "react-native-vector-icons/Ionicons"
import NoteIcon from "@app/assets/icons/note.svg"
import { Button } from "react-native-elements"
import { useI18nContext } from "@app/i18n/i18n-react"
import { testProps } from "../../../utils/testProps"

const Styles = StyleSheet.create({
scrollView: {
Expand Down Expand Up @@ -462,6 +463,7 @@ const SendBitcoinDetailsScreen = ({
{fromWallet.walletCurrency === "BTC" && paymentAmount.currency === "BTC" && (
<>
<FakeCurrencyInput
{...testProps("BTC Amount")}
value={paymentAmountToDollarsOrSats(btcAmount)}
onChangeValue={setAmountsWithBtc}
prefix=""
Expand All @@ -474,6 +476,7 @@ const SendBitcoinDetailsScreen = ({
style={Styles.walletBalanceInput}
/>
<FakeCurrencyInput
{...testProps("USD Amount")}
value={paymentAmountToDollarsOrSats(usdAmount)}
onChangeValue={(amount) => setAmountsWithUsd(amount * 100)}
prefix="$"
Expand All @@ -489,6 +492,7 @@ const SendBitcoinDetailsScreen = ({
{fromWallet.walletCurrency === "BTC" && paymentAmount.currency === "USD" && (
<>
<FakeCurrencyInput
{...testProps("USD Amount")}
value={paymentAmountToDollarsOrSats(usdAmount)}
onChangeValue={(amount) => setAmountsWithUsd(amount * 100)}
prefix="$"
Expand All @@ -500,6 +504,7 @@ const SendBitcoinDetailsScreen = ({
editable={!isFixedAmountInvoice}
/>
<FakeCurrencyInput
{...testProps("BTC Amount")}
value={paymentAmountToDollarsOrSats(btcAmount)}
onChangeValue={setAmountsWithBtc}
prefix=""
Expand Down Expand Up @@ -581,6 +586,7 @@ const SendBitcoinDetailsScreen = ({

<View style={Styles.buttonContainer}>
<Button
{...testProps(LL.common.next())}
title={LL.common.next()}
buttonStyle={[Styles.button, Styles.activeButtonStyle]}
titleStyle={Styles.activeButtonTitleStyle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useMainQuery from "@app/hooks/use-main-query"
import { StackScreenProps } from "@react-navigation/stack"
import { RootStackParamList } from "@app/navigation/stack-param-lists"
import { useI18nContext } from "@app/i18n/i18n-react"
import { testProps } from "../../../utils/testProps"

const styles = StyleSheet.create({
scrollView: {
Expand Down Expand Up @@ -59,7 +60,12 @@ const SendBitcoinSuccessScreen = ({
style={styles.lottie}
resizeMode="cover"
/>
<Text style={styles.successLottieText}>{LL.SendBitcoinScreen.success()}</Text>
<Text
{...testProps(LL.SendBitcoinScreen.success())}
style={styles.successLottieText}
>
{LL.SendBitcoinScreen.success()}
</Text>
</View>
</ScrollView>
)
Expand Down
7 changes: 6 additions & 1 deletion app/screens/settings-screen/settings-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { copyPaymentInfoToClipboard } from "@app/utils/clipboard"
import { useI18nContext } from "@app/i18n/i18n-react"
import { openWhatsApp } from "@app/utils/external"
import { CustomIcon } from "@app/components/custom-icon"
import { testProps } from "../../../utils/testProps"

type Props = {
navigation: StackNavigationProp<RootStackParamList, "settings">
Expand Down Expand Up @@ -307,7 +308,11 @@ export const SettingsScreenJSX: ScreenType = (params: SettingsScreenProps) => {

return (
<React.Fragment key={`setting-option-${i}`}>
<ListItem onPress={setting.action} disabled={!setting.enabled}>
<ListItem
onPress={setting.action}
disabled={!setting.enabled}
{...testProps(setting.category)}
>
{!setting.icon?.startsWith("custom") && (
<Icon name={setting.icon} type="ionicon" color={settingColor} />
)}
Expand Down
84 changes: 83 additions & 1 deletion docs/e2e-testing.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# E2E Testing

```mermaid
flowchart TD;
A[E2E Test]-->B{Cloud CI or Local?};
B -- Cloud --> C[Browserstack]
B -- Local --> D[Appium]
C -- Test On --> E[Cloud Device]
D -- Test On --> G[Local Simulator/Emulator/Phone]
```

## To Test locally with Appium and Webdriver:

1. run the debug version of the app `yarn android` or `yarn ios`
Expand All @@ -19,7 +28,9 @@ export BROWSERSTACK_APP_ID=bs://YOURAPPID

run `yarn test:browserstack:android`

## Getting the Name of an Android or IOS device
## To Test Locally

### Getting the Name of an Android or IOS device

There is a script in `bin/get-testing-device.sh` that will automatically get the name of the android or ios device and set the env vars `TEST_DEVICE_ANDROID` and `TEST_DEVICE_IOS`

Expand All @@ -37,6 +48,77 @@ IOS
TEST_DEVICE_IOS="iPhone 13" yarn test:e2e:ios
```

Here are the other env variables you need to set

```
GALOY_TOKEN={YOUR_TOKEN}
GALOY_TOKEN_2={SECOND_WALLET_TOKEN}
E2E_DEVICE={ios or android}
```

## Authenticated Tests

To run the authenticated tests you need to set the env variable `GALOY_TOKEN`. The e2e test will navigate to the settings/build version page and input the token

Then you can run one test at a time:

```
TEST="03" PLATFORM_VERSION="15.4" yarn test:e2e:ios:auth
```

## Troubleshooting

If you have any issues with appium then run `yarn appium-doctor`

## Finding Elements

Appium uses `Accessibility Labels` to locate components, such as buttons.

```ts
// This is used for E2E tests to apply id's to a <Component/>
// Usage:
// <Button {...testProps("testID")} />
export const testProps = (testID: string) => {
return {
testID,
accessible: true,
accessibilityLabel: testID,
}
}
```

You can install `appium inspector` https://github.com/appium/appium-inspector to find elements in the GUI. It can be configured by setting the `remote path` to `/wd/hub` and then using the `Desired Capabilities JSON repesentation`, example below. (make sure to input your simulator or android emulator settings):

ios

```
{
"platformName": "iOS",
"appium:deviceName": "iPhone 13",
"appium:bundleId": "io.galoy.bitcoinbeach",
"appium:automationName": "XCUITest",
"appium:platformVersion": "15.4"
}
```

android

```
{
"app": "/path/to/code/galoy-mobile/android/app/build/outputs/apk/debug/app-debug.apk",
"platformName": "Android",
"deviceName": "generic_x86",
"automationName": "UiAutomator2"
}
```

ios on browserstack - choose 'select cloud providers' then 'browserstack'

```
{
"appium:deviceName": "iPhone 13",
"appium:automationName": "XCUITest",
"appium:platformVersion": "15.1",
"appium:app": "bs://{YOUR_BROWSERSTACK_ID_FROM_CIRCLE_CI}"
}
```
16 changes: 5 additions & 11 deletions e2e/01-welcome-screen-flow.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { i18nObject } from "../app/i18n/i18n-util"
import { loadLocale } from "../app/i18n/i18n-util.sync"
import { swipe, selector } from "./utils"
import { swipeLeft, selector } from "./utils"

describe("Welcome Screen Flow", async () => {
loadLocale("en")
const LL = i18nObject("en")
const timeout = 30000
beforeEach(async () => {
console.info("[beforeAll]")
})
afterEach(async () => {
console.info("[afterAll] Done with testing!")
await browser.pause(1500)
})

it("loads and clicks 'Get Started button' ", async () => {
const getStartedButton = await $(selector(LL.GetStartedScreen.getStarted()))
await getStartedButton.waitForDisplayed({ timeout })
Expand All @@ -23,19 +17,19 @@ describe("Welcome Screen Flow", async () => {
it("swipes Why Should I Care?", async () => {
const caresText = await $(selector(LL.WelcomeFirstScreen.care(), "StaticText"))
await caresText.waitForDisplayed({ timeout })
await swipe()
await swipeLeft()
})

it("swipes Bitcoin is designed to let you...bank", async () => {
const bankText = await $(selector(LL.WelcomeFirstScreen.bank(), "StaticText"))
await bankText.waitForDisplayed({ timeout })
await swipe()
await swipeLeft()
})

it("swipes Before Bitcoin people had to...", async () => {
const beforeText = await $(selector(LL.WelcomeFirstScreen.before(), "StaticText"))
await beforeText.waitForDisplayed({ timeout })
await swipe()
await swipeLeft()
})

it("clicks 'Learn to Earn' and enters the main app", async () => {
Expand Down
Loading

0 comments on commit 0e11797

Please sign in to comment.