Skip to content

feat: add maestro test on CI/CD #10

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Mar 5, 2025
Merged
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
131 changes: 131 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: React Native CI

on:
pull_request:
branches: main
push:
branches: main
schedule:
- cron: '0 0 * * *' # Runs at 00:00 UTC every day

jobs:
ios-build:
name: iOS Build
runs-on: macos-latest
defaults:
run:
working-directory: example

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: example/package-lock.json

- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: |
example/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-

- name: Install dependencies
run: |
npm install --frozen-lockfile
cd ios && pod install

- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
brew tap facebook/fb
brew install facebook/fb/idb-companion

- name: Add Maestro to path
run: echo "${HOME}/.maestro/bin" >> $GITHUB_PATH

- name: Start packager
run: npm start &

- name: Build iOS
run: |
npm run ios

- name: Setup iOS simulator
run: |
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})")
if [ -z "$UDID" ]; then
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})")
xcrun simctl boot "${UDID}"
fi
open -a Simulator
xcrun simctl launch "${UDID}" com.jscexample

- name: Run iOS tests
run: |
export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000
export MAESTRO_WAIT_TIMEOUT=10000
npm run test:e2e


android-build:
name: Android Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: example

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: example/package-lock.json

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'

- name: Install dependencies
run: npm install --frozen-lockfile

- name: Start packager
run: npm start &

- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash

- name: Add Maestro to path
run: echo "${HOME}/.maestro/bin" >> $GITHUB_PATH

- name: Create AVD and generate snapshot for caching
uses: reactivecircus/android-emulator-runner@v2
with:
target: aosp_atd
api-level: 30
arch: x86
ram-size: 4096M
channel: canary
profile: pixel
avd-name: Pixel_3a_API_30_AOSP
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-boot-timeout: 12000
disable-animations: false
working-directory: example
script: |
npm run android
npm run test:e2e
214 changes: 142 additions & 72 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/

import React from 'react';
import type {PropsWithChildren} from 'react';
import React, { useState } from 'react';
import {
Button,
SafeAreaView,
ScrollView,
StatusBar,
Expand All @@ -16,52 +9,127 @@ import {
useColorScheme,
View,
} from 'react-native';

import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

type SectionProps = PropsWithChildren<{
title: string;
}>;

function Section({children, title}: SectionProps): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
}

function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const [testResult, setTestResult] = useState('No test run yet');
const [testName, setTestName] = useState('');

const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};

const testConsoleLog = () => {
console.log('Hello from JSC');
setTestName('Console Test Result');
setTestResult('Hello from JSC');
};

const testBasicOperations = () => {
const mathResult = 2 + 2;
const stringResult = 'Hello ' + 'World';
const arrayResult = [1, 2, 3].map(x => x * 2);

const result = `Math: ${mathResult}\nString: ${stringResult}\nArray: ${arrayResult}`;
console.log(result);
setTestName('Basic Operations Result');
setTestResult(result);
};

const testComplexOperations = () => {
const obj = { a: 1, b: 2 };
const square = (x: number) => x * x;
const squareResult = square(4);

let result = `Object: ${JSON.stringify(obj)}\nSquare(4): ${squareResult}`;

try {
// eslint-disable-next-line no-eval
const dynamicFn = eval('(x) => x * 3');
const dynamicResult = dynamicFn(4);
result += `\nDynamic function(4): ${dynamicResult}`;
} catch (error) {
result += `\nDynamic function error: ${error}`;
}

console.log(result);
setTestName('Complex Operations Result');
setTestResult(result);
};

const testGlobalAccess = () => {
const result = `SetTimeout exists: ${typeof global.setTimeout === 'function'}`;
console.log(result);
setTestName('Global Access Result');
setTestResult(result);
};

const testErrorHandling = () => {
let results: string[] = [];

try {
throw new Error('Custom error');
} catch (error) {
if (error instanceof Error) {
results.push(`Regular error: ${error.message}`);
}
}

try {
const undefined1 = undefined;
// @ts-ignore
undefined1.someMethod();
} catch (error) {
if (error instanceof Error) {
results.push(`Type error: ${error.message}`);
}
}

try {
// eslint-disable-next-line no-eval
eval('syntax error{');
} catch (error) {
if (error instanceof Error) {
results.push(`Eval error: ${error.message}`);
}
}

const result = results.join('\n');
console.log(result);
setTestName('Error Handling Result');
setTestResult(result);
};

const testAsync = async () => {
try {
const result = await new Promise((resolve) => {
setTimeout(() => resolve('Regular async completed'), 1000);
});
console.log('Regular async result:', result);
setTestName('Async Test Result');
setTestResult(String(result));
} catch (error) {
setTestName('Async Error');
setTestResult(String(error));
}
};

const testMemoryAndPerformance = () => {
const arr = new Array(1000000);
for (let i = 0; i < arr.length; i++) {
arr[i] = i;
}
const result = `Array length: ${arr.length}`;

console.log(result);
setTestName('Memory & Performance Result');
setTestResult(result);
};

return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
Expand All @@ -73,45 +141,47 @@ function App(): React.JSX.Element {
style={backgroundStyle}>
<Header />
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
style={[
styles.container,
{backgroundColor: isDarkMode ? Colors.black : Colors.white},
]}>
<Button title="Console Log Test" onPress={testConsoleLog} />
<Button title="Basic Operations" onPress={testBasicOperations} />
<Button title="Complex Operations" onPress={testComplexOperations} />
<Button title="Global Access Test" onPress={testGlobalAccess} />
<Button title="Error Handling Test" onPress={testErrorHandling} />
<Button title="Async Test" onPress={testAsync} />
<Button title="Memory & Performance" onPress={testMemoryAndPerformance} />
<View style={styles.resultContainer}>
<Text style={styles.resultTitle} testID="resultTitle">
{testName || 'Test Results'}
</Text>
<Text style={styles.resultContent} testID="resultContent">
{testResult}
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
container: {
padding: 12,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
resultContainer: {
marginTop: 20,
padding: 10,
backgroundColor: '#f0f0f0',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
resultTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
},
highlight: {
fontWeight: '700',
resultContent: {
fontSize: 14,
},
});

Expand Down
Loading
Loading