Skip to content

Commit a28ed3f

Browse files
grichaclaude
andauthored
Add Maestro E2E tests for mobile app (#20)
* Add Maestro E2E tests for mobile app - Add Maestro test flows for app launch and setup validation - Add testIDs to SetupScreen and WorkspaceDetailScreen components - Create GitHub Actions workflow for iOS simulator testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix Maestro CI: add Expo prebuild step and fix bundle ID - Add expo prebuild step to generate iOS native code in CI - Update iOS bundle identifier to com.gricha.perry to match local dev setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix CI: move Xcode selection before CocoaPods install React Native 0.81 requires Xcode >= 16.1, so we need to select the right version before running pod install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix Maestro test: use eraseText instead of clearText clearText is not a valid Maestro command, the correct command is eraseText. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Optimize Maestro CI and fix build configuration - Only run on push to main, manual trigger, or weekly schedule (not PRs) - Use Release configuration instead of Debug (no Metro required) - Add Xcode derived data caching to speed up subsequent builds - Add debug screenshot step to diagnose test failures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Re-enable PR trigger for mobile E2E tests Need to test the workflow on the PR itself before merging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7f9a323 commit a28ed3f

6 files changed

Lines changed: 207 additions & 1 deletion

File tree

.github/workflows/mobile-e2e.yml

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
name: Mobile E2E Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'mobile/**'
8+
pull_request:
9+
branches: [main]
10+
paths:
11+
- 'mobile/**'
12+
workflow_dispatch:
13+
# Weekly run to catch regressions
14+
schedule:
15+
- cron: '0 9 * * 1' # Monday 9am UTC
16+
17+
jobs:
18+
maestro-ios:
19+
runs-on: macos-14
20+
timeout-minutes: 45
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Set up Bun
26+
uses: oven-sh/setup-bun@v2
27+
with:
28+
bun-version: latest
29+
30+
- name: Cache bun dependencies
31+
uses: actions/cache@v4
32+
with:
33+
path: |
34+
~/.bun/install/cache
35+
mobile/node_modules
36+
key: ${{ runner.os }}-bun-mobile-${{ hashFiles('mobile/bun.lock') }}
37+
restore-keys: |
38+
${{ runner.os }}-bun-mobile-
39+
40+
- name: Install dependencies
41+
working-directory: mobile
42+
run: bun install
43+
44+
- name: Select Xcode version
45+
run: |
46+
# Try Xcode 16.2 first, fall back to 16.1 if not available
47+
if [ -d "/Applications/Xcode_16.2.app" ]; then
48+
sudo xcode-select -s /Applications/Xcode_16.2.app
49+
elif [ -d "/Applications/Xcode_16.1.app" ]; then
50+
sudo xcode-select -s /Applications/Xcode_16.1.app
51+
elif [ -d "/Applications/Xcode_16.app" ]; then
52+
sudo xcode-select -s /Applications/Xcode_16.app
53+
else
54+
echo "Available Xcode versions:"
55+
ls -d /Applications/Xcode*.app 2>/dev/null || echo "No Xcode found"
56+
exit 1
57+
fi
58+
xcodebuild -version
59+
60+
- name: Generate iOS native code
61+
working-directory: mobile
62+
run: bunx expo prebuild --platform ios --clean
63+
64+
- name: Cache CocoaPods
65+
uses: actions/cache@v4
66+
with:
67+
path: mobile/ios/Pods
68+
key: ${{ runner.os }}-pods-${{ hashFiles('mobile/ios/Podfile.lock') }}
69+
restore-keys: |
70+
${{ runner.os }}-pods-
71+
72+
- name: Install CocoaPods dependencies
73+
working-directory: mobile/ios
74+
run: pod install
75+
76+
- name: List available simulators
77+
run: xcrun simctl list devices available
78+
79+
- name: Boot iOS Simulator
80+
run: |
81+
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 16" | head -1 | grep -oE '[A-F0-9-]{36}')
82+
if [ -z "$DEVICE_ID" ]; then
83+
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 15" | head -1 | grep -oE '[A-F0-9-]{36}')
84+
fi
85+
if [ -z "$DEVICE_ID" ]; then
86+
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -oE '[A-F0-9-]{36}')
87+
fi
88+
echo "DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV
89+
xcrun simctl boot "$DEVICE_ID" || true
90+
xcrun simctl bootstatus "$DEVICE_ID" -b
91+
92+
- name: Cache Xcode build
93+
uses: actions/cache@v4
94+
with:
95+
path: mobile/ios/build
96+
key: ${{ runner.os }}-xcode-${{ hashFiles('mobile/ios/Podfile.lock', 'mobile/src/**/*.ts', 'mobile/src/**/*.tsx') }}
97+
restore-keys: |
98+
${{ runner.os }}-xcode-
99+
100+
- name: Build iOS app for simulator
101+
working-directory: mobile/ios
102+
run: |
103+
xcodebuild -workspace Perry.xcworkspace \
104+
-scheme Perry \
105+
-configuration Release \
106+
-sdk iphonesimulator \
107+
-destination "id=${{ env.DEVICE_ID }}" \
108+
-derivedDataPath build \
109+
build
110+
111+
- name: Install app on simulator
112+
run: |
113+
APP_PATH=$(find mobile/ios/build -name "*.app" -type d | head -1)
114+
xcrun simctl install "${{ env.DEVICE_ID }}" "$APP_PATH"
115+
116+
- name: Install Maestro
117+
run: |
118+
curl -Ls "https://get.maestro.mobile.dev" | bash
119+
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
120+
121+
- name: Launch app and take debug screenshot
122+
run: |
123+
export PATH="$HOME/.maestro/bin:$PATH"
124+
xcrun simctl launch "${{ env.DEVICE_ID }}" com.gricha.perry || true
125+
sleep 5
126+
xcrun simctl io "${{ env.DEVICE_ID }}" screenshot /tmp/app-launch-debug.png || true
127+
128+
- name: Run Maestro tests
129+
working-directory: mobile
130+
run: |
131+
export PATH="$HOME/.maestro/bin:$PATH"
132+
maestro test .maestro/flows/ --format junit --output maestro-report.xml
133+
env:
134+
MAESTRO_DRIVER_STARTUP_TIMEOUT: 120000
135+
136+
- name: Upload debug screenshot
137+
uses: actions/upload-artifact@v4
138+
if: failure()
139+
with:
140+
name: debug-screenshot
141+
path: /tmp/app-launch-debug.png
142+
retention-days: 7
143+
144+
- name: Upload Maestro report
145+
uses: actions/upload-artifact@v4
146+
if: always()
147+
with:
148+
name: maestro-report
149+
path: mobile/maestro-report.xml
150+
retention-days: 7
151+
152+
- name: Upload Maestro screenshots
153+
uses: actions/upload-artifact@v4
154+
if: failure()
155+
with:
156+
name: maestro-screenshots
157+
path: ~/.maestro/tests/
158+
retention-days: 7
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
appId: com.gricha.perry
2+
name: App Launch - Fresh Install
3+
tags:
4+
- smoke
5+
- setup
6+
---
7+
- launchApp:
8+
clearState: true
9+
10+
# On fresh install, should show setup screen with Perry branding
11+
- assertVisible: "Perry"
12+
- assertVisible: "Connect to your workspace server"
13+
- assertVisible:
14+
id: "hostname-input"
15+
- assertVisible:
16+
id: "port-input"
17+
- assertVisible:
18+
id: "connect-button"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
appId: com.gricha.perry
2+
name: Setup Screen - Form Validation
3+
tags:
4+
- setup
5+
- validation
6+
---
7+
- launchApp:
8+
clearState: true
9+
10+
# Test empty hostname validation
11+
- tapOn:
12+
id: "connect-button"
13+
- assertVisible: "Please enter a hostname"
14+
15+
# Test port validation
16+
- tapOn:
17+
id: "hostname-input"
18+
- inputText: "test-server.local"
19+
- tapOn:
20+
id: "port-input"
21+
- eraseText
22+
- inputText: "0"
23+
- tapOn:
24+
id: "connect-button"
25+
- assertVisible: "Please enter a valid port number"

