How to Test OTP Verification on React Native (Complete Guide)
How to Test Otp Verification on React Native (Complete Guide)
How to Test Otp Verification on React Native (Complete Guide)
Testing one‑time passcode (OTP) flows in React Native applications is a critical quality gate because a broken verification step blocks onboarding, payments, account recovery, and any feature that relies on phone‑number identity. When OTP verification fails in production you see abandoned sign‑ups, frustrated users, and increased support load. This guide walks you through why OTP matters, what typically breaks, a comprehensive test matrix, manual and automated techniques, accessibility and security checks, and how autonomous persona‑driven exploration surfaces bugs that scripted tests miss. Every section contains concrete steps, code samples, or command‑line snippets you can copy into a React Native project today.
1. Why OTP Verification Deserves Dedicated Testing
OTP verification is often the first real interaction a user has with your backend after they supply a phone number. The flow typically involves:
- User enters a phone number and taps Send Code.
- Backend generates a cryptographically random 6‑digit code, stores it (often with TTL), and sends it via SMS or a push‑based provider.
- User inputs the code into an OTP field.
- Client validates the code against the backend, receives a token, and proceeds.
If any of those steps misbehave—network timeout, code expiration, incorrect UI handling, or accessibility barrier—the user cannot continue. Unlike static UI components, OTP flows are stateful, time‑sensitive, and depend on external services, making them prone to race conditions, flaky tests, and edge‑case bugs that only appear under load or with specific carrier delays.
2. Common Failure Modes in Production
| Failure Category | Typical Symptom | Root Cause |
|---|---|---|
| Network latency | “Sending…” spinner never resolves, user taps back | API timeout not handled, no retry UI |
| Code expiry | Valid code rejected after 30 s | Client uses stale TTL, server enforces shorter window |
| Incorrect masking | OTP shows plain numbers in screenshots/logs | TextInput secureTextEntry missing or overridden |
| Focus loss | Keyboard dismisses after each digit, forcing re‑tap | Auto‑focus logic broken when using react-native-otp-input |
| International formatting | +1 (555) 123‑4567 rejected as invalid | Phone‑number parser not E.164‑aware |
| Accessibility | TalkBack reads “edit text” instead of “OTP field 1 of 6” | Missing accessibilityLabel or accessibilityValue |
| Security leakage | Code appears in React Native debugger logs | console.log of OTP left in dev build |
| Duplicate submission | User resends code, receives two different codes, first expires | No debounce on resend button, server allows multiple active codes |
| Error‑state UI | Error toast disappears before user reads it | Toast duration too short, not tied to user action |
Understanding these patterns helps you build a test matrix that covers both happy paths and the ways the flow can break.
3. Complete Test Matrix for OTP Verification
The following sections.
| Test ID | Description | Preconditions | Steps | Expected Result | Pass/Fail Criteria |
|---|---|---|---|---|---|
| OTP‑01 | Happy path: send, receive, verify | User has valid phone number, network ok | 1. Enter number → Send 2. Wait for SMS (simulate) 3. Input 6‑digit code → Verify | Backend returns auth token, navigation proceeds to home screen | Token present, screen changes |
| OTP‑02 | Invalid code rejection | Same as OTP‑01 | Same steps, but input wrong code (e.g., 000000) | Backend returns 400 invalid‑otp, UI shows error toast | Error message displayed, token absent |
| OTP‑03 | Code expiry | Same as OTP‑01, but wait > TTL (Simulate 90‑second delay) then input code | Backend returns 410 expired, UI shows “code expired” | Proper expiry handling | |
| OTP‑04 | Resend cooldown | Same as OTP‑01 | 1. Send code 2. Immediately tap Resend 3. Wait cooldown period | Resend button disabled for 30 s, then enabled | Button state reflects cooldown |
| OTP‑05 | Network failure on send | Disable Wi‑Fi/cellular | Enter number → Send | App shows “Unable to send code, try again” and does not crash | Graceful error UI, no exception |
| OTP‑06 | Network failure on verify | Disable after code received | Input correct code → Verify | App shows verification failed, offers retry | No crash, retry possible |
| OTP‑07 | International number formatting | Input number with spaces, dashes, parentheses | Enter “(555) 123‑4567” → Send | Backend receives E.164 “+15551234567”, SMS sent | Parser normalizes correctly |
| OTP‑08 | OTP field secure entry | Screen with OTPInput component | Enable “Show passwords” in developer options | Digits remain obscured (dots) | secureTextEntry works |
| OTP‑09 | Focus traversal | Six‑box OTP input | Fill first five boxes, then tap sixth | Cursor moves to next box automatically, keyboard stays open | Focus management correct |
| OTP‑10 | Accessibility label | TalkBack enabled | Navigate to OTP field | TalkBack reads “OTP field 1 of 6, edit text” | Proper accessibilityLabel/accessibilityValue |
| OTP‑11 | Error toast duration | Trigger invalid code | Submit wrong code | Toast remains visible ≥ 4 s or until dismiss | User can read message |
| OTP‑12 | Duplicate send prevention | Rapid double tap on Send | Tap Send twice within 500 ms | Only one request sent to backend | Debounce/throttle in place |
| OTP‑13 | Leak in logs | Enable remote JS debugging | Perform OTP flow | No OTP appears in console or React Native DevTools | No console.log of OTP |
| OTP‑14 | Backend rate limiting | Simulate many rapid attempts | Send code 5 times in 10 s | Backend returns 429 too‑many‑requests, UI shows rate‑limit message | Proper handling of 429 |
| OTP‑15 | Autofill from SMS (Android) | Android ≥ 18 with SMS Retriever API | Receive SMS with app‑specific hash | OTP auto‑fills into field without manual entry | Autofill works, security hash verified |
You can copy this table into a test‑management tool (e.g., TestRail, Zephyr) and map each ID to automated test cases.
4. Manual Testing Step‑by‑Step
Even with automation, a manual sanity check catches UI quirks and device‑specific behavior. Follow this checklist on a physical Android/iOS device (or emulator/simulator) before each release.
- Setup
- Install the debug build via
adb install app-debug.apkor Xcode. - Ensure you have a test phone number capable of receiving SMS (use a service like Twilio test credentials or a dedicated SIM).
- Clear app data (
adb shell pm clear com.yourapp) to start from a clean state.
- Send Code
- Enter the test number in the international format (e.g.,
+1 555‑123‑4567). - Tap Send Code. Observe the loading indicator; it should disappear within 5 s on a good connection.
- Verify that the Resend button becomes disabled for the configured cooldown.
- Capture OTP
- If using a test SMS provider, retrieve the code from the dashboard.
- On Android, you can also enable the SMS Retriever API to see if autofill triggers.
- Input and Verify
- Paste or type the six digits into the OTP boxes.
- Confirm that focus jumps automatically after each digit (if your UI implements it).
- Tap Verify.
- On success, you should be redirected to the next screen (e.g., profile setup) and receive an auth token stored in SecureStore or AsyncStorage.
- Error Paths
- Repeat steps 2‑4 but input an incorrect code. Verify that an error toast appears and remains visible long enough to read.
- Disable network after sending the code, then attempt verification. Confirm the app shows a network‑error message and does not crash.
- Wait beyond the TTL (e.g., 2 minutes if TTL is 60 s) then try verification. Expect an expiry message.
- Accessibility
- Turn on TalkBack (Android) or VoiceOver (iOS).
- Navigate to the OTP field; listen for a descriptive label that indicates position and purpose.
- Ensure that activating the field opens the keyboard and that digits are spoken as they are entered.
- Security Check
- Enable remote JS debugging (
adb reverse tcp:8081 tcp:8081) and open Chrome DevTools. - Perform the OTP flow; inspect the Console tab for any
console.logthat prints the OTP. - Confirm that the OTP never appears in network payloads (it should be sent over HTTPS only).
- Cleanup
- Log out or clear session data to verify that a fresh OTP flow works again without stale state.
Document any deviations from the expected results in a bug ticket, referencing the test‑matrix ID (e.g., OTP‑07). Manual testing is especially valuable for checking device‑specific SMS delivery delays and for validating that custom OTP UI components render correctly on different screen densities.
5. Automated Unit Testing with Jest and React Native Testing Library
Unit tests validate the logic that prepares the request, handles responses, and updates UI state without relying on a real SMS gateway. The goal is to test the *client‑side* state machine.
5.1. Setup
# Add dependencies
yarn add -D jest @testing-library/react-native @testing-library/jest-native
# If using Expo
expo install @testing-library/react-native
Add a jest.setup.js file:
import '@testing-library/jest-native/extend-expect';
5.2. Mocking the API Layer
Create a mock for your OTP service (api/otpService.js):
// __mocks__/api/otpService.js
export const sendOtp = jest.fn();
export const verifyOtp = jest.fn();
In your component test, jest will automatically use the mock if the file resides under __mocks__.
5.3. Component Example
Assume a simple OTP screen:
// src/screens/OtpScreen.js
import React, { useState } from 'react';
import { View, TextInput, Button, Text, ActivityIndicator } from 'react-native';
import { sendOtp, verifyOtp } from '../api/otpService';
export default function OtpScreen({ phoneNumber }) {
const [otp, setOtp] = useState('');
const [status, setStatus] = useState('idle'); // idle, sending, verifying, success, error
const [error, setError] = setError('');
const handleSend = async () => {
setStatus('sending');
try {
await sendOtp({ phoneNumber });
setStatus('idle');
} catch (e) {
setStatus('error');
setError('Failed to send code');
}
};
const handleVerify = async () => {
setStatus('verifying');
try {
await verifyOtp({ phoneNumber, code: otp });
setStatus('success');
} catch (e) {
setStatus('error');
setError('Invalid or expired code');
}
};
return (
<View style={{ padding: 20 }}>
<Text>Enter the 6‑digit code sent to {phoneNumber}</Text>
{status === 'sending' && <ActivityIndicator />}
{status === 'error' && <Text style={{ color: 'red' }}>{error}</Text>}
<TextInput
placeholder="______"
value={otp}
onChangeText={setOtp}
maxLength={6}
keyboardType="number-pad"
secureTextEntry
autoFocus
editable={status !== 'success'}
/>
{status === 'idle' && (
<Button title={otp.length === 6 ? 'Verify' : 'Send Code'} onPress={otp.length === 6 ? handleVerify : handleSend} disabled={status !== 'idle'} />
)}
</View>
);
}
5.4. Test Suite
// __tests__/OtpScreen.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import OtpScreen from '../src/screens/OtpScreen';
import { sendOtp, verifyOtp } from '../src/api/otpService';
describe('OtpScreen', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('sends OTP when button pressed with empty field', async () => {
const { getByLabelText, getByText } = render(<OtpScreen phoneNumber="+15551234567" />);
const sendBtn = getByText(/send code/i);
fireEvent.press(sendBtn);
expect(sendOtp).toHaveBeenCalledWith({ phoneNumber: '+15551234567' });
});
test('shows sending indicator while waiting', async () => {
sendOtp.mockReturnValue(new Promise(() => {})); // pending promise
const { getByTestId } = render(<OtpScreen phoneNumber="+15551234567" />);
fireEvent.press(getByText(/send code/i));
expect(getByTestId('activity-indicator')).toBeTruthy();
});
test('transitions to success on correct OTP', async () => {
verifyOtp.mockResolvedValueOnce();
const { getByLabelText, getByText } = render(<OtpScreen phoneNumber="+15551234567" />);
const codeInput = getByLabelText(/enter the 6‑digit code/i);
fireEvent.changeText(codeInput, '123456');
fireEvent.press(getByText(/verify/i));
await waitFor(() => {
expect(getByText(/success/i)).toBeTruthy();
});
expect(verifyOtp).toHaveBeenCalledWith({ phoneNumber: '+15551234567', code: '123456' });
});
test('shows error on invalid OTP', async () => {
verifyOtp.mockRejectedValueOnce(new Error('bad otp'));
const { getByLabelText, getByText } = render(<OtpScreen phoneNumber="+15551234567" />);
const codeInput = getByLabelText(/enter the 6‑digit code/i);
fireEvent.changeText(codeInput, '000000');
fireEvent.press(getByText(/verify/i));
await waitFor(() => {
expect(getByText(/invalid or expired/i)).toBeTruthy();
});
});
});
Key points
- Mock the network layer to isolate UI logic.
- Use
waitForfor asynchronous state changes (sending/verifying). - Test both success and error branches, ensuring the UI reflects the correct status.
- Verify that
secureTextEntryis present (you can add an expectation that the input’ssecureTextEntryprop is true).
Run the suite with yarn test. Aim for > 90 % line coverage on the OTP screen; uncovered lines often indicate missing error handling.
6. End‑to‑End Testing with Detox
Detox exercises the compiled native binary on a device or emulator, making it ideal for verifying that the OTP flow works with real navigation, animations, and native modules (e.g., SMS Retriever).
6.1. Install Detox
yarn add -D detox
detox init -r jest
# For Android
yarn add -D jest-circus
Update package.json:
"detox": {
"test-runner": "jest",
"configs": {
"android.emu.debug": {
"binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug",
"type": "android.emulator",
"device": {
"avdName": "pixel_5_api_33"
}
}
}
}
6.2. Writing the Test
Create e2e/otpFlow.test.js:
describe('OTP verification flow', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } });
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('sends code, receives via SMS Retriever, verifies and navigates home', async () => {
// 1. Enter phone number
await element(by.id('phone-input')).typeText('+15551234567');
await element(by.id('send-code-btn')).tap();
// 2. Wait for sending indicator to disappear
await waitFor(element(by.id('sending-spinner')))
.toNotExist()
.withTimeout(10000);
// 3. Simulate SMS arrival – Detox can push a test SMS via ADB
await device.sendSMS({ phoneNumber: '+15551234567', message: `Your code is 987654` });
// 4. Expect auto‑fill (if using SMS Retriever) or manual entry
await waitFor(element(by.id('otp-input-1')))
.toHaveValue('9')
.withTimeout(5000);
// If autofill not enabled, fill manually:
// await element(by.id('otp-input')).typeText('987654');
// 5. Press verify
await element(by.id('verify-btn')).tap();
// 6. Wait for home screen
await waitFor(element(by.id('home-screen')))
.toExist()
.withTimeout(10000);
});
it('shows error on wrong code', async () => {
await element(by.id('phone-input')).typeText('+15551234567');
await element(by.id('send-code-btn')).tap();
await waitFor(element(by.id('sending-spinner'))).toNotExist().withTimeout(10000);
await device.sendSMS({ phoneNumber: '+15551234567', message: `Your code is 000000` });
await element(by.id('otp-input')).typeText('111111'); // wrong
await element(by.id('verify-btn')).tap();
await waitFor(element(by.id('error-toast')))
.toHaveText(/invalid or expired/i)
.withTimeout(5000);
});
});
Explanation of key commands
device.sendSMSpushes a mock SMS to the emulator/device; on a real device you would rely on a test SIM or a service like Twilio that can forward messages to the test number via an API.waitForensures the test does not race with animations.- Using
idattributes (testIDin React Native) makes selectors stable.
Run the suite:
detox test -c android.emu.debug
Detox will spin up the emulator, install the app, and execute the scenarios. Adjust the timeout values based on your network simulation (you can also throttle with adb shell netcfg or Facebook’s network-throttling tool).
6.3. Handling Time‑Sensitive Code Expiry
To test expiry, advance the device clock or use a mock backend that returns a preset TTL. Example with a mock server (using msw):
// In detox test, before sending code:
await device.sendSMS({ phoneNumber: '+15551234567', message: `Your code is 123456` });
// Wait 70 seconds (if TTL=60s)
await new Promise(r => setTimeout(r, 70000));
await element(by.id('otp-input')).typeText('123456');
await element(by.id('verify-btn')).tap();
// Expect expiry error
await waitFor(element(by.id('error-toast')))
.toHaveText(/code expired/i)
.withTimeout(5000);
Detox’s ability to control the device clock (adb shell date) is limited on non‑rooted emulators, so a backend‑controlled TTL is often simpler.
7. Testing with Expo’s Development Tools
If your project uses Expo, you can leverage Expo’s built‑in testing utilities and the Expo Go client for rapid manual checks, plus Expo’s expo-dev-client for custom native modules.
7.1. Unit Tests with Jest (same as section 5)
Expo projects already include Jest configuration; just ensure jest.setup.js is imported.
7.2. Using expo-dev-client for Detox
Create a dev client:
expo install expo-dev-client
expo prebuild -p android
Then follow the Detox steps from section 6, pointing binaryPath to the dev client build (android/app/build/outputs/apk/dev/client-debug.apk). This lets you test OTP flows that rely on native modules not available in the Expo Go sandboxed in the standard Expo SDK (e.g., a custom SMS Retriever module).
7.3. Manual Checks with Expo Go
- Scan the QR code with Expo Go on a physical device.
- Enable “Disable JS bundler” in the dev menu to test the production bundle.
- Use the “Logs” panel to watch for any
console.warnabout missingsecureTextEntry.
Expo’s ease of iteration is valuable for early‑stage UI tweaks, but for reliable CI you should still run Detox against a dev client or a bare workflow build.
8. Accessibility Testing (aXe, TalkBack, VoiceOver)
Accessibility bugs in OTP flows often manifest as missing labels or confusing focus order. Automated tools can catch many of these issues early.
8.1. Setting Up @testing-library/react-native with axe-core
yarn add -D @testing-library/react-native @axe-core/react
Create a test file:
// __tests__/OtpScreen.accessibility.test.js
import React from 'react';
import { render } from '@testing-library/react-native';
import { axe, toHaveNoViolations } from '@axe-core/react';
import OtpScreen from '../src/screens/OtpScreen';
expect.extend(toHaveNoViolations);
test('OtpScreen has no accessibility violations', async () => {
const { container } = render(<OtpScreen phoneNumber="+15551234567" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Run with yarn test. The rule set includes checks for:
aria-label/accessibilityLabelon each OTP box.- Sufficient contrast between input background and text.
- Proper role (
textbox) and state (readonlywhen auto‑filled).
8.2. Manual TalkBack / VoiceOver Verification
- Enable TalkBack (Android Settings → Accessibility → TalkBack) or VoiceOver (iOS Settings → Accessibility → VoiceOver).
- Navigate to the OTP screen.
- Listen for each field: it should announce “OTP field 1 of 6, edit text, blank”.
- When a digit is entered, the announcement should update to include the entered value (e.g., “OTP field 1 of 6, edit text, 5”).
- Ensure that moving focus with swipe gestures does not skip any box and that the keyboard remains visible when expected.
If any field is silent or announces incorrectly, add appropriate props:
<TextInput
testID={`otp-input-${index}`}
value={otp[index] || ''}
onChangeText={val => handleChange(index, val)}
maxLength={1}
keyboardType="number-pad"
secureTextEntry
autoFocus={index === 0}
accessibilityLabel={`OTP field ${index + 1} of ${total}`}
accessibilityValue={otp[index] ?? ''}
/>
8.3. Color Contrast
Use the react-native-contrast-checker library or manually verify with the WCAG 2.1 AA requirement (≥ 4.5:1 for normal text). OTP boxes often have a thin border; ensure the border color contrasts sufficiently with the background.
9. Security and Privacy Considerations
OTP handling touches on several security domains: transmission secrecy, storage, replay resistance, and leakage via logs or screenshots.
9.1. Transport Security
- Verify that the API endpoints use HTTPS with a valid certificate.
- In development, disable clear‑text traffic (
android:usesCleartextTraffic="false"inAndroidManifest.xml) to catch accidental HTTP calls.
Add a test that asserts the request URL starts with https://:
// jest mock for fetch
global.fetch = jest.fn();
test('sendOtp uses HTTPS', async () => {
await sendOtp({ phoneNumber: '+15551234567' });
expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('https://'));
});
9.2. Code Storage
Never persist the raw OTP in AsyncStorage or SharedPreferences. Keep it only in memory (Redux state, React context, or a closure). Write a unit test that attempts to read the storage after verification and expects null.
import AsyncStorage from '@react-native-async-storage/async-storage';
jest.mock('@react-native-async-storage/async-storage');
test('OTP not stored after verification', async () => {
AsyncStorage.getItem.mockResolvedValue(null);
await verifyOtp({ phoneNumber: '+15551234567', code: '654321' });
expect(AsyncStorage.getItem).not.toHaveBeenCalledWith('otp');
});
9.3. Replay Attack Mitigation
The backend should mark a code as used after a successful verification. To test the client’s handling of a reused code:
- Mock the verify endpoint to return success on the first call, then a 400
code already usedon subsequent calls. - In the test, call verify twice with the same OTP and assert that the second attempt shows an error toast.
verifyOtp
.mockResolvedValueOnce() // first success
.mockRejectedValueOnce(new Error('code already used'));
test('prevents OTP reuse', async () => {
const { getByLabelText, getByText } = render(<OtpScreen phoneNumber="+15551234567" />);
const input = getByLabelText(/enter the 6‑digit code/i);
fireEvent.changeText(input, '123456');
fireEvent.press(getByText(/verify/i));
await waitFor(() => expect(getByText(/success/i)).toBeTruthy());
// second attempt
fireEvent.changeText(input, '123456');
fireEvent.press(getByText(/verify/i));
await waitFor(() => expect(getByText(/already used/i)).toBeTruthy());
});
9.4. Screenshot Protection (Android)
Enable the FLAG_SECURE window flag to prevent OTP from appearing in screenshots or recent‑apps thumbnails.
import { PermissionsAndroid } from 'react-native';
import { NativeModules } from 'react-native';
const { RNWindowManager } = NativeModules;
useEffect(() => {
(async () => {
try {
await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.SYSTEM_ALERT_WINDOW);
RNWindowManager.setSecureFlag(true);
} catch (e) {
console.warn('Unable to set secure flag', e);
}
})();
}, []);
Write a test that attempts to capture a screenshot via adb shell screencap and asserts that the OTP region is blank (this is more of a manual check, but you can automate with device farm tools that compare pixel values).
9.5. Logging Sanitization
Add a ESLint rule to forbid console.log of variables named otp, code, or passcode. Example .eslintrc.js:
rules: {
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-restricted-syntax': [
'error',
{
selector: 'CallExpression[callee.object.name="console"][callee.property.name="log"]',
message: 'Do not log OTP or sensitive data',
},
],
};
Run yarn lint as part of CI to catch accidental leaks.
10. Autonomous, Persona‑Driven Exploration with SUSA
Scripted tests excel at verifying known paths, but they cannot anticipate every way a real user might interact with an OTP screen—especially when users are distracted, impatient, or use assistive technology in unconventional ways. SUSA (the autonomous QA platform) explores the app by simulating a variety of user personas, each with distinct behavior profiles, and automatically discovers issues that static test suites miss.
10.1. How SUSA Works with React Native
- Upload – Provide the APK (or a testflight build) and the base URL of your backend.
- Persona Engine – SUSA spins up virtual users:
- *Curious*: taps every visible element, tries long‑press, swipes.
- *Impatient*: rapidly taps send/resend, enters incomplete OTP, hits back.
- *Novice*: reads hints slowly, often mis‑types, uses the paste button.
- *Accessibility*: enables TalkBack/VoiceOver, navigates via swipe gestures, changes font size.
- *Adversarial*: attempts SQL‑like strings, extremely long inputs, Unicode emojis.
- Exploration – The agent drives the UI, captures network traffic, monitors logs, and records UI state transitions.
- Assertion‑Free Oracles – It flags crashes, ANRs, unhandled promise rejections, accessibility violations (via automated axe scans), and security red flags (e.g., OTP appearing in logs).
- Regression Script Generation – After a run, SUSA outputs ready‑to‑run Appium (Android) and Playwright (Web) scripts that reproduce the discovered flows, letting you add them to your CI pipeline.
10.2. Configuring a SUSA Run for OTP Verification
Create a susa-config.json in your repo:
{
"app": "./android/app/build/outputs/apk/debug/app-debug.apk",
"backend": "https://api.example.com",
"personas": ["curious", "impatient", "novice", "accessibility", "adversarial"],
"maxDepth": 8,
"timePerSessionMs": 30000,
"outputDir": "./susa-reports",
"generateScripts": true,
"scriptFormats": ["appium", "playwright"]
}
Run the agent:
npx susatest-agent run --config susa-config.json
During the exploration, SUSA might notice:
- The impatient persona tapping Send three times within 200 ms, causing the backend to receive three distinct OTPs and the UI to show a confusing “code sent” toast each time.
- The accessibility persona using TalkBack discovering that the OTP field’s
accessibilityLabeldoes not update after each digit, making it hard to know which box is active. - The **advers
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