From c9161add35b263c0e23ec8002eec178432496686 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Fri, 19 Jun 2026 16:48:31 +0200 Subject: [PATCH 01/21] fix(test): stabilize e2e tests --- .../e2e/appleAppDetoxUtils.cjs | 145 +++++++++++------- .../e2e/createDetoxJestConfig.cjs | 4 +- .../e2e/detoxTiming.cjs | 21 +++ .../e2e/detoxUtils.cjs | 20 ++- 4 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 apps/brownfield-example-shared-tests/e2e/detoxTiming.cjs diff --git a/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs b/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs index 36625217..9366f87a 100644 --- a/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs +++ b/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs @@ -1,10 +1,15 @@ -const { device, element, by, waitFor } = require('detox'); +const { device, element, by } = require('detox'); const { brownfieldE2eTestIds: ids } = require('@callstack/brownfield-example-shared-tests/e2e/e2eTestIds'); +const { DETOX_TIMING } = require('@callstack/brownfield-example-shared-tests/e2e/detoxTiming.cjs'); const { assertDetoxTextMatches, + reloadReactNativeIgnoringSync, waitForVisibleIgnoringSync, } = require('@callstack/brownfield-example-shared-tests/e2e/detoxUtils'); +const EXPO_HOME_TAB = by.label('Home'); +const EXPO_WELCOME_TITLE = by.text(/Welcome to\s+Expo\s+55/); + const detoxLaunchArgs = { BrownfieldPreferEmbeddedBundleInDebug: 'YES', DetoxE2E: 'YES', @@ -26,7 +31,7 @@ async function scrollToEmbeddedRnExpo() { try { await scrollView.scrollTo('bottom'); } catch { - await element(by.label('Home')).swipe('up', 'fast', 0.85); + await element(EXPO_HOME_TAB).atIndex(0).swipe('up', 'fast', 0.85); } } } @@ -43,98 +48,124 @@ async function scrollToNativeShellExpo() { try { await element(by.type('UIScrollView')).atIndex(0).scrollTo('top'); } catch { - await element(by.label('Home')).swipe('down', 'fast', 0.85); + await element(EXPO_HOME_TAB).atIndex(0).swipe('down', 'fast', 0.85); } } -async function waitForAppleAppReadyVanilla() { - const rnHomeMatcher = by.id(ids.rnAppHome); - const rnHome = element(rnHomeMatcher); +async function waitForEmbeddedMatcher(matcher, index = 0) { try { - await waitForVisibleIgnoringSync(rnHomeMatcher, 30000); + await scrollToEmbeddedRnExpo(); } catch { - // Some CI runs start with an unmounted RN surface; one reload usually recovers. - await device.reloadReactNative(); - try { - await waitForVisibleIgnoringSync(rnHomeMatcher, 30000); - return; - } catch { - // Embedded RN may be off-screen in the native scroll view. - } - await device.disableSynchronization(); - try { - await scrollToEmbeddedRnVanilla(); - await waitFor(rnHome).toBeVisible().withTimeout(20000); - } finally { - await device.enableSynchronization(); - } + // Embedded RN may already be on screen. } -} -async function waitForAppleAppReadyExpo() { - const homeTab = by.label('Home'); - const homeElement = () => element(homeTab).atIndex(0); - const welcomeTitle = by.text(/Welcome to\s+Expo\s+55/); try { - await waitForVisibleIgnoringSync(homeTab, 30000, 0); + await waitForVisibleIgnoringSync( + matcher, + DETOX_TIMING.VISIBILITY_TIMEOUT_MS, + index + ); return; } catch { - // Some CI runs start with an unmounted RN surface; one reload usually recovers. + // Continue with reload recovery. } - await device.reloadReactNative(); + await reloadReactNativeIgnoringSync(); + try { - await waitForVisibleIgnoringSync(homeTab, 30000, 0); + await scrollToEmbeddedRnExpo(); + } catch { + // Continue polling visibility. + } + + await waitForVisibleIgnoringSync( + matcher, + DETOX_TIMING.VISIBILITY_TIMEOUT_MS, + index + ); +} + +async function waitForAppleAppReadyVanilla() { + const rnHomeMatcher = by.id(ids.rnAppHome); + + try { + await scrollToEmbeddedRnVanilla(); + } catch { + // Continue polling visibility. + } + + try { + await waitForVisibleIgnoringSync( + rnHomeMatcher, + DETOX_TIMING.VISIBILITY_TIMEOUT_MS + ); return; } catch { - // Embedded RN may be off-screen in the native scroll view. + // Continue with reload recovery. } - await device.disableSynchronization(); + await reloadReactNativeIgnoringSync(); + try { - for (let attempt = 0; attempt < 3; attempt += 1) { - try { - await scrollToEmbeddedRnExpo(); - } catch {} - - try { - await waitFor(homeElement()).toBeVisible().withTimeout(10000); - return; - } catch {} - - try { - await waitFor(element(welcomeTitle)).toBeVisible().withTimeout(10000); - return; - } catch {} - } + await scrollToEmbeddedRnVanilla(); + } catch { + // Continue polling visibility. + } + + await waitForVisibleIgnoringSync( + rnHomeMatcher, + DETOX_TIMING.VISIBILITY_TIMEOUT_MS + ); +} - await waitFor(homeElement()).toBeVisible().withTimeout(10000); - } finally { - await device.enableSynchronization(); +async function waitForAppleAppReadyExpo() { + try { + await waitForEmbeddedMatcher(EXPO_HOME_TAB, 0); + return; + } catch { + // Home tab can be off-screen or slow; welcome title is a reliable fallback. } + + await waitForEmbeddedMatcher(EXPO_WELCOME_TITLE); } async function openPostMessageTabExpo() { - await waitForVisibleIgnoringSync(by.label('postMessage API'), 30000, 0); + await scrollToEmbeddedRnExpo(); + await waitForVisibleIgnoringSync( + by.label('postMessage API'), + DETOX_TIMING.VISIBILITY_TIMEOUT_MS, + 0 + ); await element(by.label('postMessage API')).atIndex(0).tap(); - await waitForVisibleIgnoringSync(by.id(ids.sendMessageToNative), 30000); + await waitForVisibleIgnoringSync( + by.id(ids.sendMessageToNative), + DETOX_TIMING.VISIBILITY_TIMEOUT_MS + ); } async function sendPostMessageToNativeAndWaitForToast(rnMessagePattern) { - await waitForVisibleIgnoringSync(by.id(ids.sendMessageToNative), 30000); + await waitForVisibleIgnoringSync( + by.id(ids.sendMessageToNative), + DETOX_TIMING.VISIBILITY_TIMEOUT_MS + ); await element(by.id(ids.sendMessageToNative)).tap(); const bubble = element(by.id(ids.rnPostMessageText)).atIndex(0); - const deadline = Date.now() + 15000; + const deadline = Date.now() + DETOX_TIMING.POST_MESSAGE_BUBBLE_TIMEOUT_MS; while (Date.now() < deadline) { try { await assertDetoxTextMatches(bubble, rnMessagePattern); break; } catch { - await new Promise((resolve) => setTimeout(resolve, 200)); + await new Promise((resolve) => + setTimeout(resolve, DETOX_TIMING.POLL_INTERVAL_MS) + ); } } await assertDetoxTextMatches(bubble, rnMessagePattern); - await waitForVisibleIgnoringSync(by.id(ids.appleAppPostMessageToast), 10000); + await waitForVisibleIgnoringSync( + by.id(ids.appleAppPostMessageToast), + DETOX_TIMING.TOAST_VISIBILITY_TIMEOUT_MS + ); } module.exports = { diff --git a/apps/brownfield-example-shared-tests/e2e/createDetoxJestConfig.cjs b/apps/brownfield-example-shared-tests/e2e/createDetoxJestConfig.cjs index f7e99e2b..ca3018c2 100644 --- a/apps/brownfield-example-shared-tests/e2e/createDetoxJestConfig.cjs +++ b/apps/brownfield-example-shared-tests/e2e/createDetoxJestConfig.cjs @@ -1,6 +1,7 @@ 'use strict'; const path = require('node:path'); +const { DETOX_TIMING } = require('./detoxTiming.cjs'); /** * Shared Detox Jest config for brownfield example apps (RNApp, ExpoApp54, ExpoApp55, AppleApp). @@ -20,8 +21,7 @@ function createDetoxJestConfig({ e2eDir, testMatch }) { roots: [appRoot, sharedTestsRoot], // Shared E2E files live under brownfield-example-shared-tests; resolve host-app deps (detox) from here. modulePaths: [path.join(appRoot, 'node_modules')], - // beforeEach relaunches the app and waits for the embedded RN surface (up to ~80s on slow CI). - testTimeout: 300000, + testTimeout: DETOX_TIMING.TEST_TIMEOUT_MS, verbose: true, reporters: ['detox/runners/jest/reporter'], globalSetup: 'detox/runners/jest/globalSetup', diff --git a/apps/brownfield-example-shared-tests/e2e/detoxTiming.cjs b/apps/brownfield-example-shared-tests/e2e/detoxTiming.cjs new file mode 100644 index 00000000..efc79b16 --- /dev/null +++ b/apps/brownfield-example-shared-tests/e2e/detoxTiming.cjs @@ -0,0 +1,21 @@ +'use strict'; + +/** Shared Detox timing constants — keep poll intervals and budgets centralized. */ +const DETOX_TIMING = { + /** Poll interval for the visibility of RN surfaces. */ + POLL_INTERVAL_MS: 250, + + /** Timeout for the visibility of RN surfaces. */ + VISIBILITY_TIMEOUT_MS: 30_000, + + /** Timeout for the visibility of post message bubbles. */ + POST_MESSAGE_BUBBLE_TIMEOUT_MS: 15_000, + + /** Timeout for the visibility of toasts. */ + TOAST_VISIBILITY_TIMEOUT_MS: 15_000, + + /** Timeout for the entire E2E test suite (detox + Jest + Maestro). */ + TEST_TIMEOUT_MS: 300_000, +}; + +module.exports = { DETOX_TIMING }; diff --git a/apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs b/apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs index f57d8dc7..c8d16be4 100644 --- a/apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs +++ b/apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs @@ -1,5 +1,6 @@ const assert = require('node:assert/strict'); const { device, element, waitFor, expect: detoxExpect } = require('detox'); +const { DETOX_TIMING } = require('./detoxTiming.cjs'); function detoxAttrsText(attrs) { if (!attrs || typeof attrs !== 'object') { @@ -29,6 +30,20 @@ async function configureDetoxForBrownfieldIos() { ]); } +/** + * Reload RN without waiting for the app to become idle. RN debug surfaces (and + * animations) keep the run loop busy, so reload with sync enabled can hang until + * the Jest timeout. + */ +async function reloadReactNativeIgnoringSync() { + await device.disableSynchronization(); + try { + await device.reloadReactNative(); + } finally { + await device.enableSynchronization(); + } +} + async function waitForVisible(matcher, timeoutMs = 20000) { await waitFor(element(matcher)).toBeVisible().withTimeout(timeoutMs); } @@ -47,7 +62,9 @@ async function waitForVisibleIgnoringSync(matcher, timeoutMs = 20000, index = 0) await detoxExpect(target()).toBeVisible(); return; } catch { - await new Promise((resolve) => setTimeout(resolve, 200)); + await new Promise((resolve) => + setTimeout(resolve, DETOX_TIMING.POLL_INTERVAL_MS) + ); } } await detoxExpect(target()).toBeVisible(); @@ -60,6 +77,7 @@ module.exports = { detoxAttrsText, assertDetoxTextMatches, configureDetoxForBrownfieldIos, + reloadReactNativeIgnoringSync, waitForVisible, waitForVisibleIgnoringSync, }; From 426c69e6c2b3d92399edf7f338ba368f03166989 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Fri, 19 Jun 2026 16:57:56 +0200 Subject: [PATCH 02/21] fix: relative import in appleAppDetoxUtils --- .../e2e/appleAppDetoxUtils.cjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs b/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs index 9366f87a..663aaba8 100644 --- a/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs +++ b/apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs @@ -1,6 +1,8 @@ -const { device, element, by } = require('detox'); -const { brownfieldE2eTestIds: ids } = require('@callstack/brownfield-example-shared-tests/e2e/e2eTestIds'); -const { DETOX_TIMING } = require('@callstack/brownfield-example-shared-tests/e2e/detoxTiming.cjs'); +const { element, by } = require('detox'); +const { + brownfieldE2eTestIds: ids, +} = require('@callstack/brownfield-example-shared-tests/e2e/e2eTestIds'); +const { DETOX_TIMING } = require('./detoxTiming.cjs'); const { assertDetoxTextMatches, reloadReactNativeIgnoringSync, From 19bd2f0ba604d50321bf4b2f848d5f040faf8676 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Fri, 19 Jun 2026 17:03:44 +0200 Subject: [PATCH 03/21] ci: build only x86_64 android React Native apps to speed up --- .github/actions/prepare-android/action.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/prepare-android/action.yml b/.github/actions/prepare-android/action.yml index 44424131..f8e06c5c 100644 --- a/.github/actions/prepare-android/action.yml +++ b/.github/actions/prepare-android/action.yml @@ -36,6 +36,11 @@ runs: # and captures Build Scan links. Do not combine with actions/cache on ~/.gradle. workflow-job-context: ${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }} + # ubuntu-latest is x86_64; match the emulator ABI so CI only compiles one JNI arch. + - name: Limit native builds to runner emulator ABI + run: echo "ORG_GRADLE_PROJECT_reactNativeArchitectures=x86_64" >> "$GITHUB_ENV" + shell: bash + - name: Free Disk Space (Ubuntu) if: inputs.free-disk-space == 'true' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 From 012a510b1b418697a5bbe0b004dac0ddb250aa88 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 14:34:31 +0200 Subject: [PATCH 04/21] ci: ccache for android cxx builds --- .../actions/androidapp-road-test/action.yml | 32 +++++++++++++------ .github/actions/ccache-summary/action.yml | 7 +++- .github/actions/prepare-android/action.yml | 27 ++++++++++++++++ .gitignore | 3 ++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index 3acb2ea0..476b5ad4 100644 --- a/.github/actions/androidapp-road-test/action.yml +++ b/.github/actions/androidapp-road-test/action.yml @@ -31,6 +31,7 @@ runs: uses: ./.github/actions/prepare-android with: gradle-workflow-job-context: ${{ inputs.flavor }} + rn-project-path: ${{ inputs.rn-project-path }} # == Brownfield Gradle Plugin == - name: Publish Brownfield Gradle Plugin to Maven Local @@ -73,16 +74,24 @@ runs: run: stat ~/.m2/repository/${{ inputs.rn-project-maven-path }}/0.0.1-SNAPSHOT/brownfieldlib-0.0.1-SNAPSHOT-release.aar shell: bash - # clean up build artifacts to ensure no ENOSPC - - name: Clean up local build artifacts + - name: Save Android ccache after native AAR build + uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + with: + path: .android_ccache + key: ${{ runner.os }}-android-ccache-${{ inputs.flavor }}-x86_64-${{ hashFiles('yarn.lock', '**/gradle-wrapper.properties', format('{0}/package.json', inputs.rn-project-path)) }} + + # Compile results are in .android_ccache (saved above + at job end). Drop .cxx CMake trees + # and other RN android outputs to avoid ENOSPC before AndroidApp assemble. + - name: Clean up local RN Android build outputs run: | - rm -rf ${{ inputs.rn-project-path }}/android/build - rm -rf ${{ inputs.rn-project-path }}/android/.cxx - rm -rf ${{ inputs.rn-project-path }}/android/.gradle - rm -rf ${{ inputs.rn-project-path }}/android/app/build - rm -rf ${{ inputs.rn-project-path }}/android/app/.cxx - rm -rf ${{ inputs.rn-project-path }}/android/app/.gradle - rm -rf ${{ inputs.rn-project-path }}/android/app/build + ANDROID_DIR="${{ inputs.rn-project-path }}/android" + rm -rf "$ANDROID_DIR/build" + rm -rf "$ANDROID_DIR/.cxx" + rm -rf "$ANDROID_DIR/.gradle" + rm -rf "$ANDROID_DIR/app/build" + rm -rf "$ANDROID_DIR/app/.cxx" + rm -rf "$ANDROID_DIR/app/.gradle" + find "$ANDROID_DIR" -maxdepth 2 -name '.cxx' -type d -prune -exec rm -rf {} + shell: bash # == AndroidApp == @@ -90,3 +99,8 @@ runs: - name: Build native Android Brownfield app run: yarn run build:example:android-consumer:${{ inputs.flavor }} shell: bash + + - name: Log Android ccache stats + uses: ./.github/actions/ccache-summary + with: + name: Android road test (${{ inputs.flavor }}) diff --git a/.github/actions/ccache-summary/action.yml b/.github/actions/ccache-summary/action.yml index 52df8958..44e1744e 100644 --- a/.github/actions/ccache-summary/action.yml +++ b/.github/actions/ccache-summary/action.yml @@ -10,5 +10,10 @@ runs: using: composite steps: - name: Log ccache stats - run: ccache -s > $GITHUB_STEP_SUMMARY + run: | + { + echo "### ${{ inputs.name }}" + echo + ccache -s + } >> "$GITHUB_STEP_SUMMARY" shell: bash diff --git a/.github/actions/prepare-android/action.yml b/.github/actions/prepare-android/action.yml index f8e06c5c..49539f90 100644 --- a/.github/actions/prepare-android/action.yml +++ b/.github/actions/prepare-android/action.yml @@ -17,6 +17,11 @@ inputs: required: false default: '' + rn-project-path: + description: 'Optional RN app path for native ccache key segmentation (e.g. apps/ExpoApp55)' + required: false + default: '' + runs: using: composite steps: @@ -60,6 +65,28 @@ runs: test -d "$ANDROID_HOME/ndk/27.0.12077973" shell: bash + - name: Install ccache + run: sudo apt-get update && sudo apt-get install -y ccache + shell: bash + + - name: Configure Android ccache + run: | + mkdir -p "${GITHUB_WORKSPACE}/.android_ccache" + echo "compiler_check = content" > "${GITHUB_WORKSPACE}/.android_ccache.conf" + echo "CCACHE_CONFIGPATH=${GITHUB_WORKSPACE}/.android_ccache.conf" >> "$GITHUB_ENV" + echo "CCACHE_DIR=${GITHUB_WORKSPACE}/.android_ccache" >> "$GITHUB_ENV" + echo "CCACHE_BASEDIR=${GITHUB_WORKSPACE}" >> "$GITHUB_ENV" + echo "CCACHE_COMPRESS=1" >> "$GITHUB_ENV" + shell: bash + + - name: Restore Android ccache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + with: + path: .android_ccache + key: ${{ runner.os }}-android-ccache-${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }}-x86_64-${{ hashFiles('yarn.lock', '**/gradle-wrapper.properties', inputs.rn-project-path != '' && format('{0}/package.json', inputs.rn-project-path) || 'package.json') }} + restore-keys: | + ${{ runner.os }}-android-ccache-${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }}- + - name: Build packages if: inputs.run-yarn-build == 'true' run: yarn build diff --git a/.gitignore b/.gitignore index df918d12..a05d1915 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ local.properties *.iml *.hprof **/.cxx/ +.android_ccache/ +.android_ccache.conf +.ios_ccache/ *.keystore !debug.keystore .kotlin/ From 13b3ae5fb421f7beb2e2a89f31cdeb0a61354d09 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 15:26:57 +0200 Subject: [PATCH 05/21] ci: pin all actions to SHAs --- .github/actions/prepare-android/action.yml | 2 +- .github/actions/setup/action.yml | 2 +- .github/workflows/ci.yml | 3 +-- .github/workflows/expo-beta-road-test.yml | 2 +- .github/workflows/release-brownfield-gradle-plugin.yml | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/actions/prepare-android/action.yml b/.github/actions/prepare-android/action.yml index 49539f90..d4908de2 100644 --- a/.github/actions/prepare-android/action.yml +++ b/.github/actions/prepare-android/action.yml @@ -80,7 +80,7 @@ runs: shell: bash - name: Restore Android ccache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: .android_ccache key: ${{ runner.os }}-android-ccache-${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }}-x86_64-${{ hashFiles('yarn.lock', '**/gradle-wrapper.properties', inputs.rn-project-path != '' && format('{0}/package.json', inputs.rn-project-path) || 'package.json') }} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index b514b84d..6ce76f70 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -18,7 +18,7 @@ runs: - name: Restore Turbo cache if: inputs.restore-turbo-cache == 'true' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | .turbo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a64f3e5b..c3c68d2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Filter paths - uses: dorny/paths-filter@v3 + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: filter with: filters: | @@ -237,4 +237,3 @@ jobs: rn-project-path: apps/ExpoApp${{ matrix.version }} run-e2e: ${{ matrix.run-e2e }} e2e-artifact-name: detox-appleapp-expo${{ matrix.version }} - diff --git a/.github/workflows/expo-beta-road-test.yml b/.github/workflows/expo-beta-road-test.yml index 0b4dd671..8e7ab59e 100644 --- a/.github/workflows/expo-beta-road-test.yml +++ b/.github/workflows/expo-beta-road-test.yml @@ -148,7 +148,7 @@ jobs: printf '%s\n' '${{ needs.prepare.outputs.latest_version }}' > expo-beta-test-result/version.txt - name: Upload Expo beta success marker - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: expo-beta-tested-${{ needs.prepare.outputs.latest_version }} path: expo-beta-test-result/version.txt diff --git a/.github/workflows/release-brownfield-gradle-plugin.yml b/.github/workflows/release-brownfield-gradle-plugin.yml index 7cb2a4e6..12705508 100644 --- a/.github/workflows/release-brownfield-gradle-plugin.yml +++ b/.github/workflows/release-brownfield-gradle-plugin.yml @@ -148,7 +148,7 @@ jobs: path: gradle-plugins/react/brownfield/build/staging-deploy - name: Run JReleaser - uses: jreleaser/release-action@v2 + uses: jreleaser/release-action@90ac653bb9c79d11179e65d81499f3f34527dcd5 # v2.5.0 with: arguments: full-release env: From 19af8b32b366de24c78150d3c44243c9a4de9b16 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 15:29:01 +0200 Subject: [PATCH 06/21] ci: print GHA groups in composite actions for readability of logs --- .../actions/androidapp-road-test/action.yml | 54 +++++++++++++-- .github/actions/appleapp-road-test/action.yml | 66 +++++++++++++++++-- .github/actions/prepare-android/action.yml | 34 ++++++++++ .github/actions/prepare-ios/action.yml | 18 +++++ 4 files changed, 160 insertions(+), 12 deletions(-) diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index 476b5ad4..318eee0d 100644 --- a/.github/actions/androidapp-road-test/action.yml +++ b/.github/actions/androidapp-road-test/action.yml @@ -22,6 +22,12 @@ inputs: runs: using: composite steps: + - name: '::group:: Setup & prepare Android' + run: | + echo "::group::Setup & prepare Android" + echo "flavor=${{ inputs.flavor }} rn-project=${{ inputs.rn-project-path }}" + shell: bash + - name: Setup uses: ./.github/actions/setup with: @@ -33,13 +39,27 @@ runs: gradle-workflow-job-context: ${{ inputs.flavor }} rn-project-path: ${{ inputs.rn-project-path }} - # == Brownfield Gradle Plugin == + - name: '::endgroup:: Setup & prepare Android' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Brownfield Gradle plugin' + run: echo "::group::Brownfield Gradle plugin" + shell: bash + - name: Publish Brownfield Gradle Plugin to Maven Local run: | yarn run brownfield:plugin:publish:local shell: bash - # == RN app == + - name: '::endgroup:: Brownfield Gradle plugin' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: RN app — prebuild, package & publish AAR' + run: echo "::group::RN app — prebuild, package & publish AAR" + shell: bash + - name: Prebuild Expo app if: ${{ startsWith(inputs.flavor, 'expo') }} run: | @@ -74,14 +94,20 @@ runs: run: stat ~/.m2/repository/${{ inputs.rn-project-maven-path }}/0.0.1-SNAPSHOT/brownfieldlib-0.0.1-SNAPSHOT-release.aar shell: bash + - name: '::endgroup:: RN app — prebuild, package & publish AAR' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Save ccache & clean RN android outputs' + run: echo "::group::Save ccache & clean RN android outputs" + shell: bash + - name: Save Android ccache after native AAR build uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5 with: path: .android_ccache key: ${{ runner.os }}-android-ccache-${{ inputs.flavor }}-x86_64-${{ hashFiles('yarn.lock', '**/gradle-wrapper.properties', format('{0}/package.json', inputs.rn-project-path)) }} - # Compile results are in .android_ccache (saved above + at job end). Drop .cxx CMake trees - # and other RN android outputs to avoid ENOSPC before AndroidApp assemble. - name: Clean up local RN Android build outputs run: | ANDROID_DIR="${{ inputs.rn-project-path }}/android" @@ -94,13 +120,31 @@ runs: find "$ANDROID_DIR" -maxdepth 2 -name '.cxx' -type d -prune -exec rm -rf {} + shell: bash - # == AndroidApp == + - name: '::endgroup:: Save ccache & clean RN android outputs' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: AndroidApp — assemble consumer app' + run: echo "::group::AndroidApp — assemble consumer app" + shell: bash - name: Build native Android Brownfield app run: yarn run build:example:android-consumer:${{ inputs.flavor }} shell: bash + - name: '::endgroup:: AndroidApp — assemble consumer app' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Summary' + run: echo "::group::Summary" + shell: bash + - name: Log Android ccache stats uses: ./.github/actions/ccache-summary with: name: Android road test (${{ inputs.flavor }}) + + - name: '::endgroup:: Summary' + run: echo "::endgroup::" + shell: bash diff --git a/.github/actions/appleapp-road-test/action.yml b/.github/actions/appleapp-road-test/action.yml index 031b3854..1c92e342 100644 --- a/.github/actions/appleapp-road-test/action.yml +++ b/.github/actions/appleapp-road-test/action.yml @@ -28,6 +28,10 @@ inputs: runs: using: composite steps: + - name: '::group:: Setup & prepare iOS' + run: echo "::group::Setup & prepare iOS" + shell: bash + - name: Setup uses: ./.github/actions/setup with: @@ -36,6 +40,14 @@ runs: - name: Prepare iOS environment uses: ./.github/actions/prepare-ios + - name: '::endgroup:: Setup & prepare iOS' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: ccache & iOS build caches' + run: echo "::group::ccache & iOS build caches" + shell: bash + - name: Configure ccache environment run: | echo "USE_CCACHE=1" >> "$GITHUB_ENV" @@ -60,7 +72,7 @@ runs: shell: bash - name: Restore AppleApp ccache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | .ios_ccache @@ -68,10 +80,18 @@ runs: restore-keys: | ${{ runner.os }}-rnapp-appleapp-${{ inputs.variant }}-ios-ccache- - # == RN app == + - name: '::endgroup:: ccache & iOS build caches' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: RN app — pods & package iOS XCFramework' + run: | + echo "::group::RN app — pods & package iOS XCFramework" + echo "variant=${{ inputs.variant }} rn-project=${{ inputs.rn-project-path }}" + shell: bash - name: Restore Pods cache (RN ${{ inputs.variant }} app) - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ${{ inputs.rn-project-path }}/ios/Pods @@ -106,7 +126,7 @@ runs: shell: bash - name: Restore DerivedData cache (RN ${{ inputs.variant }} app) - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ inputs.rn-project-path }}/ios/build key: ${{ runner.os }}-ios-rnapp-${{ inputs.variant }}-derived-data-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.rn-project-path), format('{0}/ios/*.xcodeproj/project.pbxproj', inputs.rn-project-path)) }} @@ -119,7 +139,14 @@ runs: yarn run brownfield:package:ios shell: bash - # == AppleApp == + - name: '::endgroup:: RN app — pods & package iOS XCFramework' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: AppleApp — Release road test build' + if: inputs.run-e2e != 'true' + run: echo "::group::AppleApp — Release road test build" + shell: bash - name: Build Brownfield iOS native app (${{ inputs.variant }}) if: inputs.run-e2e != 'true' @@ -127,6 +154,16 @@ runs: yarn run build:example:ios-consumer:${{ inputs.variant }} shell: bash + - name: '::endgroup:: AppleApp — Release road test build' + if: inputs.run-e2e != 'true' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: AppleApp — Detox E2E' + if: inputs.run-e2e == 'true' + run: echo "::group::AppleApp — Detox E2E" + shell: bash + - name: Resolve AppleApp E2E settings if: inputs.run-e2e == 'true' run: | @@ -154,7 +191,7 @@ runs: - name: Restore Detox build cache (AppleApp) if: inputs.run-e2e == 'true' - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: apps/AppleApp/build key: ${{ runner.os }}-e2e-appleapp-${{ inputs.variant }}-build-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.rn-project-path), 'apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj', 'apps/brownfield-example-shared-tests/e2e/**') }} @@ -176,6 +213,7 @@ runs: - name: Verify embedded JS bundle in BrownfieldLib (E2E) if: inputs.run-e2e == 'true' run: | + echo "::group::Verify embedded JS bundle" set -euo pipefail PRODUCTS_DIR="apps/AppleApp/build/Build/Products/${APPLEAPP_E2E_CONFIGURATION}-iphonesimulator" APP_PATH="$(find "$PRODUCTS_DIR" -maxdepth 1 -name '*.app' -print -quit)" @@ -197,13 +235,16 @@ runs: fi echo "App executable OK: $EXECUTABLE_PATH ($(wc -c < "$EXECUTABLE_PATH") bytes)" echo "Embedded bundle OK: $BUNDLE_PATH ($(wc -c < "$BUNDLE_PATH") bytes)" + echo "::endgroup::" shell: bash - name: Detox test (AppleApp ${{ inputs.variant }}) if: inputs.run-e2e == 'true' run: | + echo "::group::Detox test" rm -rf e2e-artifacts yarn "$APPLEAPP_E2E_TEST_SCRIPT" + echo "::endgroup::" working-directory: apps/AppleApp shell: bash @@ -216,9 +257,20 @@ runs: if-no-files-found: warn retention-days: 5 - # ============== + - name: '::endgroup:: AppleApp — Detox E2E' + if: always() && inputs.run-e2e == 'true' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Summary' + run: echo "::group::Summary" + shell: bash - name: Log ccache stats uses: ./.github/actions/ccache-summary with: name: RN ${{ inputs.variant }} app & AppleApp + + - name: '::endgroup:: Summary' + run: echo "::endgroup::" + shell: bash diff --git a/.github/actions/prepare-android/action.yml b/.github/actions/prepare-android/action.yml index d4908de2..3fd5cb20 100644 --- a/.github/actions/prepare-android/action.yml +++ b/.github/actions/prepare-android/action.yml @@ -25,6 +25,10 @@ inputs: runs: using: composite steps: + - name: '::group:: Gradle & Java' + run: echo "::group::Gradle & Java" + shell: bash + - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@6f229686ee4375cc4a86b2514c89bac4930e82c4 # v5 @@ -46,6 +50,14 @@ runs: run: echo "ORG_GRADLE_PROJECT_reactNativeArchitectures=x86_64" >> "$GITHUB_ENV" shell: bash + - name: '::endgroup:: Gradle & Java' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Disk space & Android NDK' + run: echo "::group::Disk space & Android NDK" + shell: bash + - name: Free Disk Space (Ubuntu) if: inputs.free-disk-space == 'true' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 @@ -65,6 +77,14 @@ runs: test -d "$ANDROID_HOME/ndk/27.0.12077973" shell: bash + - name: '::endgroup:: Disk space & Android NDK' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Android ccache' + run: echo "::group::Android ccache" + shell: bash + - name: Install ccache run: sudo apt-get update && sudo apt-get install -y ccache shell: bash @@ -87,7 +107,21 @@ runs: restore-keys: | ${{ runner.os }}-android-ccache-${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }}- + - name: '::endgroup:: Android ccache' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Build packages' + if: inputs.run-yarn-build == 'true' + run: echo "::group::Build packages" + shell: bash + - name: Build packages if: inputs.run-yarn-build == 'true' run: yarn build shell: bash + + - name: '::endgroup:: Build packages' + if: inputs.run-yarn-build == 'true' + run: echo "::endgroup::" + shell: bash diff --git a/.github/actions/prepare-ios/action.yml b/.github/actions/prepare-ios/action.yml index 95d789cb..8d20324c 100644 --- a/.github/actions/prepare-ios/action.yml +++ b/.github/actions/prepare-ios/action.yml @@ -10,6 +10,10 @@ inputs: runs: using: composite steps: + - name: '::group:: Xcode & Ruby' + run: echo "::group::Xcode & Ruby" + shell: bash + - name: Use appropriate Xcode version uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: @@ -21,7 +25,21 @@ runs: ruby-version: '3.2' bundler-cache: true + - name: '::endgroup:: Xcode & Ruby' + run: echo "::endgroup::" + shell: bash + + - name: '::group:: Build packages' + if: inputs.run-yarn-build == 'true' + run: echo "::group::Build packages" + shell: bash + - name: Build packages if: inputs.run-yarn-build == 'true' run: yarn build shell: bash + + - name: '::endgroup:: Build packages' + if: inputs.run-yarn-build == 'true' + run: echo "::endgroup::" + shell: bash From 551f3546a92c2641b7b390c87a867f6bdf9a0891 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 15:30:54 +0200 Subject: [PATCH 07/21] ci: upgrade outdated actions --- .github/actions/appleapp-road-test/action.yml | 2 +- .github/actions/prepare-ios/action.yml | 2 +- .github/workflows/release-brownfield-gradle-plugin.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/appleapp-road-test/action.yml b/.github/actions/appleapp-road-test/action.yml index 1c92e342..eebcfe52 100644 --- a/.github/actions/appleapp-road-test/action.yml +++ b/.github/actions/appleapp-road-test/action.yml @@ -250,7 +250,7 @@ runs: - name: Upload Detox recordings on failure if: failure() && inputs.run-e2e == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.e2e-artifact-name }}-${{ inputs.variant }}-ios-recordings path: apps/AppleApp/e2e-artifacts diff --git a/.github/actions/prepare-ios/action.yml b/.github/actions/prepare-ios/action.yml index 8d20324c..f7a59981 100644 --- a/.github/actions/prepare-ios/action.yml +++ b/.github/actions/prepare-ios/action.yml @@ -15,7 +15,7 @@ runs: shell: bash - name: Use appropriate Xcode version - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: '26.3' diff --git a/.github/workflows/release-brownfield-gradle-plugin.yml b/.github/workflows/release-brownfield-gradle-plugin.yml index 12705508..6a50bba9 100644 --- a/.github/workflows/release-brownfield-gradle-plugin.yml +++ b/.github/workflows/release-brownfield-gradle-plugin.yml @@ -105,13 +105,13 @@ jobs: --stacktrace - name: Upload release notes artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: brownfield-gradle-plugin-release-notes path: out/jreleaser/brownfield-gradle-plugin-release-notes.md - name: Upload staged Maven repository artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: brownfield-gradle-plugin-staging-repo path: gradle-plugins/react/brownfield/build/staging-deploy @@ -165,7 +165,7 @@ jobs: - name: Upload JReleaser output if: always() - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: brownfield-gradle-plugin-jreleaser-output path: | From 9d30d64ef73bf1554aedb7951f14912e928dfeae Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 15:39:16 +0200 Subject: [PATCH 08/21] ci: disable caches in release workflows --- .github/actions/prepare-android/action.yml | 7 +++++++ .github/actions/setup/action.yml | 8 ++++++-- .github/workflows/release-brownfield-gradle-plugin.yml | 3 ++- .github/workflows/release.yml | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/actions/prepare-android/action.yml b/.github/actions/prepare-android/action.yml index 3fd5cb20..de852500 100644 --- a/.github/actions/prepare-android/action.yml +++ b/.github/actions/prepare-android/action.yml @@ -12,6 +12,11 @@ inputs: required: false default: 'false' + disable-cache: + description: 'Disable Gradle and Android ccache restore (use for release/publish workflows)' + required: false + default: 'false' + gradle-workflow-job-context: description: 'Segment Gradle cache per app/project (e.g. vanilla, expo54). Falls back to github.job when empty.' required: false @@ -44,6 +49,7 @@ runs: # Validates all gradle-wrapper.jar files, caches Gradle User Home efficiently, # and captures Build Scan links. Do not combine with actions/cache on ~/.gradle. workflow-job-context: ${{ inputs.gradle-workflow-job-context != '' && inputs.gradle-workflow-job-context || github.job }} + cache-disabled: ${{ inputs.disable-cache == 'true' }} # ubuntu-latest is x86_64; match the emulator ABI so CI only compiles one JNI arch. - name: Limit native builds to runner emulator ABI @@ -100,6 +106,7 @@ runs: shell: bash - name: Restore Android ccache + if: inputs.disable-cache != 'true' uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: .android_ccache diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 6ce76f70..fd8d294a 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -2,6 +2,10 @@ name: Setup description: Setup Node.js and install dependencies inputs: + disable-cache: + description: 'Disable Node.js yarn cache and Turbo cache restore (use for release/publish workflows)' + required: false + default: 'false' restore-turbo-cache: description: 'Whether to restore the Turbo cache' required: false @@ -14,10 +18,10 @@ runs: uses: actions/setup-node@65d868f8d4d85d7d4abb7de0875cde3fcc8798f5 # v6 with: node-version: 'lts/*' - cache: 'yarn' + cache: ${{ inputs.disable-cache != 'true' && 'yarn' || '' }} - name: Restore Turbo cache - if: inputs.restore-turbo-cache == 'true' + if: inputs.disable-cache != 'true' && inputs.restore-turbo-cache == 'true' uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | diff --git a/.github/workflows/release-brownfield-gradle-plugin.yml b/.github/workflows/release-brownfield-gradle-plugin.yml index 6a50bba9..15f10a31 100644 --- a/.github/workflows/release-brownfield-gradle-plugin.yml +++ b/.github/workflows/release-brownfield-gradle-plugin.yml @@ -40,13 +40,14 @@ jobs: - name: Setup uses: ./.github/actions/setup with: - restore-turbo-cache: 'false' + disable-cache: 'true' - name: Prepare Android environment uses: ./.github/actions/prepare-android with: free-disk-space: 'false' run-yarn-build: 'false' + disable-cache: 'true' - name: Derive release metadata id: metadata diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 847af816..c2e4e349 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - name: Setup uses: ./.github/actions/setup with: - restore-turbo-cache: 'false' # in release workflow, build from scratch + disable-cache: 'true' - name: Build From ccec468e30f98f0d954d910188dd927402758d19 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Sat, 20 Jun 2026 15:58:47 +0200 Subject: [PATCH 09/21] fix(test): do not show expo-updates dialog when in E2E tests --- .../android/example/MainActivity.kt | 9 +++++ .../components/ContentView.swift | 13 ++++--- apps/ExpoApp54/RNApp.tsx | 19 +++++++--- apps/ExpoApp54/app/(tabs)/postMessage.tsx | 4 +-- .../components/postMessage/MessageBubble.tsx | 4 +-- apps/ExpoApp54/entry.tsx | 12 ++++++- apps/ExpoApp54/utils/expo-rn-updates.ts | 6 ++-- apps/ExpoApp55/RNApp.tsx | 19 +++++++--- .../__tests__/brownfield.example.test.tsx | 2 ++ apps/ExpoApp55/entry.tsx | 12 ++++++- apps/ExpoApp55/src/app/postMessage.tsx | 4 +-- .../components/postMessage/MessageBubble.tsx | 4 +-- apps/ExpoApp55/src/utils/expo-rn-updates.ts | 6 ++-- apps/RNApp/jest.config.js | 1 + apps/RNApp/src/App.tsx | 16 ++++++--- apps/RNApp/src/HomeScreen.tsx | 14 ++++---- apps/RNApp/src/components/counter/index.tsx | 6 ++-- .../e2e/appleAppBrownfield.e2e.js | 4 ++- .../e2e/appleAppDetoxUtils.cjs | 8 ++--- .../e2e/appleAppExpoBrownfield.e2e.js | 4 ++- .../e2e/detoxUtils.cjs | 6 ++++ .../e2e/e2eTestIds.cjs | 4 +-- .../e2e/expoPostMessageBrownfield.e2e.js | 7 ++-- .../e2e/rnAppBrownfield.e2e.js | 7 ++-- .../jest/expo-config.js | 1 + .../src/e2eTestIds.ts | 2 +- .../src/index.ts | 8 +++++ .../src/suites/expoRnApp.suite.tsx | 3 +- .../src/suites/userAlert.suite.ts | 35 +++++++++++++++++++ .../src/userAlert.ts | 35 +++++++++++++++++++ 30 files changed, 217 insertions(+), 58 deletions(-) create mode 100644 apps/brownfield-example-shared-tests/src/suites/userAlert.suite.ts create mode 100644 apps/brownfield-example-shared-tests/src/userAlert.ts diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt index 7d81eb7f..6113b2d8 100644 --- a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt @@ -1,6 +1,7 @@ package com.callstack.brownfield.android.example import com.callstack.brownie.registerStoreIfNeeded +import android.app.Activity import android.content.Intent import android.content.res.Configuration import android.os.Build @@ -20,9 +21,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.compose.AndroidFragment @@ -137,6 +140,11 @@ private fun MainScreen(modifier: Modifier = Modifier) { fun ReactNativeView( modifier: Modifier = Modifier ) { + val activity = LocalContext.current as? Activity + val brownfieldE2E = remember(activity) { + activity?.intent?.getStringExtra("DetoxE2E") == "YES" + } + AndroidFragment( modifier = modifier, arguments = Bundle().apply { @@ -151,6 +159,7 @@ fun ReactNativeView( "nativeOsVersionLabel", "Android ${Build.VERSION.RELEASE}" ) + putBoolean("brownfieldE2E", brownfieldE2E) } ) } diff --git a/apps/AppleApp/Brownfield Apple App/components/ContentView.swift b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift index 0b2ceb83..035111ae 100644 --- a/apps/AppleApp/Brownfield Apple App/components/ContentView.swift +++ b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift @@ -34,6 +34,14 @@ private func brownfieldPostMessageText(from raw: String) -> String { return raw } +private var brownfieldInitialProperties: [String: Any] { + [ + "nativeOsVersionLabel": + "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", + "brownfieldE2E": ProcessInfo.processInfo.arguments.contains("-DetoxE2E"), + ] +} + struct ContentView: View { @State private var messageObserver: NSObjectProtocol? @State private var showPostMessageToast = false @@ -50,10 +58,7 @@ struct ContentView: View { ReactNativeView( moduleName: reactNativeModuleName, - initialProperties: [ - "nativeOsVersionLabel": - "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" - ] + initialProperties: brownfieldInitialProperties ) .navigationBarHidden(true) .clipShape(RoundedRectangle(cornerRadius: 16)) diff --git a/apps/ExpoApp54/RNApp.tsx b/apps/ExpoApp54/RNApp.tsx index 5f771364..fc7b70cc 100644 --- a/apps/ExpoApp54/RNApp.tsx +++ b/apps/ExpoApp54/RNApp.tsx @@ -1,16 +1,27 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Button, StyleSheet, Text, View } from 'react-native'; +import { useEffect } from 'react'; import BrownfieldNavigation from '@callstack/brownfield-navigation'; +import { + syncBrownfieldE2EModeFromRootProps, + type BrownfieldRootProps, +} from '@callstack/brownfield-example-shared-tests'; import Counter from './components/counter'; import { checkAndFetchUpdate } from './utils/expo-rn-updates'; -type RNAppProps = { - nativeOsVersionLabel?: string; -}; +type RNAppProps = BrownfieldRootProps; + +export default function RNApp({ + nativeOsVersionLabel, + brownfieldE2E, +}: RNAppProps) { + useEffect(() => { + syncBrownfieldE2EModeFromRootProps({ brownfieldE2E }); + return () => syncBrownfieldE2EModeFromRootProps(undefined); + }, [brownfieldE2E]); -export default function RNApp({ nativeOsVersionLabel }: RNAppProps) { return ( Expo React Native Brownfield diff --git a/apps/ExpoApp54/app/(tabs)/postMessage.tsx b/apps/ExpoApp54/app/(tabs)/postMessage.tsx index 86b75e1b..815dd098 100644 --- a/apps/ExpoApp54/app/(tabs)/postMessage.tsx +++ b/apps/ExpoApp54/app/(tabs)/postMessage.tsx @@ -1,7 +1,7 @@ import { StyleSheet, FlatList, TouchableOpacity } from 'react-native'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { brownfieldE2eTestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; +import { brownfieldE2ETestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; import type { MessageEvent } from '@callstack/react-native-brownfield'; @@ -52,7 +52,7 @@ export default function PostMessageTab() { return ( {item.text} diff --git a/apps/ExpoApp54/entry.tsx b/apps/ExpoApp54/entry.tsx index 3c70f46c..79d7a2ec 100644 --- a/apps/ExpoApp54/entry.tsx +++ b/apps/ExpoApp54/entry.tsx @@ -1,7 +1,17 @@ import { ExpoRoot } from 'expo-router'; +import { useEffect } from 'react'; import { AppRegistry } from 'react-native'; +import { + syncBrownfieldE2EModeFromRootProps, + type BrownfieldRootProps, +} from '@callstack/brownfield-example-shared-tests'; + +function App(props: BrownfieldRootProps) { + useEffect(() => { + syncBrownfieldE2EModeFromRootProps(props); + return () => syncBrownfieldE2EModeFromRootProps(undefined); + }, [props.brownfieldE2E]); -function App() { const ctx = require.context('./app'); return ; } diff --git a/apps/ExpoApp54/utils/expo-rn-updates.ts b/apps/ExpoApp54/utils/expo-rn-updates.ts index 58eaed01..aab4c4fa 100644 --- a/apps/ExpoApp54/utils/expo-rn-updates.ts +++ b/apps/ExpoApp54/utils/expo-rn-updates.ts @@ -1,5 +1,5 @@ +import { userAlert } from '@callstack/brownfield-example-shared-tests'; import * as Updates from 'expo-updates'; -import { Alert } from 'react-native'; export async function checkAndFetchUpdate() { try { @@ -17,9 +17,9 @@ export async function checkAndFetchUpdate() { }, }); } else { - Alert.alert('No update available'); + userAlert('No update available'); } } catch (error) { - Alert.alert('Update check failed', String(error)); + userAlert('Update check failed', String(error)); } } diff --git a/apps/ExpoApp55/RNApp.tsx b/apps/ExpoApp55/RNApp.tsx index bb470700..ff66d315 100644 --- a/apps/ExpoApp55/RNApp.tsx +++ b/apps/ExpoApp55/RNApp.tsx @@ -1,15 +1,26 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Button, StyleSheet, Text, View } from 'react-native'; +import { useEffect } from 'react'; import BrownfieldNavigation from '@callstack/brownfield-navigation'; +import { + syncBrownfieldE2EModeFromRootProps, + type BrownfieldRootProps, +} from '@callstack/brownfield-example-shared-tests'; import { checkAndFetchUpdate } from './src/utils/expo-rn-updates'; import Counter from './src/components/counter'; -type RNAppProps = { - nativeOsVersionLabel?: string; -}; +type RNAppProps = BrownfieldRootProps; + +export default function RNApp({ + nativeOsVersionLabel, + brownfieldE2E, +}: RNAppProps) { + useEffect(() => { + syncBrownfieldE2EModeFromRootProps({ brownfieldE2E }); + return () => syncBrownfieldE2EModeFromRootProps(undefined); + }, [brownfieldE2E]); -export default function RNApp({ nativeOsVersionLabel }: RNAppProps) { return ( Expo React Native Brownfield diff --git a/apps/ExpoApp55/__tests__/brownfield.example.test.tsx b/apps/ExpoApp55/__tests__/brownfield.example.test.tsx index eeaf54e0..893da8e6 100644 --- a/apps/ExpoApp55/__tests__/brownfield.example.test.tsx +++ b/apps/ExpoApp55/__tests__/brownfield.example.test.tsx @@ -5,8 +5,10 @@ import { runPostMessageTabSuite, runCounterSuite, runExpoRnAppSuite, + runUserAlertSuite, } from '@callstack/brownfield-example-shared-tests'; runPostMessageTabSuite('ExpoApp55', PostMessageTab); runCounterSuite('ExpoApp55', Counter); runExpoRnAppSuite('ExpoApp55', RNApp); +runUserAlertSuite('ExpoApp55'); diff --git a/apps/ExpoApp55/entry.tsx b/apps/ExpoApp55/entry.tsx index 87dd9e5e..f8434d28 100644 --- a/apps/ExpoApp55/entry.tsx +++ b/apps/ExpoApp55/entry.tsx @@ -1,7 +1,17 @@ import { ExpoRoot } from 'expo-router'; +import { useEffect } from 'react'; import { AppRegistry } from 'react-native'; +import { + syncBrownfieldE2EModeFromRootProps, + type BrownfieldRootProps, +} from '@callstack/brownfield-example-shared-tests'; + +function App(props: BrownfieldRootProps) { + useEffect(() => { + syncBrownfieldE2EModeFromRootProps(props.brownfieldE2E); + return () => syncBrownfieldE2EModeFromRootProps(undefined); + }, [props.brownfieldE2E]); -function App() { const ctx = require.context('./src/app'); return ; } diff --git a/apps/ExpoApp55/src/app/postMessage.tsx b/apps/ExpoApp55/src/app/postMessage.tsx index 86b75e1b..815dd098 100644 --- a/apps/ExpoApp55/src/app/postMessage.tsx +++ b/apps/ExpoApp55/src/app/postMessage.tsx @@ -1,7 +1,7 @@ import { StyleSheet, FlatList, TouchableOpacity } from 'react-native'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { brownfieldE2eTestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; +import { brownfieldE2ETestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; import type { MessageEvent } from '@callstack/react-native-brownfield'; @@ -52,7 +52,7 @@ export default function PostMessageTab() { return ( {item.text} diff --git a/apps/ExpoApp55/src/utils/expo-rn-updates.ts b/apps/ExpoApp55/src/utils/expo-rn-updates.ts index 58eaed01..aab4c4fa 100644 --- a/apps/ExpoApp55/src/utils/expo-rn-updates.ts +++ b/apps/ExpoApp55/src/utils/expo-rn-updates.ts @@ -1,5 +1,5 @@ +import { userAlert } from '@callstack/brownfield-example-shared-tests'; import * as Updates from 'expo-updates'; -import { Alert } from 'react-native'; export async function checkAndFetchUpdate() { try { @@ -17,9 +17,9 @@ export async function checkAndFetchUpdate() { }, }); } else { - Alert.alert('No update available'); + userAlert('No update available'); } } catch (error) { - Alert.alert('Update check failed', String(error)); + userAlert('Update check failed', String(error)); } } diff --git a/apps/RNApp/jest.config.js b/apps/RNApp/jest.config.js index bb08fc51..4e9a7073 100644 --- a/apps/RNApp/jest.config.js +++ b/apps/RNApp/jest.config.js @@ -7,6 +7,7 @@ module.exports = { '^react$': require.resolve('react'), '^react/jsx-runtime$': require.resolve('react/jsx-runtime'), '^react/jsx-dev-runtime$': require.resolve('react/jsx-dev-runtime'), + '^react-native$': require.resolve('react-native'), '^@testing-library/react-native$': require.resolve('@testing-library/react-native'), '^@callstack/react-native-brownfield$': path.join( diff --git a/apps/RNApp/src/App.tsx b/apps/RNApp/src/App.tsx index 84ca5f7a..e0532106 100644 --- a/apps/RNApp/src/App.tsx +++ b/apps/RNApp/src/App.tsx @@ -1,16 +1,24 @@ import '../BrownfieldStore.brownie'; import { NavigationContainer } from '@react-navigation/native'; +import { useEffect } from 'react'; +import { + syncBrownfieldE2EModeFromRootProps, + type BrownfieldRootProps, +} from '@callstack/brownfield-example-shared-tests'; import { HomeScreen } from './HomeScreen'; import { NativeOsVersionLabelContext } from './nativeHostContext'; import { Stack } from './navigation/RootStack'; -type AppProps = { - nativeOsVersionLabel?: string; -}; +type AppProps = BrownfieldRootProps; + +export default function App({ nativeOsVersionLabel, brownfieldE2E }: AppProps) { + useEffect(() => { + syncBrownfieldE2EModeFromRootProps({ brownfieldE2E }); + return () => syncBrownfieldE2EModeFromRootProps(undefined); + }, [brownfieldE2E]); -export default function App({ nativeOsVersionLabel }: AppProps) { return ( diff --git a/apps/RNApp/src/HomeScreen.tsx b/apps/RNApp/src/HomeScreen.tsx index c0d20c29..d3c27d5d 100644 --- a/apps/RNApp/src/HomeScreen.tsx +++ b/apps/RNApp/src/HomeScreen.tsx @@ -10,7 +10,7 @@ import { } from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; -import { brownfieldE2eTestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; +import { brownfieldE2ETestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds'; import BrownfieldNavigation from '@callstack/brownfield-navigation'; import { getRandomTheme } from './utils'; @@ -64,7 +64,7 @@ function MessageBubble({ item, color }: { item: Message; color: string }) { @@ -128,11 +128,11 @@ export function HomeScreen({ return ( @@ -152,7 +152,7 @@ export function HomeScreen({