Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b61c084
feat: add vanilla android e2e initial work
alpharius-ck Jun 17, 2026
2d24a85
Merge branch 'main' into e2e-for-android
alpharius-ck Jun 25, 2026
33e958c
feat: fix un support for node 20
alpharius-ck Jun 25, 2026
f806482
feat: add missing mjs
alpharius-ck Jun 25, 2026
f0f4952
feat: fix non platform runs
alpharius-ck Jun 25, 2026
69593ea
feat: fix failed android ci build
alpharius-ck Jun 25, 2026
4affd55
feat: fix android run emulator
alpharius-ck Jun 25, 2026
da8929c
feat: fix android vanilla
alpharius-ck Jun 25, 2026
36e2a92
feat: restore fix for ci
alpharius-ck Jun 25, 2026
39bfd53
feat: switch test
alpharius-ck Jun 25, 2026
c0eb8b5
feat: update vanilla
alpharius-ck Jun 25, 2026
3d61e94
feat: update vanilla
alpharius-ck Jun 25, 2026
fad4b44
feat: add new test ids and fix device pick for expo55
alpharius-ck Jun 25, 2026
ed25164
feat: fix build
alpharius-ck Jun 25, 2026
807d3ea
feat: fix build
alpharius-ck Jun 25, 2026
a2a1c07
feat: try waiting approach
alpharius-ck Jun 25, 2026
219ba95
feat: update tests
alpharius-ck Jun 26, 2026
be60ed2
feat: update tests
alpharius-ck Jun 26, 2026
a03e450
feat: more fixes for tests
alpharius-ck Jun 26, 2026
af9be05
feat: more fixes for tests
alpharius-ck Jun 26, 2026
9f2b269
feat: fixes for expo55
alpharius-ck Jun 26, 2026
2e650de
feat: fixes for expo55
alpharius-ck Jun 26, 2026
dfaae37
feat: fixes for expo55
alpharius-ck Jun 26, 2026
82b2a09
feat: fixes for expo55
alpharius-ck Jun 26, 2026
8131114
feat: fixes pip
alpharius-ck Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@callstack/brownfield-example-rn-app",
"@callstack/brownfield-example-expo-app-54",
"@callstack/brownfield-example-expo-app-55",
"@callstack/brownfield-example-shared-tests",
"@callstack/brownfield-gradle-plugin-react"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
Expand Down
146 changes: 144 additions & 2 deletions .github/actions/androidapp-road-test/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Android road test (selected RN app & AndroidApp)
description: Package the given RN app as AAR, publish to Maven Local, and build the corresponding AndroidApp flavor
description: Package the given RN app as AAR, publish to Maven Local, build the corresponding AndroidApp flavor, and optionally run Detox E2E

inputs:
flavor:
Expand All @@ -19,6 +19,16 @@ inputs:
required: false
default: 'true'

run-e2e:
description: 'Run Detox E2E after packaging (uses release APK with embedded JS bundle, no Metro)'
required: false
default: 'false'

e2e-artifact-name:
description: 'Name prefix for Detox artifacts uploaded on failure'
required: false
default: 'detox-androidapp'

runs:
using: composite
steps:
Expand Down Expand Up @@ -136,7 +146,24 @@ runs:
run: echo "::group::AndroidApp — assemble consumer app"
shell: bash

- name: Verify embedded JS bundle in release AAR (E2E)
if: inputs.run-e2e == 'true'
run: |
set -euo pipefail
AAR_PATH="${HOME}/.m2/repository/${{ inputs.rn-project-maven-path }}/0.0.1-SNAPSHOT/brownfieldlib-0.0.1-SNAPSHOT-${{ steps.aar-variants.outputs.release }}.aar"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
unzip -q "${AAR_PATH}" -d "${TMP_DIR}"
BUNDLE_PATH="$(find "${TMP_DIR}/assets" -name 'index.android.bundle' -print -quit)"
if [[ -z "${BUNDLE_PATH}" ]]; then
echo "error: index.android.bundle missing from ${AAR_PATH} — E2E needs the packaged AAR bundle, not Metro." >&2
exit 1
fi
echo "Embedded bundle OK: ${BUNDLE_PATH} ($(wc -c < "${BUNDLE_PATH}") bytes)"
shell: bash

- name: Build native Android Brownfield app
if: inputs.run-e2e != 'true'
run: yarn run build:example:android-consumer:${{ inputs.flavor }}
shell: bash

Expand All @@ -149,7 +176,7 @@ runs:
shell: bash

- name: Save Android ccache
if: steps.prepare-android.outputs.android-ccache-cache-hit != 'true'
if: steps.prepare-android.outputs.android-ccache-cache-hit != 'true' && inputs.run-e2e != 'true'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: .android_ccache
Expand All @@ -163,3 +190,118 @@ runs:
- name: '::endgroup:: Save ccache & summary'
run: echo "::endgroup::"
shell: bash

