Skip to content

Commit e7c9cf8

Browse files
committed
feat: Android detox tests
1 parent dcbdc2c commit e7c9cf8

27 files changed

+417
-79
lines changed

.detoxrc.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// run iPhone 14 on local machine, iPhone 15 Pro on mac mini
22
const iOSDevice = process.env.MACMINI ? 'iPhone 15 Pro' : 'iPhone 14';
33

4+
const reversePorts = [3003, 8080, 8081, 9735, 10009, 28334, 28335, 28336, 39388, 43782, 60001];
5+
6+
/** @type {Detox.DetoxConfig} */
47
module.exports = {
58
testRunner: {
69
$0: 'jest',
@@ -28,12 +31,14 @@ module.exports = {
2831
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
2932
build:
3033
'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd .. ',
34+
reversePorts,
3135
},
3236
'android.release': {
3337
type: 'android.apk',
3438
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
3539
build:
3640
'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..',
41+
reversePorts,
3742
},
3843
},
3944
devices: {
@@ -46,7 +51,7 @@ module.exports = {
4651
emulator: {
4752
type: 'android.emulator',
4853
device: {
49-
avdName: 'Pixel_API_29_AOSP',
54+
avdName: 'Pixel_API_31_AOSP',
5055
},
5156
},
5257
},

.env.test.template

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ BACKUPS_SERVER_PUBKEY=0319c4ff23820afec0c79ce3a42031d7fef1dff78b7bdd69b5560684f3
2121
WEB_RELAY=https://webrelay.slashtags.to
2222

2323
# Blocktank
24-
BLOCKTANK_HOST=https://api1.blocktank.to/api
24+
BLOCKTANK_HOST=https://api.stag.blocktank.to
2525

2626
# Network
2727
ELECTRUM_BITCOIN_HOST=35.187.18.233

.github/workflows/e2e-android.yml

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
name: e2e-android
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.ref }}
9+
cancel-in-progress: true
10+
11+
env:
12+
E2E_TESTS: 1 # build without transform-remove-console babel plugin
13+
DEBUG: 'lnurl* lnurl server'
14+
15+
jobs:
16+
e2e:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 1
24+
25+
- name: Enable KVM group perms
26+
run: |
27+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
28+
sudo udevadm control --reload-rules
29+
sudo udevadm trigger --name-match=kvm
30+
31+
- name: Free Disk Space
32+
uses: jlumbroso/free-disk-space@main
33+
with:
34+
# this might remove tools that are actually needed,
35+
# if set to "true" but frees about 6 GB
36+
tool-cache: false
37+
android: false
38+
dotnet: true
39+
haskell: true
40+
large-packages: true
41+
docker-images: true
42+
swap-storage: true
43+
44+
- name: yarn and gradle caches in /mnt
45+
run: |
46+
rm -rf ~/.yarn
47+
rm -rf ~/.gradle
48+
sudo mkdir -p /mnt/.yarn
49+
sudo mkdir -p /mnt/.gradle
50+
sudo chown -R runner /mnt/.yarn
51+
sudo chown -R runner /mnt/.gradle
52+
ln -s /mnt/.yarn /home/runner/
53+
ln -s /mnt/.gradle /home/runner/
54+
55+
- name: Create artifacts directory on /mnt
56+
run: |
57+
sudo mkdir -p /mnt/artifacts
58+
sudo chown -R runner /mnt/artifacts
59+
60+
- name: Specify node version
61+
uses: actions/setup-node@v4
62+
with:
63+
node-version: 20
64+
65+
- name: Use gradle caches
66+
uses: actions/cache@v4
67+
with:
68+
path: |
69+
~/.gradle/caches
70+
~/.gradle/wrapper
71+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
72+
restore-keys: |
73+
${{ runner.os }}-gradle-
74+
75+
- name: Use yarn caches
76+
uses: actions/cache@v4
77+
with:
78+
path: ~/.yarn
79+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/package-lock.json') }}
80+
restore-keys: |
81+
${{ runner.os }}-yarn-
82+
83+
- name: Activate enviroment variables
84+
run: cp .env.test.template .env
85+
86+
- name: Yarn Install
87+
run: yarn || yarn
88+
env:
89+
HUSKY: 0
90+
91+
- name: Activate Gradle variables
92+
run: |
93+
cp .github/workflows/gradle.properties ~/.gradle/gradle.properties
94+
patch -p1 -i ./.github/workflows/react-native-quick-crypto.patch
95+
96+
- name: Use specific Java version for sdkmanager to work
97+
uses: actions/setup-java@v4
98+
with:
99+
distribution: 'temurin'
100+
java-version: '17'
101+
102+
- name: Build
103+
run: yarn e2e:build:android-release || yarn e2e:build:android-release
104+
105+
- name: Kill java processes
106+
run: pkill -9 -f java || true
107+
108+
- name: Run regtest setup
109+
run: |
110+
cd docker
111+
mkdir lnd && chmod 777 lnd
112+
docker-compose pull --quiet
113+
docker compose up -d
114+
115+
- name: Wait for electrum server and LND
116+
timeout-minutes: 10
117+
run: |
118+
while ! nc -z '127.0.0.1' 60001; do sleep 1; done
119+
while ! nc -z '127.0.0.1' 10009; do sleep 1; done
120+
sudo chmod -R 777 docker/lnd
121+
122+
- name: Test attempt 1
123+
continue-on-error: true
124+
id: test1
125+
uses: reactivecircus/android-emulator-runner@v2
126+
with:
127+
profile: 5.4in FWVGA # devices list: avdmanager list device
128+
api-level: 31
129+
avd-name: Pixel_API_31_AOSP
130+
force-avd-creation: false
131+
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
132+
arch: x86_64
133+
script: yarn e2e:test:android-release --record-videos all --record-logs all --take-screenshots all --headless -d 200000 --artifacts-location /mnt/artifacts
134+
135+
- name: Test attempt 2
136+
continue-on-error: true
137+
id: test2
138+
if: steps.test1.outcome != 'success'
139+
uses: reactivecircus/android-emulator-runner@v2
140+
with:
141+
profile: 5.4in FWVGA # devices list: avdmanager list device
142+
api-level: 31
143+
avd-name: Pixel_API_31_AOSP
144+
force-avd-creation: false
145+
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
146+
arch: x86_64
147+
script: yarn e2e:test:android-release --record-videos all --record-logs all --take-screenshots all --headless -d 200000 --artifacts-location /mnt/artifacts
148+
149+
- name: Test attempt 3
150+
continue-on-error: true
151+
id: test3
152+
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success'
153+
uses: reactivecircus/android-emulator-runner@v2
154+
with:
155+
profile: 5.4in FWVGA # devices list: avdmanager list device
156+
api-level: 31
157+
avd-name: Pixel_API_31_AOSP
158+
force-avd-creation: false
159+
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
160+
arch: x86_64
161+
script: yarn e2e:test:android-release --record-videos all --record-logs all --take-screenshots all --headless -d 200000 --artifacts-location /mnt/artifacts
162+
163+
- name: Restart docker before last attempt
164+
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success' && steps.test3.outcome != 'success'
165+
run: |
166+
cd docker && docker compose down -t 60 && docker compose up --quiet-pull -d && cd ..
167+
while ! nc -z '127.0.0.1' 60001; do sleep 1; done
168+
169+
- name: Test attempt 4
170+
if: steps.test1.outcome != 'success' && steps.test2.outcome != 'success' && steps.test3.outcome != 'success'
171+
uses: reactivecircus/android-emulator-runner@v2
172+
with:
173+
profile: 5.4in FWVGA # devices list: avdmanager list device
174+
api-level: 31
175+
avd-name: Pixel_API_31_AOSP
176+
force-avd-creation: false
177+
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
178+
arch: x86_64
179+
script: yarn e2e:test:android-release --record-videos all --record-logs all --take-screenshots all --headless -d 200000 --artifacts-location /mnt/artifacts
180+
181+
- uses: actions/upload-artifact@v4
182+
if: failure()
183+
with:
184+
name: e2e-test-videos
185+
path: /mnt/artifacts/
186+
187+
- name: Dump docker logs on failure
188+
if: failure()
189+
uses: jwalton/gh-docker-logs@v2

