Skip to content

ci: run E2E suites in demo apps#352

Open
alpharius-ck wants to merge 52 commits into
mainfrom
fixes-for-local-build
Open

ci: run E2E suites in demo apps#352
alpharius-ck wants to merge 52 commits into
mainfrom
fixes-for-local-build

Conversation

@alpharius-ck

@alpharius-ck alpharius-ck commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Adds CI coverage for the existing Detox iOS E2E suites in RNApp, ExpoApp54, and ExpoApp55.

Introduces a reusable composite action (.github/actions/e2e-ios) that:

  • Sets up the monorepo and Xcode (same stack as other iOS jobs)
  • Installs applesimutils for Detox on CI
  • Runs brownfield codegen for RNApp (generated Brownie types are not committed)
  • Runs Expo iOS prebuild for Expo apps (ios/ is gitignored)
  • Installs CocoaPods, builds with yarn e2e:build:ios, and runs yarn e2e:test:ios
  • Uploads Detox artifacts/ on failure for debugging
  • Extends .github/workflows/ci.yml with two jobs:
  • E2E iOS (RNApp) — runs when RNApp, shared tests, packages, or CI config change
  • E2E iOS (Expo 54 & 55) — matrix job with the same path-filter pattern as the AppleApp road tests
  • Updates path filters so changes under apps/brownfield-example-shared-tests/** trigger the relevant app E2E jobs
  • This closes the gap where E2E only ran locally; regressions in brownfield flows (postMessage, home screen, counter, navigation) are now caught on PRs.

Test plan

  • Open this PR and confirm CI runs E2E iOS (RNApp) and E2E iOS (Expo 54) / E2E iOS (Expo 55) (triggered by .github/** or app changes)
  • Verify all three E2E jobs complete successfully on macos-26
  • Optionally confirm path filtering: a PR that only touches apps/RNApp/** should run RNApp E2E but skip Expo E2E (unless packages/CI also changed)
  • Local sanity check (optional, mirrors CI):
  • RNApp: cd apps/RNApp && yarn codegen && cd ios && pod install && cd .. && yarn e2e:build:ios && yarn e2e:test:ios
  • Expo 54/55: cd apps/ExpoApp54 (or 55) → yarn prebuild → cd ios && pod install → yarn e2e:build:ios && yarn e2e:test:ios

If a job fails, download the detox-rnapp-ios / detox-expo54-ios / detox-expo55-ios artifact and inspect screenshots/logs

Changes for native files:

BrownfieldStore “not found” fix (RNApp iOS)

This document summarizes the investigation and fixes for the Brownie error when running RNApp on iOS:

[Brownie] Store "BrownfieldStore" not found.
Make sure to register it on the native side before accessing it from JS.

AppleApp (brownfield host) worked with the same JS and store schema; only standalone RNApp failed until these changes were applied.


The problem

When running RNApp on iOS, React crashed on the first render of Counter with the error above. useStore('BrownfieldStore', …) in JavaScript could not see a store that native code was supposed to have registered.


How Brownie works (short)

Brownie shares state between native and React Native through two layers:

  1. C++ BrownieStoreManager — holds stores and backs global.__brownieGetStore (what JS uses).
  2. Swift Store / StoreManager — optional native-side wrapper; BrownfieldStore.register(...) creates a Store and calls into the C++ bridge.

JavaScript (@callstack/brownie) reads stores via:

global.__brownieGetStore?.(key)

That global is installed by BrownieInstaller::install(), which must run when the Brownie turbo module is loaded under the New Architecture.

Native registration must hit the same C++ manager instance that JavaScript uses.


Root causes

There were two separate issues, not one.

1. Duplicate Brownie in RNApp (main RNApp bug)

RNApp linked and embedded BrownfieldLib.framework into the app. That framework was built with inherit! :complete in the Podfile, so it contained a full second copy of Brownie (including its own BrownieStoreManager singleton).

At runtime:

Action Which Brownie copy
AppDelegateBrownfieldStore.register(...) Often the copy inside BrownfieldLib
JS → __brownieGetStore → C++ lookup Copy linked into RNApp / React Native pods

Registration and JS were talking to different singletons, so JS always saw “store not found” even though registration appeared to succeed on native.

AppleApp avoids this: it links one Brownie.xcframework and one BrownfieldLib.xcframework separately, and registers via import Brownie in the app — not by embedding a BrownfieldLib that re-exports and duplicates Brownie.

2. JSI bindings not installed on iOS New Architecture (brownie package bug)

On iOS with RCT_NEW_ARCH_ENABLED, React installs turbo-module JSI bindings only when the module is a C++ TurboModuleWithJSIBindings.

BrownieModule only implemented the ObjC protocol RCTTurboModuleWithJSIBindings. In bridgeless / New Architecture, that path is not used; dynamic_cast<TurboModuleWithJSIBindings*> on the C++ module fails, so BrownieInstaller::install() never ran and global.__brownieGetStore stayed undefined.

Android already handled this in BrownieModule.initialize() via installJSIBindingsIfNeeded().

This could affect any iOS React Native 0.85+ app using Brownie; RNApp made it obvious because JS hits the store immediately on launch.


What we changed

RNApp — iOS app target

File Change Problem addressed
ios/RNApp.xcodeproj/project.pbxproj Removed link, embed, and target dependency on BrownfieldLib from the RNApp app target. The BrownfieldLib target remains for brownfield package:ios. Duplicate Brownie singleton
ios/RNApp/AppDelegate.swift import Brownie; register store before factory.startReactNative(...) via BrownieBootstrap.register(...). Store not registered / wrong instance
ios/BrownfieldLib/BrownfieldLib.swift Removed @_exported import Brownie. Only re-exports ReactBrownfield. Prevent duplicate Brownie inside packaged BrownfieldLib

RNApp — Android

File Change Problem addressed
android/app/src/main/java/com/rnapp/MainApplication.kt registerStoreIfNeeded(...) for BrownfieldStore before loadReactNative(this). No native registration on Android
android/app/build.gradle implementation(project(":BrownfieldLib")) for generated Kotlin types. Types / brownfield lib on Android

@callstack/brownie package

File Change Problem addressed
packages/brownie/ios/BrownieModule.mm Introduced C++ BrownieTurboModule extending NativeBrownieModuleSpecJSI + TurboModuleWithJSIBindings; getTurboModule returns it; installJSIBindingsWithRuntime calls BrownieInstaller::install(runtime). __brownieGetStore never set on New Architecture
packages/brownie/ios/BrownieModule.h Dropped unused RCTTurboModuleWithJSIBindings import from header. Cleanup
packages/brownie/ios/BrownieStore.swift Added BrownieBootstrap — registers via BrownieStoreBridge directly (C++ path JS uses). Clear app entry-point API

RNApp — tooling

File Change Problem addressed
package.json ios / android scripts run yarn codegen first. Generated Swift/Kotlin types are gitignored and must exist before build

Codegen (operational)

From apps/RNApp:

yarn codegen

This generates:

  • packages/brownie/ios/Generated/BrownfieldStore.swift (gitignored)
  • android/BrownfieldLib/.../Generated/BrownfieldStore.kt

Why each fix works

Stop embedding BrownfieldLib in RNApp

RNApp as a standalone React Native app does not need the brownfield packaging framework at runtime. Embedding it pulled in a second Brownie. After removal, there is one BrownieStoreManager in the process; registration and JS use the same store map.

BrownfieldLib is still built for yarn brownfield:package:ios; it is just not part of the dev app binary anymore.

Register before React Native starts

Brownie expects stores to exist before the JS bundle runs components that call useStore. Both AppDelegate (iOS) and MainApplication (Android) register initial state before starting RN.

BrownieTurboModule (library fix)

When JS first loads the Brownie turbo module, React calls TurboModuleWithJSIBindings::installJSIBindings on the C++ module. That runs BrownieInstaller::install, which defines global.__brownieGetStore. Without this, JS fails even with a correctly registered native store.

BrownieBootstrap (optional)

BrownfieldStore.register creates a Swift Store and also talks to the bridge. After the duplicate was removed, BrownfieldStore.register is sufficient for RNApp again.

BrownieBootstrap registers straight through BrownieStoreBridge and documents “use this from AppDelegate before RN starts.” You can use either:

// Option A — standard API (also updates Swift StoreManager for @UseStore)
BrownfieldStore.register(initialState)

// Option B — explicit C++ registration
BrownieBootstrap.register(initialState)

No Brownie re-export in BrownfieldLib

Packaged BrownfieldLib.xcframework should not embed another full Brownie. Host apps (like AppleApp) link Brownie explicitly. That keeps brownfield packaging aligned with AppleApp’s working layout.


Required vs optional going forward

Required for RNApp

  • Do not re-embed BrownfieldLib in the RNApp app target.
  • Register stores before RN starts (iOS + Android).
  • Keep BrownieTurboModule in @callstack/brownie for iOS New Architecture.
  • Run yarn codegen when store schemas change.

Optional

  • BrownieBootstrap vs BrownfieldStore.register in AppDelegate (equivalent after duplicate fix).
  • yarn codegen && prepended to ios / android scripts.

Unchanged

  • AppleApp: BrownfieldStore.register in app init, separate Brownie + BrownfieldLib xcframeworks — no change needed.
  • BrownfieldLib Xcode target: still used for packaging, not for day-to-day RNApp runs.

End-to-end flow after fixes (RNApp iOS)

sequenceDiagram
  participant AD as AppDelegate
  participant BB as Brownie C++ manager
  participant RN as React Native
  participant JS as JS useStore

  AD->>BB: BrownieBootstrap / register store
  AD->>RN: startReactNative
  RN->>JS: Load bundle
  JS->>RN: Require Brownie turbo module
  RN->>BB: BrownieTurboModule installs __brownieGetStore
  JS->>BB: __brownieGetStore("BrownfieldStore")
  BB-->>JS: Host object with state
Loading

Takeaway

The error looked like “forgot to register the store,” but RNApp actually had two separate issues:

  1. Wrong Brownie instance (duplicate via embedded BrownfieldLib) — why AppleApp worked and RNApp did not.
  2. JS bridge never installed on iOS New Architecture — fixed in the brownie library with BrownieTurboModule.

Removing the duplicate was the decisive fix for RNApp; the turbo module fix is still necessary for correct Brownie behavior on modern React Native iOS.

Dev workflow: RN prebuilt XCFramework re-signing

prepareXCFrameworks.js now re-signs React.xcframework and ReactNativeDependencies.xcframework with ad-hoc signing (codesign --force --sign - --deep) after copying them into AppleApp.

RN prebuilts ship with a sealed signature that CocoaPods / Brownfield packaging can invalidate (e.g. when module.modulemap drifts). Without re-signing, Xcode may refuse to embed the frameworks — a failure that can look like a broken Pods install and lead to nuking Pods and reinstalling.

This is a local dev / AppleApp consumer-build convenience; CI benefits from it too when E2E copies fresh XCFrameworks via prepareXCFrameworks.js.

Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
@alpharius-ck alpharius-ck changed the title Fixes for local build Adds CI coverage for the existing Detox iOS E2E suites in RNApp, ExpoApp54, and ExpoApp55. May 26, 2026
@alpharius-ck alpharius-ck requested a review from hurali97 June 11, 2026 08:53
@artus9033 artus9033 changed the title Adds CI coverage for the existing Detox iOS E2E suites in RNApp, ExpoApp54, and ExpoApp55. ci: run iOS E2E suites in demo apps Jun 15, 2026
@artus9033 artus9033 changed the title ci: run iOS E2E suites in demo apps ci: run E2E suites in demo apps Jun 15, 2026
Comment thread .changeset/three-impalas-brush.md Outdated
@alpharius-ck

Copy link
Copy Markdown
Collaborator Author

@hurali97 @artus9033 i think we good to merge?

@hurali97

Copy link
Copy Markdown
Member

@hurali97 @artus9033 i think we good to merge?

Yes, before that I would remove the .changeset/fresh-socks-argue.md. The brownfield-example-shared-tests shouldn't need a changeset.

@alpharius-ck

Copy link
Copy Markdown
Collaborator Author

@hurali97 @artus9033 i think we good to merge?

Yes, before that I would remove the .changeset/fresh-socks-argue.md. The brownfield-example-shared-tests shouldn't need a changeset.

@hurali97 i still need to add change set - otherwise CI would fail

@hurali97

Copy link
Copy Markdown
Member

@alpharius-ck - Maybe you need to adjust the changeset config to ignore this shared-test path

@alpharius-ck

alpharius-ck commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

@alpharius-ck - Maybe you need to adjust the changeset config to ignore this shared-test path

@hurali97 I think i could do it with android e2e testing with next PR - currently i also added some changes inside @callstack/react-native-brownfield - and change set is good to go

@alpharius-ck

Copy link
Copy Markdown
Collaborator Author

@artus9033 updated

@alpharius-ck

Copy link
Copy Markdown
Collaborator Author

@artus9033 @hurali97 are we good to go?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds iOS Detox E2E coverage for the brownfield demo apps (RNApp, ExpoApp54/55, and AppleApp) and wires it into CI/local tooling, while also tightening up CLI navigation codegen output handling.

Changes:

  • Introduces shared Detox E2E infrastructure (testIDs, Jest configs, Detox configs, utilities) and updates demo UIs to expose stable selectors.
  • Adds local “mirror CI” runner scripts for iOS E2E flows (RNApp/Expo54/Expo55/AppleApp).
  • Updates CI to run AppleApp road tests with optional Detox E2E and expands path filters to include shared E2E assets.

Reviewed changes

Copilot reviewed 60 out of 61 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
yarn.lock Adds Detox (and transitive deps) to the lockfile.
scripts/ci-local-rnapp-ios-e2e.sh Local runner for RNApp iOS Detox flow.
scripts/ci-local-ios-e2e-common.sh Shared helpers for local iOS Detox scripts.
scripts/ci-local-expo55-ios-e2e.sh Local runner for ExpoApp55 iOS Detox flow (prebuild + pods + detox).
scripts/ci-local-expo54-ios-e2e.sh Local runner for ExpoApp54 iOS Detox flow (prebuild + pods + detox).
scripts/ci-local-appleapp-ios-e2e.sh Local runner for AppleApp iOS Detox flow (variant-aware).
packages/cli/src/navigation/runner.ts Ensures output directories exist before writing codegen artifacts.
packages/cli/src/navigation/generators/ts.ts Changes generated type-import behavior for navigation index files.
package.json Adds local CI helper scripts and changes Yarn resolutions for Detox/glob.
apps/RNApp/src/HomeScreen.tsx Adds stable testIDs and buttons for E2E navigation/message flows.
apps/RNApp/src/components/counter/index.tsx Adds testIDs/accessibility labels for counter E2E.
apps/RNApp/package.json Adds Detox scripts and shared-tests dependency for RNApp E2E.
apps/RNApp/e2e/jest.config.cjs Adds Detox Jest config wiring shared E2E specs for RNApp.
apps/RNApp/.gitignore Ignores Detox artifacts output.
apps/RNApp/.detoxrc.cjs Adds Detox config for RNApp debug simulator build.
apps/ExpoApp55/src/components/postMessage/MessageBubble.tsx Adds testID to RN-authored message bubble text for E2E.
apps/ExpoApp55/src/app/postMessage.tsx Adds testID to the “send message” trigger for E2E.
apps/ExpoApp55/package.json Adds Detox scripts and shared-tests dependency for ExpoApp55 E2E.
apps/ExpoApp55/entry.tsx Ensures embedded module name RNApp mounts the Expo Router tree.
apps/ExpoApp55/e2e/jest.config.cjs Adds Detox Jest config wiring shared E2E specs for ExpoApp55.
apps/ExpoApp55/.gitignore Ignores Detox artifacts output.
apps/ExpoApp55/.detoxrc.cjs Adds Detox config for ExpoApp55 debug simulator build.
apps/ExpoApp54/package.json Adds Detox scripts and shared-tests dependency for ExpoApp54 E2E.
apps/ExpoApp54/entry.tsx Ensures embedded module name RNApp mounts the Expo Router tree.
apps/ExpoApp54/e2e/jest.config.cjs Adds Detox Jest config wiring shared E2E specs for ExpoApp54.
apps/ExpoApp54/components/postMessage/MessageBubble.tsx Adds testID to RN-authored message bubble text for E2E.
apps/ExpoApp54/app/(tabs)/postMessage.tsx Adds testID to the “send message” trigger for E2E.
apps/ExpoApp54/.gitignore Ignores Detox artifacts output.
apps/ExpoApp54/.detoxrc.cjs Adds Detox config for ExpoApp54 debug simulator build.
apps/brownfield-example-shared-tests/src/e2eTestIds.ts Introduces canonical cross-app E2E testIDs (TS).
apps/brownfield-example-shared-tests/package.json Exposes shared E2E utilities/config via package exports.
apps/brownfield-example-shared-tests/e2e/rnAppBrownfield.e2e.js Adds RNApp brownfield Detox suite.
apps/brownfield-example-shared-tests/e2e/expoPostMessageBrownfield.e2e.js Adds Expo brownfield postMessage Detox suite.
apps/brownfield-example-shared-tests/e2e/e2eTestIds.cjs Introduces canonical E2E testIDs (CJS for Detox).
apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs Adds shared Detox helpers (attrs parsing, waits, URL blacklist).
apps/brownfield-example-shared-tests/e2e/createDetoxJestConfig.cjs Shared Jest config factory for Detox E2E.
apps/brownfield-example-shared-tests/e2e/appleAppExpoBrownfield.e2e.js Adds AppleApp (Expo) Detox suite.
apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs Shared AppleApp Detox helpers (scrolling, nav, launch args).
apps/brownfield-example-shared-tests/e2e/appleAppBrownfield.e2e.js Adds AppleApp (Vanilla) Detox suite.
apps/brownfield-example-shared-tests/detox-rc-ios-sim-debug.cjs Shared Detox config generator for RN/Expo host apps.
apps/brownfield-example-shared-tests/detox-rc-appleapp-ios-sim-debug.cjs Detox config generator for AppleApp Xcode project variants.
apps/brownfield-example-shared-tests/detox-ios-simulator-device.cjs Adds simulator device auto-selection logic for Detox.
apps/brownfield-example-shared-tests/detox-appleapp-variants.cjs Centralizes AppleApp variant settings for Detox/CI/local.
apps/AppleApp/prepareXCFrameworks.js Re-signs RN prebuilts when present to support embedding in AppleApp.
apps/AppleApp/package.json Adds Detox scripts and shared-tests/dev deps for AppleApp E2E.
apps/AppleApp/e2e/jest.config.expo55.cjs Detox Jest config for AppleApp Expo55 variant.
apps/AppleApp/e2e/jest.config.cjs Detox Jest config for AppleApp Vanilla variant.
apps/AppleApp/Brownfield Apple App/E2eTestIds.swift Native-side E2E identifiers (keep in sync with JS IDs).
apps/AppleApp/Brownfield Apple App/components/Toast.swift Makes toast E2E-detectable and more stable in Detox runs.
apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift Adds accessibility identifier for Detox assertions.
apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift Adds accessibility identifier for Detox assertions.
apps/AppleApp/Brownfield Apple App/components/MessagesView.swift Moves postMessage observer/toast handling out of this view.
apps/AppleApp/Brownfield Apple App/components/GreetingCard.swift Adds accessibility identifier for greeting and refactors counter text.
apps/AppleApp/Brownfield Apple App/components/ContentView.swift Centralizes postMessage observer + toast overlay for E2E.
apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj Removes synchronized build file exception sets for AppleApp targets.
apps/AppleApp/.detoxrc.expo55.cjs Detox config for AppleApp Expo55 variant.
apps/AppleApp/.detoxrc.cjs Detox config for AppleApp Vanilla variant.
.github/workflows/ci.yml Adds workflow_dispatch, updates path filters, and runs AppleApp road test with optional Detox E2E.
.github/actions/setup/action.yml Disables Detox postinstall during monorepo install to avoid races.
.github/actions/appleapp-road-test/action.yml Adds optional Detox E2E execution path + artifact upload on failure.
.changeset/wet-symbols-double.md Changeset entry for publishing version bumps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/navigation/generators/ts.ts
Comment thread apps/brownfield-example-shared-tests/detox-rc-ios-sim-debug.cjs
Comment thread package.json
Comment thread .github/workflows/ci.yml
Comment thread apps/brownfield-example-shared-tests/e2e/rnAppBrownfield.e2e.js

@artus9033 artus9033 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after 2 minor comments resolved!

Comment thread .github/actions/appleapp-road-test/action.yml Outdated
Comment thread apps/AppleApp/prepareXCFrameworks.js
@alpharius-ck

Copy link
Copy Markdown
Collaborator Author

@artus9033 all updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants