Visual Testing React Native Apps with Vizzly

Integrate Vizzly with Detox, Maestro, Appium, Fastlane, or plain simulator screenshots. Make visual quality part of your React Native development workflow with local TDD and seamless CI/CD integration.

If you’re building a React Native app, you already know visual bugs are sneaky. A layout shifts on Android but looks fine on iOS. A component renders perfectly in your simulator but breaks on real devices. By the time QA catches it (or worse, your users), you’re scrambling to fix what should’ve been caught during development.

Here’s the thing: visual quality shouldn’t be something you bolt on after the fact. It should be part of your development process - right there in your local workflow when you’re iterating on UI, and automatically checked in CI when you push code.

That’s exactly what Vizzly does. You iterate locally with vizzly tdd to see visual changes as you code, then seamlessly hand off to your team through automatic CI/CD builds for review. No separate testing phase. No context switching. Just visual quality baked into how you already work.

The best part? Vizzly works with whatever you’re already using to capture screenshots. Detox, Maestro, Appium, Fastlane, or even plain simulator captures. You’re not locked into some proprietary rendering pipeline. Grab screenshots however you want, send them to Vizzly, and get visual diffs, approvals, and team collaboration.

Let’s walk through how to integrate Vizzly with the most popular React Native testing tools.

Two Ways to Send Screenshots to Vizzly

Before we dive into specific frameworks, you should know there are two core patterns for getting screenshots into Vizzly:

1. Upload from a directory (the “just works” approach)

If your testing framework writes PNG files to disk, this is your path:

vizzly upload ./screenshots --wait

Dead simple. Point it at a folder, and you’re done.

2. Stream from your tests (programmatic approach)

When your framework gives you image buffers directly, you can send them to Vizzly in real-time:

import { vizzlyScreenshot } from '@vizzly-testing/cli/client';

await vizzlyScreenshot('Home-iOS', buffer, {
  device: 'iPhone 15 Pro',
  platform: 'iOS'
});

This gives you more control and lets you attach metadata like device name, platform, or test context.

Now let’s look at how this works with specific tools.


Detox: E2E Testing with Built-In Screenshot APIs

If you’re already using Detox for end-to-end tests, you’re in great shape. Detox has first-class screenshot support at both the device and element level.

Full-screen captures

const imagePath = await device.takeScreenshot('Home');

Element-level captures

const imagePath = await element(by.id('home.root'))
  .takeScreenshot('HomeCard');

Detox saves the image and gives you a path. Read it into a buffer and send it to Vizzly:

import fs from 'node:fs';
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';

it('home screen looks right', async () => {
  let imagePath = await device.takeScreenshot('Home');
  let buffer = fs.readFileSync(imagePath);

  await vizzlyScreenshot('Home-iOS', buffer, {
    platform: 'iOS',
    device: 'iPhone 15 Pro'
  });
});

Pro tip: Stabilize your status bars (time, battery, signal) to avoid flaky diffs from changing values. Detox docs cover using simctl status_bar on iOS and demo mode on Android to freeze those values.


Maestro: YAML-Based Flow Testing

Prefer describing your test flows in YAML? Maestro is incredibly ergonomic and has a built-in takeScreenshot command that writes PNGs to your workspace:

appId: com.example.app
---
- launchApp
- tapOn: "Log in"
- takeScreenshot: LoginScreen
- inputText:
    text: "user@example.com"
- tapOn: "Continue"
- takeScreenshot: Dashboard

Run your flows, then upload the output directory to Vizzly:

maestro test flows/ --test-output-dir .maestro-out
vizzly upload .maestro-out --wait

Maestro also auto-captures screenshots on failures, which is perfect for catching regressions in CI without extra code.


Appium/WebDriver: Universal Mobile Testing

Already invested in Appium? Keep your tests exactly as they are. Just grab screenshots using WebDriver’s standard API and send them to Vizzly:

// WebdriverIO-style
let base64 = await driver.takeScreenshot();
let buffer = Buffer.from(base64, 'base64');

await vizzlyScreenshot('Checkout-Android', buffer, {
  platform: 'Android',
  device: 'Pixel 7'
});

No need to rewrite anything. Add visual checks to your existing E2E suite.


Fastlane: Repurpose Your App Store Screenshots

If you’re already generating App Store screenshots with Fastlane’s snapshot (iOS) or screengrab (Android), you can repurpose that output for visual regression testing.

Fastlane saves screenshots to these directories by default:

  • iOS: fastlane/screenshots
  • Android: fastlane/metadata/android

