How to Test Deep Links on React Native (Complete Guide)

How to Test Deep Links on React Native (Complete Guide) requires a thorough understanding of their implementation and potential failure points to ensure a robust user experience. Deep links are critic

January 10, 2026 · 14 min read · How-To Guides

How to Test Deep Links on React Native (Complete Guide) requires a thorough understanding of their implementation and potential failure points to ensure a robust user experience. Deep links are critical for navigation, user engagement, and marketing campaigns in React Native applications, allowing users to jump directly to specific content within an app from a web URL, email, or another application. However, their complex interplay with operating system intents, navigation stacks, and application lifecycle events makes them a frequent source of production bugs if not tested comprehensively. This guide will provide a complete framework for testing deep links in React Native, covering everything from manual verification to advanced automation strategies, ensuring your app handles all deep link scenarios gracefully.

Understanding React Native Deep Linking Mechanics

Before diving into testing, it's crucial to grasp how deep linking works in React Native. The core mechanism involves the operating system (iOS or Android) intercepting a URL and then passing it to your application. React Native, through libraries like react-navigation or react-native-navigation, then parses this URL to determine the target screen and any parameters.

iOS Universal Links and Custom Schemes

On iOS, deep linking primarily uses two methods: Universal Links and custom URL schemes.

Universal Links: These are standard HTTP/HTTPS links that your app registers to handle. When a user taps a Universal Link, iOS checks if any installed app is registered for that domain. If your app is, it opens directly to the specified content without going through Safari. This provides a seamless user experience and avoids security warnings associated with custom schemes. Implementing Universal Links requires:

  1. Associated Domains Entitlement: Adding applinks:yourdomain.com to your Xcode project's capabilities.
  2. Apple App Site Association (AASA) File: A JSON file hosted at https://yourdomain.com/.well-known/apple-app-site-association that lists the paths your app can handle.
  3. React Native App Configuration: Using Linking.getInitialURL() for app launch and Linking.addEventListener('url', callback) for ongoing deep link handling.

Custom URL Schemes: These are non-HTTP/HTTPS URLs like myapp://product/123. While simpler to implement, they require the user to confirm opening the app and can lead to a less polished experience. They are also prone to name collisions if multiple apps register the same scheme. Configuration involves:

  1. Xcode Info.plist: Adding URL types under CFBundleURLTypes.
  2. React Native App Configuration: Similar to Universal Links, Linking.getInitialURL() and Linking.addEventListener.

Android App Links and Intent Filters

Android's deep linking also has two main approaches: App Links and custom intent filters.

App Links: Android's equivalent of Universal Links. These are HTTP/HTTPS links verified to belong to your app. They offer the same benefits as Universal Links, directly opening your app without a disambiguation dialog. Implementation requires:

  1. Intent Filters in AndroidManifest.xml: Declaring with android.intent.action.VIEW, android.intent.category.DEFAULT, android.intent.category.BROWSABLE, and android.data schemes (http/https) for your domain and paths.
  2. Digital Asset Links JSON File: A JSON file hosted at https://yourdomain.com/.well-known/assetlinks.json to verify ownership of your domain.
  3. React Native App Configuration: Linking.getInitialURL() and Linking.addEventListener('url', callback).

Custom Intent Filters: Similar to iOS custom schemes, these use custom URI schemes like myapp://product/123. They also rely on in AndroidManifest.xml but specify custom schemes (e.g., android.data android:scheme="myapp"). Users might see a disambiguation dialog if multiple apps can handle the same intent.

React Navigation Integration

Most React Native apps use react-navigation for routing. It provides excellent integration for deep linking through its linking configuration property. This mapping object translates incoming URLs to navigation states.


// App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

const config = {
  screens: {
    Home: 'home',
    Profile: {
      path: 'profile/:userId',
      parse: {
        userId: (userId) => parseInt(userId),
      },
    },
    ProductDetail: 'product/:productId',
  },
};

const linking = {
  prefixes: ['https://yourapp.com', 'yourapp://'],
  config,
};

