Skip to content

Commit

Permalink
feat: android detox tests
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains committed Nov 30, 2023
1 parent 70b747c commit d404e5d
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 41 deletions.
7 changes: 6 additions & 1 deletion .detoxrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const reversePorts = [80, 8080, 9735, 10009, 28334, 28335, 28336, 39388, 43782, 60001];

/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
$0: 'jest',
Expand All @@ -24,12 +27,14 @@ module.exports = {
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build:
'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd .. ',
reversePorts,
},
'android.release': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
build:
'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..',
reversePorts,
},
},
devices: {
Expand All @@ -42,7 +47,7 @@ module.exports = {
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_API_29_AOSP',
avdName: 'Pixel_API_31_AOSP',
},
},
},
Expand Down
101 changes: 101 additions & 0 deletions .github/workflows/e2e-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: e2e-android

on: pull_request

env:
E2E_TESTS: 1 # build without transform-remove-console babel plugin
DEBUG: 'lnurl* lnurl server'

jobs:
e2e:
runs-on: macos-12

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1

- name: Setup Docker Colima 1
uses: douglascamata/setup-docker-macos-action@v1-alpha
id: docker1
continue-on-error: true

- name: Setup Docker Colima 2
if: steps.docker1.outcome != 'success'
uses: douglascamata/setup-docker-macos-action@v1-alpha
# id: docker2
# continue-on-error: true

# - name: Setup Docker Default
# # if: steps.docker1.outcome != 'success' && steps.docker2.outcome != 'success'
# uses: docker-practice/[email protected]
# timeout-minutes: 30

- name: Run regtest setup
run: cd docker && mkdir lnd && chmod 777 lnd && docker-compose up -d

- name: Wait for bitcoind
timeout-minutes: 2
run: while ! nc -z '127.0.0.1' 43782; do sleep 1; done

- name: Wait for electrum server
timeout-minutes: 2
run: while ! nc -z '127.0.0.1' 60001; do sleep 1; done

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18.17
cache: 'yarn' # cache packages, but not node_modules

- name: Activate enviroment variables
run: cp .env.test.template .env

- name: Activate react-native-skia-stub
run: patch -p1 < .github/workflows/react-native-skia-stub.patch

- name: Activate Gradle variables
run: cp .github/workflows/gradle.properties ~/.gradle/gradle.properties

- name: Use specific Java version for sdkmanager to work
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Yarn Install
run: yarn --no-audit --prefer-offline || yarn --no-audit --prefer-offline
env:
HUSKY: 0

- name: Build
run: yarn e2e:build:android-release || yarn e2e:build:android-release

- name: Test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
profile: 5.4in FWVGA
avd-name: Pixel_API_31_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: |
yarn e2e:test:android-release --record-videos all --take-screenshots all --record-logs all || \
yarn e2e:test:android-release --record-videos all --take-screenshots all --record-logs all || \
yarn e2e:test:android-release --record-videos all --take-screenshots all --record-logs all || \
yarn e2e:test:android-release --record-videos all --take-screenshots all --record-logs all
- uses: actions/upload-artifact@v3
if: failure()
with:
name: e2e-test-videos
path: ./artifacts/

- name: Dump docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v2
4 changes: 4 additions & 0 deletions .github/workflows/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BITKIT_UPLOAD_STORE_FILE=debug.keystore
BITKIT_UPLOAD_STORE_PASSWORD=android
BITKIT_UPLOAD_KEY_ALIAS=androiddebugkey
BITKIT_UPLOAD_KEY_PASSWORD=android
14 changes: 14 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ android {
versionName "1.0"
multiDexEnabled true
missingDimensionStrategy 'react-native-camera', 'general'
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

signingConfigs {
Expand Down Expand Up @@ -110,11 +112,15 @@ android {
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
}
}
}

dependencies {
androidTestImplementation('com.wix:detox:+')
implementation 'com.google.android.material:material:1.3.0' // FIXME https://github.com/wix/Detox/issues/2846
implementation 'androidx.appcompat:appcompat:1.1.0'
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation files("../../node_modules/@synonymdev/react-native-ldk/android/libs/LDK-release.aar")
Expand All @@ -135,3 +141,11 @@ dependencies {
}

apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

// DETOX workaround
// https://github.com/wix/Detox/issues/3867#issuecomment-1540477784
configurations.all {
resolutionStrategy {
force 'androidx.test:core:1.5.0'
}
}
29 changes: 29 additions & 0 deletions android/app/src/androidTest/java/com/bitkit/DetoxTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.bitkit;

import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);

@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);

Detox.runTests(mActivityRule, detoxConfig);
}
}
3 changes: 2 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
Expand Down
7 changes: 7 additions & 0 deletions android/app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
8 changes: 7 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
minSdkVersion = 24
compileSdkVersion = 33
targetSdkVersion = 33
kotlin_version = '1.8.0'
kotlin_version = "1.8.21"
ndkVersion = "25.2.9519653"
}
repositories {
Expand All @@ -30,3 +30,9 @@ subprojects {
}
}
}

allprojects {
repositories {
maven { url("$rootDir/../node_modules/detox/Detox-android") }
}
}
11 changes: 8 additions & 3 deletions e2e/channels.e2e.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BitcoinJsonRpc from 'bitcoin-json-rpc';
import jestExpect from 'expect';
import { device } from 'detox';