- name: Resolve AndroidApp E2E settings
if: inputs.run-e2e == 'true'
run: |
node <<'NODE'
const { getAndroidAppDetoxVariant } = require('./apps/brownfield-example-shared-tests/detox-androidapp-variants.cjs');
const variant = getAndroidAppDetoxVariant(process.env.ANDROIDAPP_VARIANT);
const append = (key, value) => {
const fs = require('node:fs');
fs.appendFileSync(process.env.GITHUB_ENV, `${key}=${value}\n`);
};
append('ANDROIDAPP_E2E_BUILD_SCRIPT', variant.e2eBuildScript);
append('ANDROIDAPP_E2E_TEST_SCRIPT', variant.e2eTestScript);
NODE
env:
ANDROIDAPP_VARIANT: ${{ inputs.flavor }}
shell: bash

- name: Install Detox Android artifacts
if: inputs.run-e2e == 'true'
run: node node_modules/detox/scripts/postinstall.js
working-directory: apps/AndroidApp
shell: bash

- name: Detox build (AndroidApp ${{ inputs.flavor }})
if: inputs.run-e2e == 'true'
run: yarn "$ANDROIDAPP_E2E_BUILD_SCRIPT"
working-directory: apps/AndroidApp
shell: bash

- name: Free workspace disk space before Detox emulator
if: inputs.run-e2e == 'true'
run: |
# Native Gradle/NDK outputs can leave too little room for the AVD userdata partition.
# Do not run jlumbroso/free-disk-space here — large-packages removal can uninstall
# libX11 and other libs the QEMU emulator needs on ubuntu-latest.
set -euo pipefail
ANDROID_DIR="${{ inputs.rn-project-path }}/android"
rm -rf "$ANDROID_DIR/build" "$ANDROID_DIR/.cxx" "$ANDROID_DIR/.gradle"
rm -rf apps/AndroidApp/.gradle apps/AndroidApp/build
rm -rf apps/AndroidApp/app/build/intermediates apps/AndroidApp/app/build/tmp
rm -rf "${HOME}/.m2/repository/${{ inputs.rn-project-maven-path }}"
rm -rf node_modules/.cache .turbo
rm -rf "${ANDROID_HOME:?}/ndk"
rm -rf "${HOME}/.gradle/caches/build-cache-1"
rm -rf "${HOME}/.gradle/caches/transforms-3"
df -h .
shell: bash

- name: Install Android emulator runtime libraries
if: inputs.run-e2e == 'true'
run: |
# free-disk-space (prepare-android) can remove libs the QEMU emulator needs,
# even with -no-audio (libpulse) and headless CI (libgl1, libxkbfile1, libX11-xcb).
sudo apt-get update
sudo apt-get install -y \
libpulse0 \
libgl1 \
libxkbfile1 \
libx11-6 \
libx11-xcb1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2
shell: bash

- name: Enable KVM for Android emulator
if: inputs.run-e2e == 'true'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -l /dev/kvm
shell: bash

- name: Detox test (AndroidApp ${{ inputs.flavor }})
if: inputs.run-e2e == 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
profile: pixel_6
avd-name: test
disable-animations: true
emulator-boot-timeout: 900
pre-emulator-launch-script: df -h .
# Do not set disk-size — a large userdata partition fails when the runner is
# low on disk after Gradle/NDK builds (see ReactiveCircus/android-emulator-runner#455).
# Do not pass partial emulator-options — they replace the action defaults
# (-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim).
# Omitting them keeps headless/software-GPU settings required on ubuntu-latest.
script: |
bash apps/brownfield-example-shared-tests/scripts/prepare-android-emulator-for-detox.sh
cd apps/AndroidApp
yarn "$ANDROIDAPP_E2E_TEST_SCRIPT"
env:
ANDROIDAPP_E2E_TEST_SCRIPT: ${{ env.ANDROIDAPP_E2E_TEST_SCRIPT }}

- name: Save Android ccache (after E2E)
if: inputs.run-e2e == 'true' && steps.prepare-android.outputs.android-ccache-cache-hit != 'true'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: .android_ccache
key: ${{ steps.prepare-android.outputs.android-ccache-cache-primary-key }}

- name: Upload Detox artifacts on failure
if: failure() && inputs.run-e2e == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ inputs.e2e-artifact-name }}-${{ inputs.flavor }}-android
path: apps/AndroidApp/artifacts
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion .github/actions/prepare-android/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ runs:
haskell: true
large-packages: true
docker-images: true
swap-storage: false
swap-storage: true

- name: Install Android NDK required by Expo
run: |
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- 'apps/brownfield-example-shared-tests/**'
androidapp:
- 'apps/AndroidApp/**'
- 'apps/brownfield-example-shared-tests/**'
appleapp:
- 'apps/AppleApp/**'
- 'apps/brownfield-example-shared-tests/**'
Expand Down Expand Up @@ -124,8 +125,9 @@ jobs:
swift test --scratch-path "$RUNNER_TEMP/swift-cache/swiftpm"

