ci: run E2E suites in demo apps#352
Conversation
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
# Conflicts: # turbo.json
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@hurali97 @artus9033 i think we good to merge? |
Yes, before that I would remove the |
@hurali97 i still need to add change set - otherwise CI would fail |
|
@alpharius-ck - Maybe you need to adjust the changeset config to |
@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 |
# Conflicts: # apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj
|
@artus9033 updated |
|
@artus9033 @hurali97 are we good to go? |
There was a problem hiding this comment.
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.
artus9033
left a comment
There was a problem hiding this comment.
LGTM after 2 minor comments resolved!
|
@artus9033 all updated |
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:
Test plan
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:
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
Counterwith 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:
BrownieStoreManager— holds stores and backsglobal.__brownieGetStore(what JS uses).Store/StoreManager— optional native-side wrapper;BrownfieldStore.register(...)creates aStoreand calls into the C++ bridge.JavaScript (
@callstack/brownie) reads stores via: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.frameworkinto the app. That framework was built withinherit! :completein the Podfile, so it contained a full second copy of Brownie (including its ownBrownieStoreManagersingleton).At runtime:
AppDelegate→BrownfieldStore.register(...)__brownieGetStore→ C++ lookupRegistration 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.xcframeworkand oneBrownfieldLib.xcframeworkseparately, and registers viaimport Browniein 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.BrownieModuleonly implemented the ObjC protocolRCTTurboModuleWithJSIBindings. In bridgeless / New Architecture, that path is not used;dynamic_cast<TurboModuleWithJSIBindings*>on the C++ module fails, soBrownieInstaller::install()never ran andglobal.__brownieGetStorestayed undefined.Android already handled this in
BrownieModule.initialize()viainstallJSIBindingsIfNeeded().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
ios/RNApp.xcodeproj/project.pbxprojBrownfieldLibfrom the RNApp app target. TheBrownfieldLibtarget remains forbrownfield package:ios.ios/RNApp/AppDelegate.swiftimport Brownie; register store beforefactory.startReactNative(...)viaBrownieBootstrap.register(...).ios/BrownfieldLib/BrownfieldLib.swift@_exported import Brownie. Only re-exportsReactBrownfield.BrownfieldLibRNApp — Android
android/app/src/main/java/com/rnapp/MainApplication.ktregisterStoreIfNeeded(...)forBrownfieldStorebeforeloadReactNative(this).android/app/build.gradleimplementation(project(":BrownfieldLib"))for generated Kotlin types.@callstack/browniepackagepackages/brownie/ios/BrownieModule.mmBrownieTurboModuleextendingNativeBrownieModuleSpecJSI+TurboModuleWithJSIBindings;getTurboModulereturns it;installJSIBindingsWithRuntimecallsBrownieInstaller::install(runtime).__brownieGetStorenever set on New Architecturepackages/brownie/ios/BrownieModule.hRCTTurboModuleWithJSIBindingsimport from header.packages/brownie/ios/BrownieStore.swiftBrownieBootstrap— registers viaBrownieStoreBridgedirectly (C++ path JS uses).RNApp — tooling
package.jsonios/androidscripts runyarn codegenfirst.Codegen (operational)
From
apps/RNApp:This generates:
packages/brownie/ios/Generated/BrownfieldStore.swift(gitignored)android/BrownfieldLib/.../Generated/BrownfieldStore.ktWhy each fix works
Stop embedding
BrownfieldLibin RNAppRNApp 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
BrownieStoreManagerin the process; registration and JS use the same store map.BrownfieldLibis still built foryarn 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. BothAppDelegate(iOS) andMainApplication(Android) register initial state before starting RN.BrownieTurboModule(library fix)When JS first loads the Brownie turbo module, React calls
TurboModuleWithJSIBindings::installJSIBindingson the C++ module. That runsBrownieInstaller::install, which definesglobal.__brownieGetStore. Without this, JS fails even with a correctly registered native store.BrownieBootstrap(optional)BrownfieldStore.registercreates a SwiftStoreand also talks to the bridge. After the duplicate was removed,BrownfieldStore.registeris sufficient for RNApp again.BrownieBootstrapregisters straight throughBrownieStoreBridgeand documents “use this from AppDelegate before RN starts.” You can use either:No Brownie re-export in
BrownfieldLibPackaged
BrownfieldLib.xcframeworkshould 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
BrownfieldLibin the RNApp app target.BrownieTurboModulein@callstack/browniefor iOS New Architecture.yarn codegenwhen store schemas change.Optional
BrownieBootstrapvsBrownfieldStore.registerinAppDelegate(equivalent after duplicate fix).yarn codegen &&prepended toios/androidscripts.Unchanged
BrownfieldStore.registerin appinit, separateBrownie+BrownfieldLibxcframeworks — no change needed.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 stateTakeaway
The error looked like “forgot to register the store,” but RNApp actually had two separate issues:
BrownfieldLib) — why AppleApp worked and RNApp did not.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.