Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 872ed4a

Browse files
committedFeb 26, 2025·
feat: add CI/CD maestro tests
[test] upload screenshots as artifacts
1 parent 45739aa commit 872ed4a

File tree

8 files changed

+365
-87
lines changed

8 files changed

+365
-87
lines changed
 

‎.github/workflows/ci.yml

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: React Native CI
2+
3+
on:
4+
pull_request:
5+
branches: main
6+
push:
7+
branches: main
8+
schedule:
9+
- cron: '0 0 * * *' # Runs at 00:00 UTC every day
10+
11+
jobs:
12+
ios-build:
13+
name: iOS Build
14+
runs-on: macos-latest
15+
defaults:
16+
run:
17+
working-directory: example
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v3
22+
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v3
25+
with:
26+
node-version: '22'
27+
cache: 'npm'
28+
cache-dependency-path: example/package-lock.json
29+
30+
- name: Cache CocoaPods
31+
uses: actions/cache@v3
32+
with:
33+
path: |
34+
example/ios/Pods
35+
key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }}
36+
restore-keys: |
37+
${{ runner.os }}-pods-
38+
39+
- name: Install dependencies
40+
run: |
41+
npm install --frozen-lockfile
42+
cd ios && pod install
43+
44+
- name: Install Maestro CLI
45+
run: |
46+
curl -Ls "https://get.maestro.mobile.dev" | bash
47+
brew tap facebook/fb
48+
brew install facebook/fb/idb-companion
49+
50+
- name: Add Maestro to path
51+
run: echo "${HOME}/.maestro/bin" >> $GITHUB_PATH
52+
53+
- name: Start packager
54+
run: npm start &
55+
56+
- name: Build iOS
57+
run: |
58+
npm run ios
59+
60+
- name: Setup iOS simulator
61+
run: |
62+
UDID=$(xcrun simctl list devices | grep "iPhone" | grep "Booted" | head -1 | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
63+
if [ -z "$UDID" ]; then
64+
UDID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
65+
xcrun simctl boot "${UDID}"
66+
fi
67+
open -a Simulator
68+
xcrun simctl launch "${UDID}" com.jscexample
69+
70+
- name: Run iOS tests
71+
run: |
72+
export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000
73+
export MAESTRO_WAIT_TIMEOUT=10000
74+
npm run test:e2e
75+
76+
77+
android-build:
78+
name: Android Build
79+
runs-on: ubuntu-latest
80+
defaults:
81+
run:
82+
working-directory: example
83+
84+
steps:
85+
- name: Checkout repository
86+
uses: actions/checkout@v3
87+
88+
- name: Setup Node.js
89+
uses: actions/setup-node@v3
90+
with:
91+
node-version: '18'
92+
cache: 'npm'
93+
cache-dependency-path: example/package-lock.json
94+
95+
- name: Setup Java
96+
uses: actions/setup-java@v3
97+
with:
98+
distribution: 'zulu'
99+
java-version: '17'
100+
101+
- name: Install dependencies
102+
run: npm install --frozen-lockfile
103+
104+
- name: Start packager
105+
run: npm start &
106+
107+
- name: Install Maestro CLI
108+
run: |
109+
curl -Ls "https://get.maestro.mobile.dev" | bash
110+
111+
- name: Add Maestro to path
112+
run: echo "${HOME}/.maestro/bin" >> $GITHUB_PATH
113+
114+
- name: Create AVD and generate snapshot for caching
115+
uses: reactivecircus/android-emulator-runner@v2
116+
with:
117+
target: aosp_atd
118+
api-level: 30
119+
arch: x86
120+
ram-size: 4096M
121+
channel: canary
122+
profile: pixel
123+
avd-name: Pixel_3a_API_30_AOSP
124+
force-avd-creation: false
125+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
126+
emulator-boot-timeout: 12000
127+
disable-animations: false
128+
working-directory: example
129+
script: |
130+
npm run android
131+
npm run test:e2e

‎example/App.tsx

Lines changed: 142 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
/**
2-
* Sample React Native App
3-
* https://github.com/facebook/react-native
4-
*
5-
* @format
6-
*/
7-
8-
import React from 'react';
9-
import type {PropsWithChildren} from 'react';
1+
import React, { useState } from 'react';
102
import {
3+
Button,
114
SafeAreaView,
125
ScrollView,
136
StatusBar,
@@ -16,52 +9,127 @@ import {
169
useColorScheme,
1710
View,
1811
} from 'react-native';
19-
2012
import {
2113
Colors,
22-
DebugInstructions,
2314
Header,
24-
LearnMoreLinks,
25-
ReloadInstructions,
2615
} from 'react-native/Libraries/NewAppScreen';
2716

28-
type SectionProps = PropsWithChildren<{
29-
title: string;
30-
}>;
31-
32-
function Section({children, title}: SectionProps): React.JSX.Element {
33-
const isDarkMode = useColorScheme() === 'dark';
34-
return (
35-
<View style={styles.sectionContainer}>
36-
<Text
37-
style={[
38-
styles.sectionTitle,
39-
{
40-
color: isDarkMode ? Colors.white : Colors.black,
41-
},
42-
]}>
43-
{title}
44-
</Text>
45-
<Text
46-
style={[
47-
styles.sectionDescription,
48-
{
49-
color: isDarkMode ? Colors.light : Colors.dark,
50-
},
51-
]}>
52-
{children}
53-
</Text>
54-
</View>
55-
);
56-
}
57-
5817
function App(): React.JSX.Element {
5918
const isDarkMode = useColorScheme() === 'dark';
19+
const [testResult, setTestResult] = useState('No test run yet');
20+
const [testName, setTestName] = useState('');
6021

6122
const backgroundStyle = {
6223
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
6324
};
6425

26+
const testConsoleLog = () => {
27+
console.log('Hello from JSC');
28+
setTestName('Console Test Result');
29+
setTestResult('Hello from JSC');
30+
};
31+
32+
const testBasicOperations = () => {
33+
const mathResult = 2 + 2;
34+
const stringResult = 'Hello ' + 'World';
35+
const arrayResult = [1, 2, 3].map(x => x * 2);
36+
37+
const result = `Math: ${mathResult}\nString: ${stringResult}\nArray: ${arrayResult}`;
38+
console.log(result);
39+
setTestName('Basic Operations Result');
40+
setTestResult(result);
41+
};
42+
43+
const testComplexOperations = () => {
44+
const obj = { a: 1, b: 2 };
45+
const square = (x: number) => x * x;
46+
const squareResult = square(4);
47+
48+
let result = `Object: ${JSON.stringify(obj)}\nSquare(4): ${squareResult}`;
49+
50+
try {
51+
// eslint-disable-next-line no-eval
52+
const dynamicFn = eval('(x) => x * 3');
53+
const dynamicResult = dynamicFn(4);
54+
result += `\nDynamic function(4): ${dynamicResult}`;
55+
} catch (error) {
56+
result += `\nDynamic function error: ${error}`;
57+
}
58+
59+
console.log(result);
60+
setTestName('Complex Operations Result');
61+
setTestResult(result);
62+
};
63+
64+
const testGlobalAccess = () => {
65+
const result = `SetTimeout exists: ${typeof global.setTimeout === 'function'}`;
66+
console.log(result);
67+
setTestName('Global Access Result');
68+
setTestResult(result);
69+
};
70+
71+
const testErrorHandling = () => {
72+
let results: string[] = [];
73+
74+
try {
75+
throw new Error('Custom error');
76+
} catch (error) {
77+
if (error instanceof Error) {
78+
results.push(`Regular error: ${error.message}`);
79+
}
80+
}
81+
82+
try {
83+
const undefined1 = undefined;
84+
// @ts-ignore
85+
undefined1.someMethod();
86+
} catch (error) {
87+
if (error instanceof Error) {
88+
results.push(`Type error: ${error.message}`);
89+
}
90+
}
91+
92+
try {
93+
// eslint-disable-next-line no-eval
94+
eval('syntax error{');
95+
} catch (error) {
96+
if (error instanceof Error) {
97+
results.push(`Eval error: ${error.message}`);
98+
}
99+
}
100+
101+
const result = results.join('\n');
102+
console.log(result);
103+
setTestName('Error Handling Result');
104+
setTestResult(result);
105+
};
106+
107+
const testAsync = async () => {
108+
try {
109+
const result = await new Promise((resolve) => {
110+
setTimeout(() => resolve('Regular async completed'), 1000);
111+
});
112+
console.log('Regular async result:', result);
113+
setTestName('Async Test Result');
114+
setTestResult(String(result));
115+
} catch (error) {
116+
setTestName('Async Error');
117+
setTestResult(String(error));
118+
}
119+
};
120+
121+
const testMemoryAndPerformance = () => {
122+
const arr = new Array(1000000);
123+
for (let i = 0; i < arr.length; i++) {
124+
arr[i] = i;
125+
}
126+
const result = `Array length: ${arr.length}`;
127+
128+
console.log(result);
129+
setTestName('Memory & Performance Result');
130+
setTestResult(result);
131+
};
132+
65133
return (
66134
<SafeAreaView style={backgroundStyle}>
67135
<StatusBar
@@ -73,45 +141,47 @@ function App(): React.JSX.Element {
73141
style={backgroundStyle}>
74142
<Header />
75143
<View
76-
style={{
77-
backgroundColor: isDarkMode ? Colors.black : Colors.white,
78-
}}>
79-
<Section title="Step One">
80-
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
81-
screen and then come back to see your edits.
82-
</Section>
83-
<Section title="See Your Changes">
84-
<ReloadInstructions />
85-
</Section>
86-
<Section title="Debug">
87-
<DebugInstructions />
88-
</Section>
89-
<Section title="Learn More">
90-
Read the docs to discover what to do next:
91-
</Section>
92-
<LearnMoreLinks />
144+
style={[
145+
styles.container,
146+
{backgroundColor: isDarkMode ? Colors.black : Colors.white},
147+
]}>
148+
<Button title="Console Log Test" onPress={testConsoleLog} />
149+
<Button title="Basic Operations" onPress={testBasicOperations} />
150+
<Button title="Complex Operations" onPress={testComplexOperations} />
151+
<Button title="Global Access Test" onPress={testGlobalAccess} />
152+
<Button title="Error Handling Test" onPress={testErrorHandling} />
153+
<Button title="Async Test" onPress={testAsync} />
154+
<Button title="Memory & Performance" onPress={testMemoryAndPerformance} />
155+
<View style={styles.resultContainer}>
156+
<Text style={styles.resultTitle} testID="resultTitle">
157+
{testName || 'Test Results'}
158+
</Text>
159+
<Text style={styles.resultContent} testID="resultContent">
160+
{testResult}
161+
</Text>
162+
</View>
93163
</View>
94164
</ScrollView>
95165
</SafeAreaView>
96166
);
97167
}
98168

99169
const styles = StyleSheet.create({
100-
sectionContainer: {
101-
marginTop: 32,
102-
paddingHorizontal: 24,
170+
container: {
171+
padding: 12,
103172
},
104-
sectionTitle: {
105-
fontSize: 24,
106-
fontWeight: '600',
173+
resultContainer: {
174+
marginTop: 20,
175+
padding: 10,
176+
backgroundColor: '#f0f0f0',
107177
},
108-
sectionDescription: {
109-
marginTop: 8,
110-
fontSize: 18,
111-
fontWeight: '400',
178+
resultTitle: {
179+
fontSize: 16,
180+
fontWeight: 'bold',
181+
marginBottom: 8,
112182
},
113-
highlight: {
114-
fontWeight: '700',
183+
resultContent: {
184+
fontSize: 14,
115185
},
116186
});
117187

‎example/ios/JSCExample.xcodeproj/project.pbxproj

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
LastUpgradeCheck = 1210;
131131
TargetAttributes = {
132132
13B07F861A680F5B00A75B9A = {
133-
LastSwiftMigration = 1120;
133+
LastSwiftMigration = 1600;
134134
};
135135
};
136136
};
@@ -255,7 +255,7 @@
255255
"-ObjC",
256256
"-lc++",
257257
);
258-
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
258+
PRODUCT_BUNDLE_IDENTIFIER = com.jscexample;
259259
PRODUCT_NAME = JSCExample;
260260
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
261261
SWIFT_VERSION = 5.0;
@@ -282,7 +282,7 @@
282282
"-ObjC",
283283
"-lc++",
284284
);
285-
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
285+
PRODUCT_BUNDLE_IDENTIFIER = com.jscexample;
286286
PRODUCT_NAME = JSCExample;
287287
SWIFT_VERSION = 5.0;
288288
VERSIONING_SYSTEM = "apple-generic";
@@ -363,10 +363,7 @@
363363
"-DFOLLY_CFG_NO_COROUTINES=1",
364364
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
365365
);
366-
OTHER_LDFLAGS = (
367-
"$(inherited)",
368-
" ",
369-
);
366+
OTHER_LDFLAGS = "$(inherited) ";
370367
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
371368
SDKROOT = iphoneos;
372369
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -440,10 +437,7 @@
440437
"-DFOLLY_CFG_NO_COROUTINES=1",
441438
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
442439
);
443-
OTHER_LDFLAGS = (
444-
"$(inherited)",
445-
" ",
446-
);
440+
OTHER_LDFLAGS = "$(inherited) ";
447441
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
448442
SDKROOT = iphoneos;
449443
USE_HERMES = false;

‎example/ios/JSCExample/Info.plist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
<true/>
2727
<key>NSAppTransportSecurity</key>
2828
<dict>
29-
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
3029
<key>NSAllowsArbitraryLoads</key>
3130
<false/>
3231
<key>NSAllowsLocalNetworking</key>

‎example/maestro-tests/base.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
appId: com.jscexample
2+
---
3+
- launchApp:
4+
appId: com.jscexample
5+
- takeScreenshot: MainScreen
6+
- assertVisible:
7+
text: "Welcome to React Native"
8+
9+
# Console Log Test
10+
- tapOn: "Console Log Test"
11+
- assertVisible:
12+
text: "Console Test Result"
13+
- assertVisible:
14+
text: "Hello from JSC"
15+
16+
# Basic Operations
17+
- tapOn: "Basic Operations"
18+
- assertVisible:
19+
text: "Basic Operations Result"
20+
- assertVisible:
21+
text: "Math: 4\nString: Hello World\nArray: 2,4,6"
22+
23+
# Complex Operations
24+
- tapOn: "Complex Operations"
25+
- assertVisible:
26+
text: "Complex Operations Result"
27+
- assertVisible:
28+
text: "Object: {\"a\":1,\"b\":2}\nSquare(4): 16\nDynamic function(4): 12"
29+
30+
# Global Access Test
31+
- tapOn: "Global Access Test"
32+
- assertVisible:
33+
text: "Global Access Result"
34+
- assertVisible:
35+
text: "SetTimeout exists: true"
36+
37+
# Error Handling Test
38+
- tapOn: "Error Handling Test"
39+
- assertVisible:
40+
text: "Error Handling Result"
41+
- assertVisible:
42+
text: "Regular error: Custom error\nType error: undefined is not an object (evaluating 'undefined1.someMethod')\nEval error: Unexpected identifier 'error'"
43+
44+
# Async Test
45+
- tapOn: "Async Test"
46+
- assertVisible:
47+
text: "Async Test Result"
48+
- assertVisible:
49+
text: "Regular async completed"
50+
51+
# Memory & Performance Test
52+
- tapOn: "Memory & Performance"
53+
- assertVisible:
54+
text: "Memory & Performance Result"
55+
- assertVisible:
56+
text: "Array length: 1000000"

‎example/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎example/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
"version": "0.0.1",
44
"private": true,
55
"scripts": {
6-
"android": "react-native run-android",
7-
"ios": "react-native run-ios",
6+
"android": "react-native run-android --mode release",
7+
"ios": "react-native run-ios --mode Release",
88
"lint": "eslint .",
99
"start": "react-native start",
10-
"test": "jest"
10+
"test": "jest",
11+
"mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"",
12+
"build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --bundle-output dist/main.android.jsbundle --assets-dest dist/res --dev false",
13+
"build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --bundle-output dist/main.ios.jsbundle --assets-dest dist --dev false",
14+
"test:e2e": "maestro test maestro-tests",
15+
"postinstall": "npx patch-package"
1116
},
1217
"dependencies": {
1318
"react": "19.0.0",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
diff --git a/node_modules/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm b/node_modules/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm
2+
index a8f3644..8431750 100644
3+
--- a/node_modules/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm
4+
+++ b/node_modules/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm
5+
@@ -38,6 +38,7 @@
6+
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
7+
#import <react/renderer/runtimescheduler/RuntimeSchedulerCallInvoker.h>
8+
#import <react/runtime/JSRuntimeFactory.h>
9+
+#import <React-jsc/RCTJscInstance.h>
10+
11+
@implementation RCTRootViewFactoryConfiguration
12+
13+
@@ -269,7 +270,8 @@
14+
#elif USE_THIRD_PARTY_JSC != 1
15+
return std::make_shared<facebook::react::RCTJscInstance>();
16+
#else
17+
- throw std::runtime_error("No JSRuntimeFactory specified.");
18+
+ return std::make_shared<facebook::react::RCTJscInstance>();
19+
+// throw std::runtime_error("No JSRuntimeFactory specified.");
20+
#endif
21+
}
22+

0 commit comments

Comments
 (0)
Please sign in to comment.