android-androidapp-expo:
name: Android road test (AndroidApp - Expo ${{ matrix.version }})
name: Android road test${{ matrix.run-e2e == 'true' && ' & E2E' || '' }} (AndroidApp - Expo ${{ matrix.version }})
runs-on: ubuntu-latest
timeout-minutes: ${{ matrix.run-e2e == 'true' && 90 || 60 }}
needs: [filter, build-lint]
if: |
always() &&
Expand All @@ -141,22 +143,27 @@ jobs:
matrix:
include:
- version: '54'
run-e2e: 'false'
- version: '55'
run-e2e: 'true'

steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Run RNApp -> AndroidApp road test (Expo ${{ matrix.version }})
- name: Run ExpoApp -> AndroidApp road test${{ matrix.run-e2e == 'true' && ' & Detox E2E' || '' }} (Expo ${{ matrix.version }})
uses: ./.github/actions/androidapp-road-test
with:
flavor: expo${{ matrix.version }}
rn-project-path: apps/ExpoApp${{ matrix.version }}
rn-project-maven-path: com/callstack/rnbrownfield/demo/expoapp${{ matrix.version }}/brownfieldlib
run-e2e: ${{ matrix.run-e2e }}
e2e-artifact-name: detox-androidapp-expo${{ matrix.version }}

android-androidapp-vanilla:
name: Android road test (AndroidApp - Vanilla)
name: Android road test & E2E (AndroidApp - Vanilla)
runs-on: ubuntu-latest
timeout-minutes: 90
needs: [filter, build-lint]
if: |
always() &&
Expand All @@ -172,12 +179,14 @@ jobs:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Run RNApp -> AndroidApp road test (Vanilla)
- name: Run RNApp -> AndroidApp road test & Detox E2E (Vanilla)
uses: ./.github/actions/androidapp-road-test
with:
flavor: vanilla
rn-project-path: apps/RNApp
rn-project-maven-path: com/rnapp/brownfieldlib
run-e2e: 'true'
e2e-artifact-name: detox-androidapp-vanilla

ios-appleapp-vanilla:
name: iOS road test & E2E (AppleApp - Vanilla)
Expand Down
8 changes: 8 additions & 0 deletions apps/AndroidApp/.detoxrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const {
createAndroidAppEmulatorReleaseDetoxConfig,
} = require('../brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs');

/** @type {import('detox').DetoxConfig} */
module.exports = createAndroidAppEmulatorReleaseDetoxConfig({
gradleFlavor: 'vanilla',
});
10 changes: 10 additions & 0 deletions apps/AndroidApp/.detoxrc.expo55.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const {
createAndroidAppEmulatorReleaseDetoxConfig,
} = require('../brownfield-example-shared-tests/detox-rc-androidapp-emulator-release.cjs');

/** @type {import('detox').DetoxConfig} */
module.exports = createAndroidAppEmulatorReleaseDetoxConfig({
gradleFlavor: 'expo55',
detoxConfiguration: 'android.emu.release.expo55',
jestConfigPath: 'e2e/jest.config.expo55.cjs',
});
8 changes: 8 additions & 0 deletions apps/AndroidApp/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testBuildType = System.getProperty("testBuildType", "debug")
missingDimensionStrategy("env", "dev")
}

Expand All @@ -47,6 +48,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isDebuggable = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand Down Expand Up @@ -84,8 +86,14 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation("com.wix:detox:+")
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
// Compose ↔ Espresso bridge (ComposeIdlingResource / EspressoLink) for Detox.
androidTestImplementation("androidx.compose.ui:ui-test")
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
androidTestImplementation(libs.androidx.compose.ui.test.manifest)
// Release Detox E2E: expose Compose semantics to Espresso (debugImplementation is not on release APKs).
releaseImplementation(libs.androidx.compose.ui.test.manifest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.callstack.brownfield.android.example

import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.test.rule.ActivityTestRule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule

/**
* Connects Jetpack Compose semantics to Espresso's idling/sync layer so Detox can
* interact with the native Compose shell while RN runs in [ReactNativeFragment].
*
* Uses [createEmptyComposeRule] because [MainActivity] already hosts Compose content;
* Detox owns activity launch via [ActivityTestRule].
*/
object ComposeDetoxBridge {
fun emptyComposeRule(): ComposeTestRule = createEmptyComposeRule()

fun ruleChain(
composeRule: ComposeTestRule,
activityRule: ActivityTestRule<*>,
): TestRule = RuleChain.outerRule(composeRule).around(activityRule)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.callstack.brownfield.android.example

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import com.wix.detox.Detox
import com.wix.detox.config.DetoxConfig
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@LargeTest
class DetoxTest {
private val activityRule = ActivityTestRule(MainActivity::class.java, false, false)
private val composeRule = ComposeDetoxBridge.emptyComposeRule()

@get:Rule
val ruleChain = ComposeDetoxBridge.ruleChain(composeRule, activityRule)

@Test
fun runDetoxTests() {
val detoxConfig = DetoxConfig().apply {
rnContextLoadTimeoutSec = 120
}
Detox.runTests(activityRule, detoxConfig)
}
}

This file was deleted.

Loading
Loading