function App() {
  return (
    <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
        <Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

This configuration maps https://yourapp.com/profile/123 or yourapp://profile/123 to the Profile screen with userId as 123. The parser ensures userId is an integer.

Comprehensive Deep Link Test Matrix and Scenarios

A robust deep link test strategy requires covering a wide array of scenarios beyond just the happy path. The goal is to identify how the app behaves under various conditions, including expected inputs, malformed URLs, and different app states.

Core Functional Scenarios (Happy Path)

These test cases ensure the basic functionality of deep links.

Scenario IDTest Case DescriptionExpected ResultReact Native Specifics
DL-001App closed, open deep link to Home screen.App opens to Home screen.Linking.getInitialURL() handles launch.
DL-002App closed, open deep link to specific product (e.g., /product/123).App opens to ProductDetail screen for product 123.react-navigation parses productId.
DL-003App closed, open deep link to user profile (e.g., /profile/user456).App opens to Profile screen for user 456.react-navigation parses userId.
DL-004App in background, open deep link to Home screen.App foregrounds, navigates to Home.Linking.addEventListener handles foreground.
DL-005App in background, open deep link to specific product.App foregrounds, navigates to ProductDetail.Linking.addEventListener handles foreground.
DL-006App in foreground, open deep link to new screen.Navigates to new screen, pushing onto stack.Linking.addEventListener handles foreground.
DL-007App in foreground, open deep link to current screen (with different params).Screen re-renders with new params or navigates to new instance.Depends on react-navigation stack configuration (e.g., replace vs push).
DL-008Deep link contains query parameters (e.g., /search?query=reactnative).App navigates to Search screen, search input pre-filled.react-navigation handles query params.
DL-009Deep link contains encoded characters (e.g., /category/Electronics%20&%20Gadgets).App navigates to Category screen, decodes correctly.URL decoding handled by OS/JS.

Error Handling and Edge Cases

These scenarios test the app's resilience and error handling for deep links.

Scenario IDTest Case DescriptionExpected ResultReact Native Specifics
DL-E01Deep link to non-existent path (e.g., /nonexistent).App opens to a default fallback screen (e.g., Home or 404).react-navigation linking.config.fallback or NotFound screen.
DL-E02Deep link with invalid parameter format (e.g., /profile/abc if userId is int).App opens to default fallback or handles error on Profile screen.react-navigation parse function handles type errors.
DL-E03Deep link with missing required parameters (e.g., /product without productId).App opens to default fallback or product list.react-navigation linking.config should define required params.
DL-E04Deep link from an unverified domain (for Universal/App Links).App does not open, opens in browser, or prompts user.OS handles verification; react-native-app-auth for verification.
DL-E05Deep link with an unknown custom scheme (e.g., unknownapp://).App does not open.OS handles scheme registration.
DL-E06Extremely long deep link URL (e.g., many query params).App opens correctly, or gracefully handles URL length limits (if any).OS/browser URL length limits.
DL-E07Deep link to a screen requiring authentication when user is logged out.App opens to Login screen, then navigates to target after login.Auth flow integration with react-navigation (e.g., AuthNavigator).
DL-E08Deep link to a screen requiring authentication when user is logged in but session expired.App navigates to Login, then target after re-auth.Token refresh/re-auth logic.
DL-E09Multiple deep links opened in rapid succession.App handles sequentially, or navigates to last valid link.Debouncing Linking.addEventListener or specific navigation logic.
DL-E10Deep link with special characters in parameters (e.g., /?q=test&!@#$).Parameters decoded correctly.URL encoding/decoding.
DL-E11Deep link opened while an overlay (modal, alert) is active.Overlay remains, deep link navigates under it, or dismisses overlay.App's modal management logic.

System and Environment Specifics

These cover platform-specific behaviors and different app states.

Scenario IDTest Case DescriptionExpected ResultReact Native Specifics
DL-S01iOS: Universal Link when AASA file is misconfigured.App opens in Safari, not in app.AASA file validation (e.g., https://search.developer.apple.com/appsearch-validation-tool/).
DL-S02iOS: Universal Link when Associated Domains entitlement is missing.App opens in Safari.Xcode project settings.
DL-S03Android: App Link when Digital Asset Links file is misconfigured.App opens in browser or prompts disambiguation dialog.Assetlinks file validation.
DL-S04Android: App Link when intent filters are incorrect.App opens in browser or prompts disambiguation dialog.AndroidManifest.xml correctness.
DL-S05Deep link with no internet connection.App opens to offline state or error screen, possibly queues navigation.Network detection and offline handling.
DL-S06Deep link received during app update/reinstall.App handles navigation after update/first launch.Post-install/update logic.
DL-S07Deep link from different source apps (e.g., Email, Safari/Chrome, Notes, Messages, WhatsApp).Consistent behavior across sources.Source app's deep link invocation method.
DL-S08Deep link via QR code scanner.App opens to correct screen.QR scanner app's forwarding mechanism.
DL-S09Deep link with different prefixes (e.g., yourapp:// vs https://yourapp.com).Both prefixes work as expected.react-navigation linking.prefixes.

Accessibility and Security Considerations

Deep links can subtly impact accessibility and introduce security risks.

Scenario IDTest Case DescriptionExpected ResultReact Native Specifics
DL-A01Screen opened via deep link is fully accessible (VoiceOver/TalkBack).All elements are reachable and announced correctly.Standard accessibility testing on target screen.
DL-A02Deep link leading to sensitive information (e.g., user profile) when not authenticated.User is prompted to log in before viewing sensitive data.Authorization checks within react-navigation stack.
DL-A03Deep link with potential XSS payload in parameters (e.g., /?q=).Payload is sanitized and not executed.Input sanitization in React Native components.
DL-A04Deep link redirects to malicious external site if parameters are not validated.Redirection only to trusted domains, or user confirmation.Whitelisting domains for external links.

Manual Testing of React Native Deep Links

Manual testing is indispensable for deep links, especially for verifying end-to-end user flows, platform-specific behaviors, and edge cases that are difficult to automate.

Setting Up Your Environment

  1. Development Build: Ensure you have a debug build of your React Native app installed on a physical device or emulator/simulator.
  2. Deep Link Configuration: Verify your app.json, Info.plist, and AndroidManifest.xml are correctly configured for deep links as per your react-navigation setup.
  3. Test URLs: Prepare a list of deep link URLs to test, including all happy path, error, and edge cases.

Step-by-Step Manual Testing

#### 1. Testing Custom URL Schemes (e.g., yourapp://)

#### 2. Testing Universal Links / App Links (e.g., https://yourapp.com)

#### 3. Testing App States

#### 4. Parameter Validation

#### 5. User Experience

This manual approach provides immediate feedback and helps catch subtle UI/UX issues that automated tests might miss.

Automated Testing Strategies for React Native Deep Links

Automating deep link tests is crucial for regression and ensuring consistent behavior across releases. While full end-to-end UI automation can be complex, there are several effective strategies.

Unit and Integration Testing for Deep Link Parsing

Before even touching the UI, ensure your deep link parsing logic is sound.

react-navigation Configuration Testing:

You can unit test the linking.config object directly.


// deepLinkConfig.js
export const linkingConfig = {
  screens: {
    Home: 'home',
    Profile: {
      path: 'profile/:userId',
      parse: {
        userId: (userId) => parseInt(userId),
      },
    },
    ProductDetail: 'product/:productId',
    NotFound: '*', // Fallback for unmatched routes
  },
};

// deepLinkConfig.test.js
import { linkingConfig } from './deepLinkConfig';
import { getPathFromState, getStateFromPath } from '@react-navigation/native';

describe('Deep Link Configuration Parsing', () => {
  it('should parse product detail link correctly', () => {
    const path = 'product/123';
    const state = getStateFromPath(path, linkingConfig);
    expect(state.routes[0].name).toBe('ProductDetail');
    expect(state.routes[0].params.productId).toBe('123');
  });

  it('should parse profile link with integer userId', () => {
    const path = 'profile/456';
    const state = getStateFromPath(path, linkingConfig);
    expect(state.routes[0].name).toBe('Profile');
    expect(state.routes[0].params.userId).toBe(456); // parsed as int
  });

  it('should handle invalid profile userId', () => {
    const path = 'profile/abc';
    const state = getStateFromPath(path, linkingConfig);
    // Depending on react-navigation version and specific config,
    // this might fallback to a default or return an unmatched state.
    // Ensure your NotFound screen is hit or parsing fails gracefully.
    expect(state.routes[0].name).toBe('Profile'); // Still navigates to Profile
    expect(isNaN(state.routes[0].params.userId)).toBe(true); // userId is NaN
  });

  it('should fallback for unknown paths', () => {
    const path = 'nonexistent/route';
    const state = getStateFromPath(path, linkingConfig);
    expect(state.routes[0].name).toBe('NotFound'); // Expecting the fallback route
  });

  it('should generate paths correctly from state', () => {
    const state = {
      routes: [{
        name: 'ProductDetail',
        params: { productId: '789' },
      }],
    };
    const path = getPathFromState(state, linkingConfig);
    expect(path).toBe('product/789');
  });
});

This approach isolates the pure parsing logic, making it fast and reliable.

End-to-End (E2E) UI Automation with Detox or Appium

For comprehensive E2E testing, tools like Detox (for React Native specifically) or Appium (cross-platform mobile automation) are essential. They allow simulating deep link invocation and verifying UI state.

#### Using Detox (Recommended for React Native)

Detox directly interacts with the native app code and provides APIs to simulate deep link events.

  1. Setup Detox: Follow the official Detox setup guide.
  2. Write Test:

    // e2e/deepLinks.e2e.js
    import { device, element, by, waitFor } from 'detox';

    describe('Deep Linking', () => {
      beforeAll(async () => {
        await device.launchApp({ newInstance: true }); // Ensure clean state for each test
      });

      beforeEach(async () => {
        await device.reloadReactNative(); // Reload JS bundle for fresh state
      });

      it('should open product detail screen from deep link when app is closed', async () => {
        await device.terminateApp(); // Ensure app is closed
        await device.launchApp({
          newInstance: true,
          url: 'yourapp://product/456', // Simulate custom scheme deep link
          // For Universal Links/App Links: url: 'https://yourapp.com/product/456'
        });

        await waitFor(element(by.text('Product ID: 456')))
          .toBeVisible()
          .withTimeout(5000);
        await expect(element(by.text('Product Detail Screen'))).toBeVisible();
      });

      it('should navigate to profile screen from deep link when app is in background', async () => {
        await device.sendTo and From Background(); // Send app to background
        await device.openURL({ url: 'yourapp://profile/789' }); // Simulate deep link while in background

        await waitFor(element(by.text('User ID: 789')))
          .toBeVisible()
          .withTimeout(5000);
        await expect(element(by.text('Profile Screen'))).toBeVisible();
      });

      it('should handle deep link to non-existent route gracefully', async () => {
        await device.openURL({ url: 'yourapp://nonexistent/route' });

        await waitFor(element(by.text('Page Not Found')))
          .toBeVisible()
          .withTimeout(5000);
        await expect(element(by.text('Page Not Found'))).toBeVisible();
      });

      // Add tests for other scenarios: app in foreground, invalid params, etc.
    });

#### Using Appium

Appium provides a language-agnostic way to automate mobile apps. It's more complex to set up but offers broader platform support.

  1. Setup Appium: Install Appium server, client libraries (e.g., webdriverio or appium-webdriverio), and platform-specific drivers.
  2. Write Test (JavaScript with WebdriverIO):

    // appium/deepLinks.test.js
    import { remote } from 'webdriverio';

    const capabilities = {
      platformName: 'Android', // or 'iOS'
      'appium:deviceName': 'Pixel 3a XL API 30',
      'appium:app': '/path/to/your/app.apk', // or .ipa
      'appium:automationName': 'UiAutomator2', // or 'XCUITest'
      'appium:appPackage': 'com.yourpackage.name', // Android only
      'appium:bundleId': 'com.yourbundle.id', // iOS only
      'appium:noReset': true, // Keep app data between sessions
      'appium:fullReset': false,
    };

    async function runTest() {
      const driver = await remote({
        hostname: 'localhost',
        port: 4723,
        capabilities: capabilities,
      });

      try {
        // Test 1: App closed, open deep link
        await driver.terminateApp(capabilities['appium:appPackage'] || capabilities['appium:bundleId']);
        await driver.activateApp(capabilities['appium:appPackage'] || capabilities['appium:bundleId']); // Activate to ensure it's in foreground
        await driver.execute('mobile: deepLink', {
          url: 'yourapp://product/123',
          bundleId: capabilities['appium:bundleId'], // iOS
          package: capabilities['appium:appPackage'], // Android
        });

        await driver.pause(5000); // Give app time to navigate
        const productText = await driver.$('~Product ID: 123'); // Accessibility ID
        await productText.waitForDisplayed({ timeout: 10000 });
        await productText.isDisplayed();

        // Test 2: App in background, open deep link
        await driver.background(-1); // Send to background (iOS)
        // For Android: await driver.pressKeyCode(3); // HOME key
        await driver.execute('mobile: deepLink', {
          url: 'yourapp://profile/456',
          bundleId: capabilities['appium:bundleId'],
          package: capabilities['appium:appPackage'],
        });
        await driver.pause(5000);
        const profileText = await driver.$('~User ID: 456');
        await profileText.waitForDisplayed({ timeout: 10000 });
        await profileText.isDisplayed();

      } finally {
        await driver.deleteSession();
      }
    }

    runTest().catch(console.error);

Leveraging Autonomous QA Platforms (e.g., SUSATest)

Traditional scripted E2E tests, while valuable, are limited to the scenarios explicitly coded by engineers. They often miss subtle bugs that arise from unexpected user flows or interactions. This is where autonomous QA platforms like SUSATest excel.

SUSATest operates by intelligently exploring your application, much like a real user, but with far greater speed and consistency. When it comes to deep links, an autonomous platform can uncover issues that scripted tests might overlook:

  1. Persona-Driven Exploration: SUSATest can simulate various user personas (e.g., "curious," "impatient," "adversarial"). An "impatient" user might tap a deep link repeatedly, revealing race conditions. An "adversarial" user might try malformed deep links or links that lead to unauthorized content, exposing security vulnerabilities or poor error handling.
  2. **Unscripted

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free