mobile/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"ios": {
1616
"supportsTablet": true,
17-
"bundleIdentifier": "com.subroutine.workspace"
17+
"bundleIdentifier": "com.gricha.perry"
1818
},
1919
"android": {
2020
"package": "com.subroutine.workspace",

mobile/src/screens/SetupScreen.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
7979
autoCapitalize="none"
8080
autoCorrect={false}
8181
keyboardType="url"
82+
testID="hostname-input"
8283
/>
8384
<Text style={styles.hint}>Your Tailscale hostname or IP address</Text>
8485
</View>
@@ -92,6 +93,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
9293
placeholder="7391"
9394
placeholderTextColor="#666"
9495
keyboardType="number-pad"
96+
testID="port-input"
9597
/>
9698
</View>
9799

@@ -105,6 +107,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
105107
style={[styles.button, isConnecting && styles.buttonDisabled]}
106108
onPress={handleConnect}
107109
disabled={isConnecting}
110+
testID="connect-button"
108111
>
109112
{isConnecting ? (
110113
<ActivityIndicator size="small" color="#fff" />

mobile/src/screens/WorkspaceDetailScreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,15 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
212212
style={styles.terminalBtn}
213213
onPress={() => navigation.navigate('Terminal', { name })}
214214
disabled={!isRunning}
215+
testID="terminal-button"
215216
>
216217
<Text style={[styles.terminalBtnText, !isRunning && styles.disabledText]}>Terminal</Text>
217218
</TouchableOpacity>
218219
<TouchableOpacity
219220
style={styles.newChatBtn}
220221
onPress={() => setShowNewChatPicker(!showNewChatPicker)}
221222
disabled={!isRunning}
223+
testID="new-chat-button"
222224
>
223225
<Text style={[styles.newChatBtnText, !isRunning && styles.disabledText]}>New Chat ▼</Text>
224226
</TouchableOpacity>

0 commit comments

Comments
 (0)