Test Expo Camera on the iOS Simulator and Android Emulator With Your AI Agent
The iOS Simulator has no camera. Expo's own Simulator guide lists the camera among the hardware the Simulator can't provide. The Expo Camera reference marks the component as device-only on both iOS and Android. The Android Emulator does expose a camera, but it's an emulated scene, a virtual scene, or your host webcam, and it supports fixed PNG or JPEG images in a virtual scene. Webcam input and changes to the scene or camera position can make captures vary.
That's a problem for any feature built around a capture: scanning a receipt for OCR, uploading an ID or a profile photo, identifying a plant from a picture, recording a workout clip. Testing those flows needs a deliberate input: a particular receipt, ID, portrait, plant photo, or clip. The iOS Simulator needs a camera substitute, and an Android virtual scene needs the image and camera position configured for the scenario.
Ceraph makes the camera deterministic in development builds. Ceraph is an MCP server that lets your coding agent drive and test a React Native or Expo app end-to-end. You register a test image or video, and the capture call returns that exact file on the iOS Simulator, the Android Emulator, or a device. Your agent can test the full camera flow with the same input every run, and see what your app does with it.
How Ceraph mocks the camera in React Native
Ceraph's camera support is built for Expo Camera (expo-camera). You use CeraphCamera from @ceraph/react-native-mcp/shim in place of CameraView. It accepts the same props as Expo Camera, and ceraph init offers to replace the CameraView components it finds for you.
Each camera gets a mediaKey that names its scenario:
import { CeraphCamera } from "@ceraph/react-native-mcp/shim";
export function ScanReceiptScreen() {
return <CeraphCamera mediaKey="receipt" style={{ flex: 1 }} ref={cameraRef} />;
}
In a development build:
takePictureAsyncreturns the image registered for that key.recordAsyncreturns the video registered for that key.- One key can have both, for screens that support photo and video capture.
The fixtures are ordinary files in your project, stored under a name that matches the key:
.ceraph/camera-images/receipt.jpg
.ceraph/camera-videos/workout.mp4
Your agent adds them itself with ceraph_add_camera_image and ceraph_add_camera_video. Images can be JPEG, PNG, WebP, or HEIC up to 5 MB. Videos can be MP4, MOV, or M4V up to 25 MB. If a key has no image fixture, development capture returns the project's default image, or Ceraph's built-in placeholder if there isn't one. If it has no video fixture, recording uses the project's default video, so register a clip for any screen that records. Because fixtures live in the repo and Ceraph keeps only its generated files out of git, you can commit them so every teammate's agent tests against the same inputs.
Simulators, emulators, and physical devices
Defaults follow the runtime. iOS Simulators and Android Emulators use test media, so captures use the project's registered fixtures. Physical devices use the real camera, so a phone in your hand still works as a phone.
Your agent can change this on a running development app with ceraph_set_camera_test_media without rebuilding. For example, it can turn test media on for a physical iPhone so a scan flow is reproducible on real hardware, then turn it off again. CERAPH_CAMERA_TEST_MEDIA=on or off fixes the choice and takes precedence over runtime switching; auto keeps runtime switching available. The cameraTestMedia preference saves your choice.
Production builds always use the real camera. Camera fixtures and test behavior are development-only and are left out of production builds.
Testing photo library pickers
Many camera features also let the user choose an existing photo instead. That flow goes through the system picker, which needs something in the library to pick, so Ceraph's ceraph_add_media seeds the runtime's photo library and the agent drives your app's real picker. The full walkthrough, including the manual simctl/adb route, is in testing image picker and photo library flows.
Walkthrough: testing a receipt scanner
Say your Expo app has a receipt scanner. The user photographs a receipt, your backend runs OCR, and the next screen shows the parsed total. You've changed how totals are parsed, and you ask your agent to test the scan flow on the iOS Simulator.
- Register the input. The agent calls
ceraph_add_camera_imagewith a real receipt photo under the keyreceipt. The expected total is printed right on it. - Bring the app up.
ceraph_startgets the development build running on the Simulator and ready to test. - Go to the scanner. The agent reads the screen with
ceraph_snapshot, then taps through to the scan screen or opens it withscreen_open_url. - Capture. The agent taps the shutter.
takePictureAsyncreturnsreceipt.jpg, your upload and OCR code run on it, and the app moves to the result screen. - Verify the result. The agent takes another snapshot and checks that the parsed total matches the receipt. Since the input is the same every run, a wrong total is a real regression.
- Read the errors. If the upload failed or the parser threw,
rn_get_errorsputs the error in the conversation. The agent reports the actual failure and doesn't call the flow passed.
The same pattern covers other capture flows: a profile photo that should be cropped and uploaded, a plant photo that should return the right species, or a recorded clip that recordAsync should hand to your rep counter.
Where camera testing fits
Camera and gallery testing fits the same agent loop as the rest of Ceraph: start the app, read the screen, act, read it again, check the errors. More on that loop in the Playwright MCP for React Native.
Try it on your camera flow: get started.