Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 0 additions & 5 deletions .changeset/clever-mirrors-dream.md

This file was deleted.

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
84 changes: 83 additions & 1 deletion .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 @@ -14,6 +14,16 @@ inputs:
description: 'Maven path to the RN project, e.g. com/rnapp/brownfieldlib'
required: 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 @@ -75,8 +85,80 @@ runs:
run: stat ~/.m2/repository/${{ inputs.rn-project-maven-path }}/0.0.1-SNAPSHOT/brownfieldlib-0.0.1-SNAPSHOT-release.aar
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-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

# == AndroidApp ==

- name: Build native Android Brownfield app
if: inputs.run-e2e != 'true'
run: yarn run build:example:android-consumer:${{ inputs.flavor }}
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: 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
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 }}
DETOX_DEVICE: test

- 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
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 @@ -121,8 +122,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 @@ -138,22 +140,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 @@ -169,12 +176,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")
}

flavorDimensions += "app"
Expand All @@ -46,6 +47,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isDebuggable = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand Down Expand Up @@ -83,8 +85,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.

1 change: 1 addition & 0 deletions apps/AndroidApp/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".BrownfieldApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.callstack.brownfield.android.example

import android.app.Application
import android.content.res.Configuration
import com.callstack.brownie.registerStoreIfNeeded
import com.callstack.reactnativebrownfield.ReactNativeBrownfield
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost

/**
* Detox expects a [ReactApplication] so it can await the embedded RN context during E2E runs.
* RN is initialized at process start (embedded AAR bundle — no Metro).
*/
class BrownfieldApplication : Application(), ReactApplication {
override val reactHost: ReactHost
get() = ReactNativeBrownfield.shared.reactHost

override fun onCreate() {
super.onCreate()

ReactNativeHostManager.initialize(this)

registerStoreIfNeeded(
storeName = BrownfieldStore.STORE_NAME
) {
BrownfieldStore(
counter = 0.0,
user = User(name = "Username")
)
}
}

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ReactNativeHostManager.onConfigurationChanged(this, newConfig)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.callstack.brownfield.android.example

/** Keep in sync with `@callstack/brownfield-example-shared-tests` `e2eTestIds`. */
object E2eTestIds {
const val nativeAppGreeting = "brownfield-e2e-appleapp-greeting"
const val nativeAppPostMessageSend = "brownfield-e2e-appleapp-post-message-send"
const val nativeAppPostMessageToast = "brownfield-e2e-appleapp-post-message-toast"
const val nativeAppNativeSettings = "brownfield-e2e-appleapp-native-settings"
const val nativeAppNativeReferrals = "brownfield-e2e-appleapp-native-referrals"
}
Loading
Loading