Just point Vizzly at them:

# iOS
vizzly upload fastlane/screenshots \
  --build-name "iOS App Store" --wait

# Android
vizzly upload fastlane/metadata/android \
  --build-name "Android Play Store" --wait

You’re already capturing these images for marketing, might as well use them to prevent visual regressions too.


Plain Simulator/Emulator Screenshots: The Minimal Approach

Don’t need a full E2E framework? You can capture screenshots directly from simulators and emulators using built-in CLI tools.

iOS Simulator

xcrun simctl io booted screenshot ./screenshots/Home-iOS.png

Android Emulator/Device

adb shell screencap -p /sdcard/Home-Android.png
adb pull /sdcard/Home-Android.png ./screenshots/

Then upload the folder:

vizzly upload ./screenshots --wait

This is perfect for smoke tests or quick visual checks without the overhead of a full testing framework.


Component-Level Visual Testing with react-native-view-shot

Building a design system? Want pixel-perfect checks for individual components? Use react-native-view-shot to snapshot specific views:

import { captureRef } from 'react-native-view-shot';
import { vizzlyScreenshot } from '@vizzly-testing/cli/client';

let uri = await captureRef(ref, {
  format: 'png',
  result: 'base64'
});

let buffer = Buffer.from(uri, 'base64');

await vizzlyScreenshot('Button/Primary', buffer, {
  component: 'Button',
  variant: 'Primary'
});

This approach is great for catching subtle rendering issues in your component library before they make it to screens.


Local Development: The vizzly tdd Workflow

Here’s where Vizzly really shines. While you’re iterating on UI:

# Start the dashboard server
vizzly tdd start --open

# In another terminal, run your tests in watch mode
npm test -- --watch

The TDD dashboard opens in your browser and updates live as your tests call vizzlyScreenshot(). You get instant visual diffs, can review changes, and accept new baselines — all without leaving your development flow.

No waiting for CI. No manual screenshot comparisons. Just save your code, your tests re-run automatically, and you see exactly what changed visually in real-time.

This is the local TDD workflow that makes visual quality feel integrated into development instead of a separate testing phase.


CI Integration: GitHub Actions Example

Vizzly plugs into whatever CI you’re already running. Here’s a GitHub Actions workflow that runs Detox tests and uploads screenshots:

name: Visual Tests
on: [push, pull_request]

jobs:
  ui-visual:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - name: Install dependencies
        run: npm ci

      - name: Run Detox tests
        run: npm test

      - name: Upload screenshots to Vizzly
        env:
          VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
        run: npx vizzly upload ./screenshots --wait

The --wait flag makes the build fail if visual differences are detected, gating your PRs on visual approval.


Best Practices: Naming and Organization

A few tips to keep your visual testing organized:

Include platform and device in filenames: Home-iOS-iPhone15Pro.png and Home-Android-Pixel7.png make it obvious what you’re looking at in diffs.

Stabilize status bars: Freeze system UI (time, battery, signal) in your test environments to avoid flaky diffs from changing clock values.

Organize by type: Keep components/ separate from screens/ if you’re mixing component-level shots with full-screen captures. Makes it easier to navigate builds.


Which Integration Should You Pick?

Already on Detox? Start streaming buffers to Vizzly from your existing tests. You’ll have visual checks running in 10 minutes.

Prefer YAML flows? Maestro is super ergonomic and CI-friendly. Write flows, capture screenshots, upload the folder.

Existing Appium shop? Keep your tests exactly as they are. Add a few takeScreenshot() calls and send them to Vizzly.

Marketing screenshots lying around? Point Vizzly at your Fastlane output and get regression checks for free.

Need the simplest smoke check? Use simctl or adb to grab screenshots and upload the folder.

Building a design system? Capture individual components with react-native-view-shot for pixel-perfect checks.

The point is: you don’t need to change how you work. Vizzly fits into your existing workflow, whether that’s a full E2E suite or simple simulator screenshots.


Get Started

  1. Follow the Vizzly Quick Start to set your auth token and pick CLI vs SDK
  2. Choose one integration above and wire up a couple of critical screens
  3. Start vizzly tdd start --open locally while you iterate
  4. Add screenshot uploads to your CI pipeline

Visual quality becomes part of your development process, not a separate testing phase. That’s how it should be.


Questions? Feedback? I’d love to hear how you’re using Vizzly with React Native. Feel free to reach out.

Happy testing!

Ready to improve your visual workflow?

Start using Vizzly today and bring visual regression testing into the workflow described in this article.