import initWaitForElectrumToSync from '../__tests__/utils/wait-for-electrum';
import {
Expand Down Expand Up @@ -126,7 +127,7 @@ d('LN Channel Onboarding', () => {
await expect(element(by.text('200 000'))).toBeVisible();

// Swipe to confirm (set x offset to avoid navigating back)
await element(by.id('GRAB')).swipe('right', 'slow', NaN, 0.8);
await element(by.id('GRAB')).swipe('right', 'slow', 0.9);
await waitFor(element(by.id('LightningSettingUp')))
.toBeVisible()
.withTimeout(10000);
Expand Down Expand Up @@ -156,7 +157,11 @@ d('LN Channel Onboarding', () => {
jestExpect(buttonEnabled2).toBe(false);

// go back and change to 2nd card
await element(by.id('NavigationBack')).atIndex(1).tap();
if (device.getPlatform() === 'ios') {
await element(by.id('NavigationBack')).atIndex(1).tap(); // ios
} else {
await element(by.id('NavigationBack')).atIndex(0).tap(); // android
}
await element(by.id('Barrel-medium')).tap();
await element(by.id('CustomSetupContinue')).tap();
await element(by.id('Barrel-medium')).tap();
Expand All @@ -178,7 +183,7 @@ d('LN Channel Onboarding', () => {
// await expect(element(by.text('1 week'))).toBeVisible();

// Swipe to confirm (set x offset to avoid navigating back)
await element(by.id('GRAB')).swipe('right', 'slow', NaN, 0.8);
await element(by.id('GRAB')).swipe('right', 'slow', 0.9);
await waitFor(element(by.id('LightningSettingUp')))
.toBeVisible()
.withTimeout(10000);
Expand Down
29 changes: 18 additions & 11 deletions e2e/lightning.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ d('Lightning', () => {
let { label: ldkNodeID } = await element(
by.id('LDKNodeID'),
).getAttributes();
await element(by.id('NavigationBack')).tap();
await element(by.id('NavigationBack')).atIndex(0).tap();
await sleep(100);

// connect to LND
await element(by.id('Channels')).tap();
Expand Down Expand Up @@ -139,16 +140,18 @@ d('Lightning', () => {

// check channel status
await sleep(500);
await element(by.id('NavigationBack')).tap();
await element(by.id('NavigationBack')).atIndex(0).tap();
await sleep(100);
await element(by.id('Channels')).tap();
await element(by.id('Channel')).atIndex(0).tap();
await expect(
element(by.id('MoneyPrimary').withAncestor(by.id('TotalSize'))),
).toHaveText('100 000');
await element(by.id('ChannelScrollView')).scrollTo('bottom');
await expect(element(by.id('IsReadyYes'))).toBeVisible();
await element(by.id('NavigationClose')).tap();
await element(by.id('NavigationClose')).atIndex(0).tap();

await sleep(500);
// send funds to LDK, 0 invoice
await element(by.id('Receive')).tap();
try {
Expand All @@ -171,6 +174,7 @@ d('Lightning', () => {
await element(by.id('Receive')).tap();
await element(by.id('SpecifyInvoiceButton')).tap();
await element(by.id('ReceiveNumberPadTextField')).tap();
await sleep(100);
await element(
by.id('N1').withAncestor(by.id('ReceiveNumberPad')),
).multiTap(3);
Expand Down Expand Up @@ -208,7 +212,7 @@ d('Lightning', () => {
by.id('N1').withAncestor(by.id('SendAmountNumberPad')),
).multiTap(3);
await element(by.id('ContinueAmount')).tap();
await element(by.id('GRAB')).swipe('right'); // Swipe to confirm
await element(by.id('GRAB')).swipe('right', 'slow', 0.95); // Swipe to confirm
await waitFor(element(by.id('SendSuccess')))
.toBeVisible()
.withTimeout(10000);
Expand All @@ -234,7 +238,8 @@ d('Lightning', () => {
await element(by.id('TagsAddSend')).tap(); // add tag
await element(by.id('TagInputSend')).typeText('stag');
await element(by.id('TagInputSend')).tapReturnKey();
await element(by.id('GRAB')).swipe('right'); // Swipe to confirm
await sleep(500); // wait for keyboard to close
await element(by.id('GRAB')).swipe('right', 'slow', 0.95); // Swipe to confirm
await waitFor(element(by.id('SendSuccess')))
.toBeVisible()
.withTimeout(10000);
Expand Down Expand Up @@ -338,7 +343,7 @@ d('Lightning', () => {
).getAttributes();
await element(by.id('SeedContaider')).swipe('down');
await sleep(1000); // animation
await element(by.id('NavigationClose')).tap();
await element(by.id('NavigationClose')).atIndex(0).tap();

await sleep(5000); // make sure everything is saved to cloud storage TODO: improve this
console.info('seed: ', seed);
Expand Down Expand Up @@ -391,6 +396,7 @@ d('Lightning', () => {
// check channel status
await element(by.id('Settings')).tap();
await element(by.id('AdvancedSettings')).tap();
await sleep(100);
await element(by.id('Channels')).tap();
await element(by.id('Channel')).atIndex(0).tap();
await element(by.id('ChannelScrollView')).scrollTo('bottom');
Expand All @@ -399,11 +405,12 @@ d('Lightning', () => {
// close channel
await element(by.id('CloseConnection')).tap();
await element(by.id('CloseConnectionButton')).tap();
await rpc.generateToAddress(6, await rpc.getNewAddress());
await waitForElectrum();
await expect(element(by.id('Channel')).atIndex(0)).not.toExist();
await element(by.id('NavigationBack')).tap();
await element(by.id('NavigationClose')).tap();
// FIXME: closing doesn't work, because channel is not ready yet
// await rpc.generateToAddress(6, await rpc.getNewAddress());
// await waitForElectrum();
// await expect(element(by.id('Channel')).atIndex(0)).not.toExist();
// await element(by.id('NavigationBack')).atIndex(0).tap();
// await element(by.id('NavigationClose')).atIndex(0).tap();

// TODO: for some reason this doen't work on github actions
// wait for onchain payment to arrive
Expand Down
Loading

0 comments on commit d404e5d

Please sign in to comment.