Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add rwmjhb/openclaw-skills --skill "ios-simulator-testing"
Install specific skill from multi-skill repository
# Description
Test and automate iOS Simulator apps without screen access. Use when the Mac is locked/headless, or when you need to drive a Flutter app on the iOS Simulator via CLI—screenshots, API testing, photo injection, batch workflows. Covers simctl commands, display-sleep detection, coordinate clicking, Flutter debug mode, and auth token extraction.
# SKILL.md
name: ios-simulator-testing
description: Test and automate iOS Simulator apps without screen access. Use when the Mac is locked/headless, or when you need to drive a Flutter app on the iOS Simulator via CLI—screenshots, API testing, photo injection, batch workflows. Covers simctl commands, display-sleep detection, coordinate clicking, Flutter debug mode, and auth token extraction.
iOS Simulator Testing (Headless / Locked Mac)
Core Constraint
When the Mac is locked or headless, loginwindow overlays (layer 2001/2004) block all mouse/keyboard events. Do NOT waste time on UI clicking. Go straight to CLI/API approaches.
Quick Detection: Is the Screen Locked?
# Compile once, reuse
cat > /tmp/windowat.swift << 'SWIFT'
import Foundation
import CoreGraphics
let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .optionOnScreenAboveWindow], kCGNullWindowID) as? [[String: Any]] ?? []
for win in windowList {
let owner = win["kCGWindowOwnerName"] as? String ?? ""
let layer = win["kCGWindowLayer"] as? Int ?? 0
if owner == "loginwindow" && layer > 2000 {
print("LOCKED")
Foundation.exit(0)
}
}
print("UNLOCKED")
SWIFT
swiftc /tmp/windowat.swift -o /tmp/windowat 2>&1 && /tmp/windowat
If LOCKED → skip all peekaboo/CGEvent clicking; use simctl + API only.
Simulator Basics
List booted devices
xcrun simctl list devices booted -j | python3 -c "
import json,sys
for rt,devs in json.loads(sys.stdin.read())['devices'].items():
for d in devs:
if d['state']=='Booted': print(f\"{d['name']}: {d['udid']}\")
"
Screenshot (always works, even locked)
xcrun simctl io <UDID> screenshot /tmp/sim.png
# Or use 'booted' — but with multiple devices, specify UDID
Launch / terminate apps
xcrun simctl launch <UDID> <bundle-id>
xcrun simctl terminate <UDID> <bundle-id>
Find bundle ID
xcrun simctl listapps <UDID> 2>/dev/null | grep CFBundleIdentifier | sort -u
Add photos to simulator library
xcrun simctl addmedia <UDID> photo1.jpg photo2.jpg
Photo library location
~/Library/Developer/CoreSimulator/Devices/<UDID>/data/Media/DCIM/100APPLE/
When Screen is UNLOCKED: UI Automation
Peekaboo (preferred)
peekaboo see --app Simulator --annotate --path /tmp/sim-see.png
peekaboo click --on elem_27 --app Simulator
Caveat: Peekaboo may return 0 elements for Simulator content or time out. If that happens, fall back to coordinate clicking.
Coordinate clicking (Swift CGEvent)
# Compile a reusable tap tool
cat > /tmp/tap.swift << 'SWIFT'
import Foundation, CoreGraphics
let x = Double(CommandLine.arguments[1])!
let y = Double(CommandLine.arguments[2])!
let point = CGPoint(x: x, y: y)
let m = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left)!
m.post(tap: CGEventTapLocation.cghidEventTap)
usleep(100000)
let d = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)!
d.post(tap: CGEventTapLocation.cghidEventTap)
usleep(50000)
let u = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)!
u.post(tap: CGEventTapLocation.cghidEventTap)
print("Tapped at \(x), \(y)")
SWIFT
swiftc /tmp/tap.swift -o /tmp/tap && /tmp/tap 1688 968
Finding window coordinates
peekaboo list windows --app Simulator --json > /tmp/w.json
python3 -c "
import json
for w in json.load(open('/tmp/w.json'))['data']['windows']:
t=w.get('title',''); b=w.get('bounds','')
if 'iPhone' in t: print(f'{t}: origin={b[0]} size={b[1]}')
"
iOS screen maps to window content area (minus ~22px title bar). iPhone 16 Pro logical: 393×852pt.
Wake display (if asleep but not locked)
caffeinate -u -t 5
Flutter Debug Mode: VM Service Control
When the app needs programmatic navigation without UI clicking.
Launch in debug mode
cd <flutter-project>
flutter run -d <UDID> --no-pub
# Outputs: A Dart VM Service on iPhone 16 Pro is available at: http://127.0.0.1:<port>/<token>=/
Hot reload: send r to stdin. Hot restart: send R.
Important rule
Never modify the project's existing source code for testing. Only create standalone test files (integration tests, scripts).
API-Level Testing (Best for Locked Mac)
The most reliable approach when the screen is locked. Directly call the app's backend APIs.
Extract auth token from simulator
# Flutter apps using Supabase store tokens in UserDefaults
PLIST="<app-container>/Library/Preferences/<bundle-id>.plist"
# Find container path
CONTAINER=$(xcrun simctl get_app_container <UDID> <bundle-id> data)
# Extract token
plutil -p "$PLIST" | python3 -c "
import sys, json, re
text = sys.stdin.read()
match = re.search(r'\"flutter\.sb-.*?auth-token\" => \"(.+?)\"$', text, re.MULTILINE | re.DOTALL)
if match:
data = json.loads(match.group(1))
print(data['access_token'])
"
Note: The plutil -extract command fails on keys with dots. Use plutil -p + regex instead.
API test pattern
TOKEN="<extracted-token>"
API="http://localhost:4001"
# Health check
curl -s "$API/health"
# Authenticated request
curl -s "$API/api/v1/endpoint" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
# File upload (multipart)
curl -s -X POST "$API/api/v1/storage/upload" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/image.jpg" \
-F "path=target/storage/path.jpg"
Gotchas & Lessons Learned
| Issue | Cause | Solution |
|---|---|---|
peekaboo click reports loginwindow |
Mac screen locked; loginwindow overlays at layer 2001+ | Use simctl/API instead |
| peekaboo see returns 0 elements | Simulator content not exposed via macOS accessibility | Use simctl screenshot + image analysis |
screencapture not found |
Not in PATH on some setups | Use peekaboo image or xcrun simctl io screenshot |
CGWindowListCreateImage error |
Deprecated in macOS 15+; use ScreenCaptureKit | Use xcrun simctl io screenshot instead |
plutil -extract fails on dotted keys |
Dots interpreted as key path separators | Use plutil -p (pretty print) + regex |
| Hot reload doesn't change widget state | StatefulWidget preserves state on hot reload | Use hot restart (R) for state-dependent changes |
| Display asleep vs locked | caffeinate -u wakes display but loginwindow stays |
Must unlock to interact with UI; use API for locked |
| Multiple booted simulators | xcrun simctl io booted picks arbitrary device |
Always specify UDID explicitly |
| App container path changes on rebuild | New container UUID each install | Always use xcrun simctl get_app_container dynamically |
Reference: See Also
references/batch-test-example.sh— Complete batch API test script
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.