Skip to content
Open
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
98 changes: 98 additions & 0 deletions .github/actions/e2e-ios/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: iOS E2E (Detox)
description: Build and run Detox iOS end-to-end tests for a brownfield example app

inputs:
app-path:
description: 'Path to the app workspace (e.g. apps/RNApp)'
required: true

run-expo-prebuild:
description: 'Run brownfield codegen and Expo iOS prebuild before pod install'
required: false
default: 'false'

run-brownfield-codegen:
description: 'Run brownfield codegen before building (required for RNApp)'
required: false
default: 'false'

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

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

- name: Setup
uses: ./.github/actions/setup

- name: Prepare iOS environment
uses: ./.github/actions/prepare-ios

- name: Install applesimutils
run: |
brew tap wix/brew
brew install applesimutils
shell: bash

- name: Brownfield codegen
if: inputs.run-brownfield-codegen == 'true'
run: yarn codegen
working-directory: ${{ inputs.app-path }}
shell: bash

- name: Expo iOS prebuild
if: inputs.run-expo-prebuild == 'true'
run: |
node ../../packages/cli/dist/main.js codegen
yarn expo prebuild --platform ios --no-install
working-directory: ${{ inputs.app-path }}
shell: bash

- name: Restore Pods cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: ${{ inputs.app-path }}/ios/Pods
key: ${{ runner.os }}-e2e-ios-pods-${{ inputs.app-path }}-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.app-path)) }}
restore-keys: |
${{ runner.os }}-e2e-ios-pods-${{ inputs.app-path }}-

- name: Install CocoaPods
run: pod install
working-directory: ${{ inputs.app-path }}/ios
shell: bash

- name: Restore Detox build cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: ${{ inputs.app-path }}/ios/build
key: ${{ runner.os }}-e2e-ios-build-${{ inputs.app-path }}-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.app-path), format('{0}/ios/*.xcodeproj/project.pbxproj', inputs.app-path)) }}
restore-keys: |
${{ runner.os }}-e2e-ios-build-${{ inputs.app-path }}-

- name: Install Detox iOS artifacts
run: node node_modules/detox/scripts/postinstall.js
working-directory: ${{ inputs.app-path }}
shell: bash

- name: Detox build (iOS Simulator)
run: yarn e2e:build:ios
working-directory: ${{ inputs.app-path }}
shell: bash

- name: Detox test (iOS Simulator)
run: yarn e2e:test:ios
working-directory: ${{ inputs.app-path }}
shell: bash

- name: Upload Detox artifacts on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ inputs.artifact-name }}-ios
path: ${{ inputs.app-path }}/artifacts
if-no-files-found: ignore
4 changes: 4 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ runs:
cache: 'yarn'

- name: Install dependencies
env:
# Monorepo has detox in multiple workspaces; parallel postinstalls race on
# $HOME/Library/Detox/ios/framework. E2E jobs run postinstall once later.
DETOX_DISABLE_POSTINSTALL: '1'
run: yarn install
shell: bash

Expand Down
71 changes: 70 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: pr-${{ github.event.pull_request.number }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
actions: write

jobs:
filter:
name: Detect changed paths
Expand All @@ -35,10 +40,13 @@ jobs:
- 'packages/**'
rnapp:
- 'apps/RNApp/**'
- 'apps/brownfield-example-shared-tests/**'
expo54:
- 'apps/ExpoApp54/**'
- 'apps/brownfield-example-shared-tests/**'
expo55:
- 'apps/ExpoApp55/**'
- 'apps/brownfield-example-shared-tests/**'
androidapp:
- 'apps/AndroidApp/**'
appleapp:
Expand Down Expand Up @@ -220,3 +228,64 @@ jobs:
with:
variant: expo${{ matrix.version }}
rn-project-path: apps/ExpoApp${{ matrix.version }}

e2e-ios-rnapp:
name: E2E iOS (RNApp)
runs-on: macos-26
timeout-minutes: 90
permissions:
contents: read
actions: write
needs: [filter, build-lint]
if: |
always() &&
(
needs.filter.outputs.rnapp == 'true' ||
needs.filter.outputs.packages == 'true' ||
needs.filter.outputs.ci == 'true'
) &&
(needs.build-lint.result == 'success' || needs.build-lint.result == 'skipped')
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Run Detox E2E (RNApp)
uses: ./.github/actions/e2e-ios
with:
app-path: apps/RNApp
artifact-name: detox-rnapp
run-brownfield-codegen: 'true'

e2e-ios-expo:
name: E2E iOS (Expo ${{ matrix.version }})
runs-on: macos-26
timeout-minutes: 90
permissions:
contents: read
actions: write
needs: [filter, build-lint]
if: |
always() &&
(
needs.filter.outputs.expo54 == 'true' ||
needs.filter.outputs.expo55 == 'true' ||
needs.filter.outputs.packages == 'true' ||
needs.filter.outputs.ci == 'true'
) &&
(needs.build-lint.result == 'success' || needs.build-lint.result == 'skipped')
strategy:
fail-fast: false
matrix:
include:
- version: '54'
- version: '55'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Run Detox E2E (Expo ${{ matrix.version }})
uses: ./.github/actions/e2e-ios
with:
app-path: apps/ExpoApp${{ matrix.version }}
artifact-name: detox-expo${{ matrix.version }}
run-expo-prebuild: 'true'
43 changes: 43 additions & 0 deletions apps/ExpoApp54/.detoxrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const {
getIosSimulatorDeviceType,
} = require('../brownfield-example-shared-tests/detox-ios-simulator-device.cjs');

/**
* Requires a native tree from `expo prebuild` / `expo run:ios` (ios/ + Pods).
* @type {Detox.DetoxConfig}
*/
module.exports = {
testRunner: {
$0: 'jest',
args: {
config: 'e2e/jest.config.cjs',
_: ['e2e'],
},
jest: {
setupTimeout: 300000,
},
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath:
'ios/build/Build/Products/Debug-iphonesimulator/ExpoApp54.app',
build:
'xcodebuild -workspace ios/ExpoApp54.xcworkspace -scheme ExpoApp54 -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
},
},
devices: {
'ios.sim': {
type: 'ios.simulator',
device: {
type: getIosSimulatorDeviceType(),
},
},
},
configurations: {
'ios.sim.debug': {
device: 'ios.sim',
app: 'ios.debug',
},
},
};
3 changes: 3 additions & 0 deletions apps/ExpoApp54/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ yarn-error.*

