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
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:
- Associated Domains Entitlement: Adding
applinks:yourdomain.comto your Xcode project's capabilities. - Apple App Site Association (AASA) File: A JSON file hosted at
https://yourdomain.com/.well-known/apple-app-site-associationthat lists the paths your app can handle. - React Native App Configuration: Using
Linking.getInitialURL()for app launch andLinking.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:
- Xcode Info.plist: Adding URL types under
CFBundleURLTypes. - React Native App Configuration: Similar to Universal Links,
Linking.getInitialURL()andLinking.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:
- Intent Filters in AndroidManifest.xml: Declaring
withandroid.intent.action.VIEW,android.intent.category.DEFAULT,android.intent.category.BROWSABLE, andandroid.dataschemes (http/https) for your domain and paths. - Digital Asset Links JSON File: A JSON file hosted at
https://yourdomain.com/.well-known/assetlinks.jsonto verify ownership of your domain. - React Native App Configuration:
Linking.getInitialURL()andLinking.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 ID | Test Case Description | Expected Result | React Native Specifics |
|---|---|---|---|
| DL-001 | App closed, open deep link to Home screen. | App opens to Home screen. | Linking.getInitialURL() handles launch. |
| DL-002 | App closed, open deep link to specific product (e.g., /product/123). | App opens to ProductDetail screen for product 123. | react-navigation parses productId. |
| DL-003 | App closed, open deep link to user profile (e.g., /profile/user456). | App opens to Profile screen for user 456. | react-navigation parses userId. |
| DL-004 | App in background, open deep link to Home screen. | App foregrounds, navigates to Home. | Linking.addEventListener handles foreground. |
| DL-005 | App in background, open deep link to specific product. | App foregrounds, navigates to ProductDetail. | Linking.addEventListener handles foreground. |
| DL-006 | App in foreground, open deep link to new screen. | Navigates to new screen, pushing onto stack. | Linking.addEventListener handles foreground. |
| DL-007 | App 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-008 | Deep link contains query parameters (e.g., /search?query=reactnative). | App navigates to Search screen, search input pre-filled. | react-navigation handles query params. |
| DL-009 | Deep 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 ID | Test Case Description | Expected Result | React Native Specifics |
|---|---|---|---|
| DL-E01 | Deep 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-E02 | Deep 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-E03 | Deep 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-E04 | Deep 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-E05 | Deep link with an unknown custom scheme (e.g., unknownapp://). | App does not open. | OS handles scheme registration. |
| DL-E06 | Extremely 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-E07 | Deep 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-E08 | Deep 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-E09 | Multiple deep links opened in rapid succession. | App handles sequentially, or navigates to last valid link. | Debouncing Linking.addEventListener or specific navigation logic. |
| DL-E10 | Deep link with special characters in parameters (e.g., /?q=test&!@#$). | Parameters decoded correctly. | URL encoding/decoding. |
| DL-E11 | Deep 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 ID | Test Case Description | Expected Result | React Native Specifics |
|---|---|---|---|
| DL-S01 | iOS: 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-S02 | iOS: Universal Link when Associated Domains entitlement is missing. | App opens in Safari. | Xcode project settings. |
| DL-S03 | Android: App Link when Digital Asset Links file is misconfigured. | App opens in browser or prompts disambiguation dialog. | Assetlinks file validation. |
| DL-S04 | Android: App Link when intent filters are incorrect. | App opens in browser or prompts disambiguation dialog. | AndroidManifest.xml correctness. |
| DL-S05 | Deep link with no internet connection. | App opens to offline state or error screen, possibly queues navigation. | Network detection and offline handling. |
| DL-S06 | Deep link received during app update/reinstall. | App handles navigation after update/first launch. | Post-install/update logic. |
| DL-S07 | Deep 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-S08 | Deep link via QR code scanner. | App opens to correct screen. | QR scanner app's forwarding mechanism. |
| DL-S09 | Deep 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 ID | Test Case Description | Expected Result | React Native Specifics |
|---|---|---|---|
| DL-A01 | Screen opened via deep link is fully accessible (VoiceOver/TalkBack). | All elements are reachable and announced correctly. | Standard accessibility testing on target screen. |
| DL-A02 | Deep 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-A03 | Deep link with potential XSS payload in parameters (e.g., /?q=). | Payload is sanitized and not executed. | Input sanitization in React Native components. |
| DL-A04 | Deep 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
- Development Build: Ensure you have a debug build of your React Native app installed on a physical device or emulator/simulator.
- Deep Link Configuration: Verify your
app.json,Info.plist, andAndroidManifest.xmlare correctly configured for deep links as per yourreact-navigationsetup. - 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://)
- From Terminal (iOS Simulator/Android Emulator):
- iOS Simulator:
xcrun simctl openurl booted "yourapp://product/123"
adb shell am start -W -a android.intent.action.VIEW -d "yourapp://product/123" com.yourpackage.name
ProductDetail screen for ID 123.yourapp://product/123) and press Enter.#### 2. Testing Universal Links / App Links (e.g., https://yourapp.com)
- From Safari/Chrome Address Bar: Type the full HTTPS URL (e.g.,
https://yourapp.com/product/123). - *Verification:*
- iOS: If Universal Links are configured correctly, the app should open directly. If not, it will open in Safari. If the app is installed, a banner "Open in App" might appear at the top.
- Android: If App Links are configured, the app should open directly. If not, it will open in Chrome or show a disambiguation dialog.
- From Notes/Email/Messaging Apps: Embed the HTTPS URL. Tap the link.
- *Verification:* This is the most realistic scenario. The app should open directly without browser intervention.
- Verifying AASA/Assetlinks Files:
- iOS: Use Apple's App Search Validation Tool (
https://search.developer.apple.com/appsearch-validation-tool/). Enter your domain (e.g.,yourapp.com) and verify the output. - Android: Manually check
https://yourdomain.com/.well-known/assetlinks.jsonin a browser. Ensure it's valid JSON and contains the correct package name and SHA256 fingerprints.
#### 3. Testing App States
- App Closed: Force quit the app before tapping the deep link.
- App in Background: Open the app, press home button to send to background, then tap deep link.
- App in Foreground: Keep the app open on a different screen, then tap deep link.
- App in Foreground (Same Screen): Keep the app open on the target screen (e.g., ProductDetail for product 123), then tap a deep link to the *same* screen but with *different* parameters (e.g., ProductDetail for product 456). Verify if it updates or pushes a new instance.
- App Offline: Turn off Wi-Fi/data, then test deep links.
#### 4. Parameter Validation
- Use URLs with invalid parameters (e.g.,
product/abcinstead ofproduct/123). - Use URLs with missing parameters (e.g.,
product/instead ofproduct/123). - Use URLs with special characters, encoded characters, and excessively long query strings.
#### 5. User Experience
- Observe navigation animations. Are they smooth?
- Does the back button work as expected after deep linking?
- Are loading states handled gracefully if the deep-linked content takes time to load?
- Is sensitive data protected by authentication?
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.
- Setup Detox: Follow the official Detox setup guide.
- 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.
});
- iOS Specifics: For Universal Links, ensure your AASA file is correctly served on a test domain, and the device/simulator has network access. Detox's
device.openURLworks for both custom schemes and Universal Links. - Android Specifics: For App Links, ensure your
assetlinks.jsonis correctly served and verified. Detox'sdevice.openURLhandles both. You might need to adjustAndroidManifest.xmlfor test builds to allow specific deep link testing.
#### Using Appium
Appium provides a language-agnostic way to automate mobile apps. It's more complex to set up but offers broader platform support.
- Setup Appium: Install Appium server, client libraries (e.g.,
webdriverioorappium-webdriverio), and platform-specific drivers. - 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);
-
mobile: deepLink: Appium provides platform-specific extensions. For iOS,mobile: deepLinkcan be used. For Android, you might need to useadb shell am startcommands via Appium'sexecutemethod or simulate actions from external apps. - Verification: Appium relies on element locators (accessibility IDs, text, class names) to verify the UI state after deep link navigation.
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:
- 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.
- **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