.github/workflows/e2e-ios-macmini.yml .github/workflows/e2e-ios.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: e2e-ios-macmini
1+
name: e2e-ios
22

33
on:
44
workflow_dispatch:

.github/workflows/gradle.properties

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
BITKIT_UPLOAD_STORE_FILE=debug.keystore
2+
BITKIT_UPLOAD_STORE_PASSWORD=android
3+
BITKIT_UPLOAD_KEY_ALIAS=androiddebugkey
4+
BITKIT_UPLOAD_KEY_PASSWORD=android
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
diff --git a/node_modules/react-native-quick-crypto/android/build.gradle b/node_modules/react-native-quick-crypto/android/build.gradle
2+
index 2ac6c0db..57afa566 100644
3+
--- a/node_modules/react-native-quick-crypto/android/build.gradle
4+
+++ b/node_modules/react-native-quick-crypto/android/build.gradle
5+
@@ -94,6 +94,8 @@ android {
6+
""
7+
]
8+
doNotStrip '**/*.so'
9+
+ pickFirst 'META-INF/com.android.tools/proguard/coroutines.pro'
10+
+ pickFirst 'META-INF/proguard/coroutines.pro'
11+
}
12+
13+
buildTypes {

android/app/build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ android {
8484
versionName "1.0.1"
8585
multiDexEnabled true
8686
missingDimensionStrategy 'react-native-camera', 'general'
87+
testBuildType System.getProperty('testBuildType', 'debug')
88+
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
8789
}
8890

8991
signingConfigs {
@@ -112,6 +114,7 @@ android {
112114
signingConfig signingConfigs.release
113115
minifyEnabled enableProguardInReleaseBuilds
114116
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
117+
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
115118
}
116119
}
117120
packagingOptions {
@@ -127,6 +130,7 @@ android {
127130
}
128131

129132
dependencies {
133+
androidTestImplementation('com.wix:detox:+')
130134
// The version of react-native is set by the React Native Gradle Plugin
131135
implementation("com.facebook.react:react-android")
132136
implementation files("../../node_modules/@synonymdev/react-native-ldk/android/libs/LDK-release.aar")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.bitkit;
2+
3+
import com.wix.detox.Detox;
4+
import com.wix.detox.config.DetoxConfig;
5+
6+
import org.junit.Rule;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
10+
import androidx.test.ext.junit.runners.AndroidJUnit4;
11+
import androidx.test.filters.LargeTest;
12+
import androidx.test.rule.ActivityTestRule;
13+
14+
@RunWith(AndroidJUnit4.class)
15+
@LargeTest
16+
public class DetoxTest {
17+
@Rule
18+
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
19+
20+
@Test
21+
public void runDetoxTests() {
22+
DetoxConfig detoxConfig = new DetoxConfig();
23+
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
24+
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
25+
detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
26+
27+
Detox.runTests(mActivityRule, detoxConfig);
28+
}
29+
}

android/app/src/main/AndroidManifest.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
android:roundIcon="@mipmap/ic_launcher_orange_round"
3131
android:allowBackup="false"
3232
android:usesCleartextTraffic="true"
33-
android:theme="@style/AppTheme">
33+
android:theme="@style/AppTheme"
34+
android:networkSecurityConfig="@xml/network_security_config">
3435
<activity
3536
android:name=".MainActivity"
3637
android:label="@string/app_name"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<network-security-config>
3+
<domain-config cleartextTrafficPermitted="true">
4+
<domain includeSubdomains="true">10.0.2.2</domain>
5+
<domain includeSubdomains="true">localhost</domain>
6+
<domain includeSubdomains="true">127.0.0.1</domain>
7+
</domain-config>
8+
</network-security-config>

android/build.gradle

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ buildscript {
77
compileSdkVersion = 34
88
targetSdkVersion = 34
99
kotlin_version = "1.8.0"
10+
kotlinVersion = "1.8.0"
1011
ndkVersion = "25.2.9519653"
1112
}
1213
repositories {
@@ -21,3 +22,9 @@ buildscript {
2122
}
2223

2324
apply plugin: "com.facebook.react.rootproject"
25+
26+
allprojects {
27+
repositories {
28+
maven { url("$rootDir/../node_modules/detox/Detox-android") }
29+
}
30+
}

e2e/backup.e2e.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,21 @@ d('Backup', () => {
7979
await element(by.id('ActivitySavings')).tap();
8080
await element(by.id('Activity-1')).tap();
8181
await element(by.id('ActivityTag')).tap();
82-
await element(by.id('TagInput')).replaceText(tag);
82+
await element(by.id('TagInput')).typeText(tag);
8383
await element(by.id('TagInput')).tapReturnKey();
8484
await sleep(200); // animation
85-
await element(by.id('NavigationClose')).tap();
85+
await element(by.id('NavigationClose')).atIndex(0).tap();
8686

8787
// change currency to GBP
8888
await element(by.id('TotalBalance')).tap(); // switch to local currency
8989
await element(by.id('Settings')).tap();
9090
await element(by.id('GeneralSettings')).tap();
9191
await element(by.id('CurrenciesSettings')).tap();
9292
await element(by.text('GBP (£)')).tap();
93-
await element(by.id('NavigationClose')).tap();
93+
await element(by.id('NavigationClose')).atIndex(0).tap();
9494

9595
// remove 2 default widgets, leave PriceWidget
96-
await element(by.id('WalletsScrollView')).scroll(100, 'down', NaN, 0.85);
96+
await element(by.id('WalletsScrollView')).scroll(200, 'down', NaN, 0.85);
9797
await element(by.id('WidgetsEdit')).tap();
9898
for (const w of ['HeadlinesWidget', 'BlocksWidget']) {
9999
await element(by.id('WidgetActionDelete').withAncestor(by.id(w))).tap();
@@ -119,7 +119,7 @@ d('Backup', () => {
119119

120120
await element(by.id('SeedContaider')).swipe('down');
121121
await sleep(200); // animation
122-
await element(by.id('NavigationClose')).tap();
122+
await element(by.id('NavigationClose')).atIndex(0).tap();
123123

124124
await sleep(5000); // make sure everything is saved to cloud storage TODO: improve this
125125

@@ -165,7 +165,7 @@ d('Backup', () => {
165165
await expect(
166166
element(by.id(`Tag-${tag}`).withAncestor(by.id('ActivityTags'))),
167167
).toBeVisible();
168-
await element(by.id('NavigationClose')).tap();
168+
await element(by.id('NavigationClose')).atIndex(0).tap();
169169

170170
// check widgets
171171
await element(by.id('WalletsScrollView')).scroll(300, 'down', NaN, 0.85);

0 commit comments

Comments
 (0)