app-example

# Detox
artifacts/

# generated native folders
/ios
/android
58 changes: 36 additions & 22 deletions apps/ExpoApp54/RNApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { Button, StyleSheet, Text, View } from 'react-native';
import BrownfieldNavigation from '@callstack/brownfield-navigation';

import PostMessageTab from './app/(tabs)/postMessage';
import Counter from './components/counter';

import { checkAndFetchUpdate } from './utils/expo-rn-updates';
Expand All @@ -13,29 +14,35 @@ type RNAppProps = {
export default function RNApp({ nativeOsVersionLabel }: RNAppProps) {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Expo React Native Brownfield</Text>
<View style={styles.header}>
<Text style={styles.title}>Expo React Native Brownfield</Text>

{nativeOsVersionLabel ? (
<Text
style={styles.nativeOsVersionLabel}
accessibilityLabel="Native OS version"
>
{nativeOsVersionLabel}
</Text>
) : null}
{nativeOsVersionLabel ? (
<Text
style={styles.nativeOsVersionLabel}
accessibilityLabel="Native OS version"
>
{nativeOsVersionLabel}
</Text>
) : null}

<View style={styles.content}>
<Counter />
<View style={styles.content}>
<Counter />

<Button
title="Navigate to Settings"
onPress={() => BrownfieldNavigation.navigateToSettings()}
/>
<Button
title="Navigate to Referrals"
onPress={() => BrownfieldNavigation.navigateToReferrals('123')}
/>
<Button title="Fetch Update" onPress={checkAndFetchUpdate} />
<Button
title="Navigate to Settings"
onPress={() => BrownfieldNavigation.navigateToSettings()}
/>
<Button
title="Navigate to Referrals"
onPress={() => BrownfieldNavigation.navigateToReferrals('123')}
/>
<Button title="Fetch Update" onPress={checkAndFetchUpdate} />
</View>
</View>

<View style={styles.postMessageSection}>
<PostMessageTab />
</View>
</SafeAreaView>
);
Expand All @@ -45,7 +52,9 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#eeeeee',
paddingTop: 20,
},
header: {
flexShrink: 0,
},
title: {
fontSize: 20,
Expand All @@ -59,10 +68,15 @@ const styles = StyleSheet.create({
marginTop: 4,
},
content: {
flex: 1,
minHeight: 220,
justifyContent: 'center',
alignItems: 'center',
},
postMessageSection: {
flex: 1,
minHeight: 200,
marginTop: 8,
},
text: {
fontSize: 18,
textAlign: 'center',
Expand Down
2 changes: 2 additions & 0 deletions apps/ExpoApp54/app/(tabs)/postMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +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 ReactNativeBrownfield from '@callstack/react-native-brownfield';
import type { MessageEvent } from '@callstack/react-native-brownfield';

Expand Down Expand Up @@ -51,6 +52,7 @@ export default function PostMessageTab() {
return (
<ThemedView style={styles.messageSection}>
<TouchableOpacity
testID={brownfieldE2eTestIds.sendMessageToNative}
style={styles.sendButton}
onPress={sendMessage}
activeOpacity={0.8}
Expand Down
10 changes: 9 additions & 1 deletion apps/ExpoApp54/components/postMessage/MessageBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Animated, StyleSheet } from 'react-native';
import { Message } from './Message';
import { useEffect, useRef } from 'react';
import { brownfieldE2eTestIds } from '@callstack/brownfield-example-shared-tests/e2eTestIds';
import { ThemedText } from '@/components/themed-text';

export function MessageBubble({ item }: { item: Message }) {
Expand Down Expand Up @@ -40,7 +41,14 @@ export function MessageBubble({ item }: { item: Message }) {
<ThemedText style={styles.bubbleLabel}>
{isFromNative ? 'From Native' : 'From RN'}
</ThemedText>
<ThemedText style={styles.bubbleText}>{item.text}</ThemedText>
<ThemedText
style={styles.bubbleText}
testID={
isFromNative ? undefined : brownfieldE2eTestIds.rnPostMessageText
}
>
{item.text}
</ThemedText>
</Animated.View>
);
}
Expand Down
Loading
Loading