Skip to content
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
158 changes: 158 additions & 0 deletions .github/workflows/mobile-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Mobile E2E Tests

on:
push:
branches: [main]
paths:
- 'mobile/**'
pull_request:
branches: [main]
paths:
- 'mobile/**'
workflow_dispatch:
# Weekly run to catch regressions
schedule:
- cron: '0 9 * * 1' # Monday 9am UTC

jobs:
maestro-ios:
runs-on: macos-14
timeout-minutes: 45

steps:
- uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Cache bun dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
mobile/node_modules
key: ${{ runner.os }}-bun-mobile-${{ hashFiles('mobile/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-mobile-

- name: Install dependencies
working-directory: mobile
run: bun install

- name: Select Xcode version
run: |
# Try Xcode 16.2 first, fall back to 16.1 if not available
if [ -d "/Applications/Xcode_16.2.app" ]; then
sudo xcode-select -s /Applications/Xcode_16.2.app
elif [ -d "/Applications/Xcode_16.1.app" ]; then
sudo xcode-select -s /Applications/Xcode_16.1.app
elif [ -d "/Applications/Xcode_16.app" ]; then
sudo xcode-select -s /Applications/Xcode_16.app
else
echo "Available Xcode versions:"
ls -d /Applications/Xcode*.app 2>/dev/null || echo "No Xcode found"
exit 1
fi
xcodebuild -version

- name: Generate iOS native code
working-directory: mobile
run: bunx expo prebuild --platform ios --clean

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

- name: Install CocoaPods dependencies
working-directory: mobile/ios
run: pod install

- name: List available simulators
run: xcrun simctl list devices available

- name: Boot iOS Simulator
run: |
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 16" | head -1 | grep -oE '[A-F0-9-]{36}')
if [ -z "$DEVICE_ID" ]; then
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 15" | head -1 | grep -oE '[A-F0-9-]{36}')
fi
if [ -z "$DEVICE_ID" ]; then
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -oE '[A-F0-9-]{36}')
fi
echo "DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV
xcrun simctl boot "$DEVICE_ID" || true
xcrun simctl bootstatus "$DEVICE_ID" -b

- name: Cache Xcode build
uses: actions/cache@v4
with:
path: mobile/ios/build
key: ${{ runner.os }}-xcode-${{ hashFiles('mobile/ios/Podfile.lock', 'mobile/src/**/*.ts', 'mobile/src/**/*.tsx') }}
restore-keys: |
${{ runner.os }}-xcode-

- name: Build iOS app for simulator
working-directory: mobile/ios
run: |
xcodebuild -workspace Perry.xcworkspace \
-scheme Perry \
-configuration Release \
-sdk iphonesimulator \
-destination "id=${{ env.DEVICE_ID }}" \
-derivedDataPath build \
build

- name: Install app on simulator
run: |
APP_PATH=$(find mobile/ios/build -name "*.app" -type d | head -1)
xcrun simctl install "${{ env.DEVICE_ID }}" "$APP_PATH"

- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH

- name: Launch app and take debug screenshot
run: |
export PATH="$HOME/.maestro/bin:$PATH"
xcrun simctl launch "${{ env.DEVICE_ID }}" com.gricha.perry || true
sleep 5
xcrun simctl io "${{ env.DEVICE_ID }}" screenshot /tmp/app-launch-debug.png || true

- name: Run Maestro tests
working-directory: mobile
run: |
export PATH="$HOME/.maestro/bin:$PATH"
maestro test .maestro/flows/ --format junit --output maestro-report.xml
env:
MAESTRO_DRIVER_STARTUP_TIMEOUT: 120000

- name: Upload debug screenshot
uses: actions/upload-artifact@v4
if: failure()
with:
name: debug-screenshot
path: /tmp/app-launch-debug.png
retention-days: 7

- name: Upload Maestro report
uses: actions/upload-artifact@v4
if: always()
with:
name: maestro-report
path: mobile/maestro-report.xml
retention-days: 7

- name: Upload Maestro screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: maestro-screenshots
path: ~/.maestro/tests/
retention-days: 7
18 changes: 18 additions & 0 deletions mobile/.maestro/flows/01-app-launch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
appId: com.gricha.perry
name: App Launch - Fresh Install
tags:
- smoke
- setup
---
- launchApp:
clearState: true

# On fresh install, should show setup screen with Perry branding
- assertVisible: "Perry"
- assertVisible: "Connect to your workspace server"
- assertVisible:
id: "hostname-input"
- assertVisible:
id: "port-input"
- assertVisible:
id: "connect-button"
25 changes: 25 additions & 0 deletions mobile/.maestro/flows/02-setup-validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
appId: com.gricha.perry
name: Setup Screen - Form Validation
tags:
- setup
- validation
---
- launchApp:
clearState: true

# Test empty hostname validation
- tapOn:
id: "connect-button"
- assertVisible: "Please enter a hostname"

# Test port validation
- tapOn:
id: "hostname-input"
- inputText: "test-server.local"
- tapOn:
id: "port-input"
- eraseText
- inputText: "0"
- tapOn:
id: "connect-button"
- assertVisible: "Please enter a valid port number"
2 changes: 1 addition & 1 deletion mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.subroutine.workspace"
"bundleIdentifier": "com.gricha.perry"
},
"android": {
"package": "com.subroutine.workspace",
Expand Down
3 changes: 3 additions & 0 deletions mobile/src/screens/SetupScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
testID="hostname-input"
/>
<Text style={styles.hint}>Your Tailscale hostname or IP address</Text>
</View>
Expand All @@ -92,6 +93,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
placeholder="7391"
placeholderTextColor="#666"
keyboardType="number-pad"
testID="port-input"
/>
</View>

Expand All @@ -105,6 +107,7 @@ export function SetupScreen({ onComplete }: SetupScreenProps) {
style={[styles.button, isConnecting && styles.buttonDisabled]}
onPress={handleConnect}
disabled={isConnecting}
testID="connect-button"
>
{isConnecting ? (
<ActivityIndicator size="small" color="#fff" />
Expand Down
2 changes: 2 additions & 0 deletions mobile/src/screens/WorkspaceDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,15 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
style={styles.terminalBtn}
onPress={() => navigation.navigate('Terminal', { name })}
disabled={!isRunning}
testID="terminal-button"
>
<Text style={[styles.terminalBtnText, !isRunning && styles.disabledText]}>Terminal</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.newChatBtn}
onPress={() => setShowNewChatPicker(!showNewChatPicker)}
disabled={!isRunning}
testID="new-chat-button"
>
<Text style={[styles.newChatBtnText, !isRunning && styles.disabledText]}>New Chat ▼</Text>
</TouchableOpacity>
Expand Down