How to Test Forgot Password on React Native (Complete Guide)
How to Test Forgot Password on React Native (Complete Guide)
How to Test Forgot Password on React Native (Complete Guide)
How to Test Forgot Password on React Native (Complete Guide): Why It Matters
Testing the forgot password flow is not a nicety; it is a gatekeeper for user trust and account security. When a user cannot recover access, they abandon the app, generate support tickets, and may resort to insecure workarounds like writing passwords down or reusing credentials across services. In production, the most common failures stem from misconfigured API endpoints, mishandled navigation states, or missing error handling when the backend returns unexpected payloads. A broken reset link can expose token leakage, while a poorly throttled endpoint invites credential stuffing attacks. Because the flow touches networking, state management, UI rendering, and often third‑party email or SMS providers, a defect in any of those layers can surface as a crash, an ANR, or a silent dead end that users never report. Consequently, a comprehensive test strategy must cover functional correctness, edge‑case resilience, accessibility compliance, and security hardening before the code reaches a release channel.
How to Test Forgot Password on React Native (Complete Guide): Building a Test Matrix
A test matrix provides a shared language for developers, QA, and product owners. It enumerates scenarios, assigns priority, and drives both manual and automated effort. Below is a matrix that balances breadth with depth, focusing on the React Native specifics that often cause regressions.
| Test ID | Category | Description | Expected Result | Priority |
|---|---|---|---|---|
| FP‑01 | Happy Path | User enters valid email, receives reset link, clicks link, sets new password, logs in with new credentials. | Success screen shown, new password works, old password invalidated. | P0 |
| FP‑02 | Error Path – Invalid Email | User submits malformed email (missing @). | Inline validation error appears, no network request sent. | P0 |
| FP‑03 | Error Path – Non‑Existent Email | User submits email not registered. | Generic message “If the address exists, you will receive an email” to avoid enumeration. | P0 |
| FP‑04 | Error Path – Backend 500 | Simulate server error on POST /forgot. | Error toast displayed, UI retains input, retry button enabled. | P1 |
| FP‑05 | Error Path – Backend 429 (Rate Limit) | Simulate too many requests. | Warning about too many attempts, lockout timer shown, further submissions blocked. | P1 |
| FP‑06 | Edge Case – Empty Input | User taps submit with empty field. | Same as FP‑02 (validation error). | P0 |
| FP‑07 | Edge Case – Whitespace Only | User submits spaces or tabs. | Trimmed value treated as empty → validation error. | P0 |
| FP‑08 | Edge Case – Very Long Email | User pastes 300‑character string. | Input rejects or truncates per UI limit, no crash. | P1 |
| FP‑09 | Edge Case – Special Characters | Email contains +, -, _, . (allowed) and Unicode. | Accepted if conforms to RFC 5322 subset used by backend. | P1 |
| FP‑10 | Edge Case – Network Loss Mid‑Request | Disable Wi‑Fi/cellular after submit. | App shows offline toast, retains state, retries on reconnect. | P1 |
| FP‑11 | Edge Case – Intermittent Slow Response | Throttle network to 2 seconds delay. | UI shows spinner, does not timeout prematurely. | P2 |
| FP‑12 | Accessibility – Screen Reader | TalkBack/VoiceOver reads each field, error messages, and button states. | All announcements are clear, live regions update errors. | P1 |
| FP‑13 | Accessibility – Color Contrast | Verify contrast ratio ≥ 4.5:1 for text and ≥ 3:1 for icons. | Passes automated contrast check. | P2 |
| FP‑14 | Accessibility – Touch Target | Buttons and input fields ≥ 48 dp. | Meets guideline. | P2 |
| FP‑15 | Security – Token in URL | Reset link contains one‑time token; ensure token not logged. | No token appears in console, network logs, or crash reports. | P0 |
| FP‑16 | Security – Rate Limiting on Reset Endpoint | Attempt to brute‑force token guess. | After N failures, endpoint returns 429 or locks account for duration. | P0 |
| FP‑17 | Security – Email Enumeration Protection | Compare responses for existing vs. non‑existing email. | Identical timing and message content. | P0 |
| FP‑18 | Localization – RTL Layout | Run with Arabic/Hebrew locale. | Layout mirrors correctly, no clipped text. | P2 |
| FP‑19 | Device Fragmentation – Low‑End Android | Test on Android Go device (≤ 1 GB RAM). | UI responsive, no frame drops > 16 ms. | P2 |
| FP‑20 | Deep Link Handling – Reset Link Click | Click universal link from email app; app opens directly to reset screen. | Reset screen pre‑filled with token, ready for password entry. | P1 |
The matrix can be copied into a test‑management tool (e.g., TestRail, Zephyr) and used to generate both manual test scripts and automated test cases. Priorities help triage when time is limited: P0 blocks release, P1 should be fixed before the next sprint, P2 is desirable but not blocking.
How to Test Forgot Password on React Native (Complete Guide): Manual Testing Approach
Manual testing remains valuable for exploratory checks, usability assessment, and catching issues that automated scripts ignore because they follow a rigid script. The following step‑by‑step procedure works on both iOS simulators and Android emulators, as well as on physical devices.
- Environment preparation
- Install the latest debug build via
npx react-native run-androidorrun-ios. - Ensure the bundler is running (
npx metro start). - Clear async storage (
adb shell pm clear com.yourapporxcrun simctl uninstall bootedthen reinstall) to start from a clean state. - Enable network throttling tools (e.g.,
adb shell netcfgor Network Link Conditioner on macOS) to simulate 3G or offline conditions.
- Baseline happy‑path validation
- Navigate to the login screen, tap “Forgot password?”.
- Verify that the forgot‑password screen loads within 1 second and that the email field is focused.
- Enter a known test email (e.g.,
test+reset@example.com). - Tap “Send reset link”. Observe a spinner; after the mocked API returns 200, a success toast appears.
- Check the email inbox (use a service like Mailinator or a real test mailbox) for the reset link. Click it; the app should deep‑link to the reset screen with the token parsed from the URL query string.
- Enter a new password that satisfies the policy (minimum length, required character types).
- Tap “Reset password”. Confirmation screen appears; proceed to login with the new credentials.
- Error‑path execution
- Repeat steps above but substitute invalid email formats, blank fields, and whitespace‑only strings. Confirm inline validation appears immediately without network calls.
- Use a tool like
mitmproxyto intercept and modify responses: return 500, 429, or malformed JSON. Observe that the UI shows an appropriate error message, retains user input, and offers a retry action.
- Edge‑case exploration
- Paste a 300‑character string into the email field; ensure the UI either rejects it or truncates gracefully.
- Rotate the device while a spinner is showing; verify that the spinner remains centered and no state loss occurs.
- Simulate an incoming call or switch to another app during the network request; after returning, the spinner should still be visible or the error handled.
- Accessibility spot‑check
- Turn on TalkBack (Android) or VoiceOver (iOS). Swipe through each element; listen for labels like “Email address, text field, required” and “Send reset link, button”.
- Trigger an error and verify that the error message is announced via live region.
- Use the Accessibility Scanner (Android) or Xcode Accessibility Inspector to check contrast and touch target sizes.
- Security sniffing
- Enable device logging (
adb logcatorConsole.app). Submit a reset request and verify that the reset token never appears in the logs. - Attempt to brute‑force the token endpoint by sending rapid requests with random tokens; ensure the server responds with 429 after a configurable threshold.
- Documentation
- For each test case, record: device model, OS version, build number, network condition, steps taken, observed result, and any logs or screenshots.
- Mark the test as PASS/FAIL in a shared spreadsheet linked to the test matrix.
Following this manual routine ensures that human intuition can catch layout glitches, confusing wording, or timing issues that a script might deem “because it never deviates from the exact sequence.
How to Test Forgot Password on React Native (Complete Guide): Automated Unit & Integration Testing
Unit and integration tests give fast feedback on business logic and UI contracts without needing a full device. In React Native, the combination of Jest and React Native Testing Library (RNTL) is the de‑facto standard.
Mocking the API Layer
Most forgot‑password flows rely on a service module (e.g., authService.ts) that wraps fetch or Axios. Create a manual mock in __mocks__/authService.ts:
// __mocks__/authService.ts
export const forgotPassword = jest.fn();
export const resetPassword = jest.fn();
In the test file, reset the mock between tests:
import { forgotPassword, resetPassword } from '../src/services/authService';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import ForgotPasswordScreen from '../src/screens/ForgotPasswordScreen';
describe('ForgotPasswordScreen unit tests', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('shows loading spinner while waiting for API', async () => {
forgotPassword.mockReturnValue(new Promise(() => {})); // pending promise
const { getByLabelText, getByRole } = render(<ForgotPasswordScreen />);
fireEvent.changeText(getByLabelText(/email/i), 'user@example.com');
fireEvent.press(getByRole('button', { name: /send reset link/i }));
expect(getByRole('progressbar')).toBeTruthy();
});
it('displays success toast on 200 response', async () => {
forgotPassword.mockResolvedValue({});
const { getByLabelText, getByRole } = render(<ForgotPasswordScreen />);
fireEvent.changeText(getByLabelText(/email/i), 'user@example.com');
fireEvent.press(getByRole('button', { name: /send reset link/i }));
await waitFor(() => expect(getByRole('status')).toHaveText(/reset link sent/i));
});
it('shows error message on 500', async () => {
forgotPassword.mockRejectedValue(new Error('Server error'));
const { getByLabelText, getByRole } = render(<ForgotPasswordScreen />);
fireEvent.changeText(getByLabelText(/email/i), 'user@example.com');
fireEvent.press(getByRole('button', { name: /send reset link/i }));
await waitFor(() =>
expect(getByRole('alert')).toHaveText(/could not send reset link/i)
);
});
});
Testing the Reset‑Link Deep Link
When the app receives a universal link, the linking module parses the token and navigates to the reset screen. Use Jest’s mock of Linking:
import * as Linking from 'react-native';
import ResetPasswordScreen from '../src/screens/ResetPasswordScreen';
describe('Deep link handling', () => {
it('navigates to reset screen with token', () => {
const token = 'abc123';
Linking.getInitialURL = jest.fn().mockResolvedValue(
`myapp://reset?token=${token}`
);
const { getByLabelText } = render(<ResetPasswordScreen />);
expect(getByLabelText(/token/i)).toHaveValue(token);
});
});
Integration Test with Detox (see later)
Unit tests cover pure functions and component rendering; integration tests with Detox or Appium validate navigation, state persistence, and native bridge interactions. Keep unit tests fast (< 200 ms per test) and run them on every PR.
How to Test Forgot Password on React Native (Complete Guide): End-to-End Testing with Detox
Detox excels at gray‑box e2e testing on real devices or simulators, synchronizing with the app’s idle state. Below is a practical setup for testing the forgot‑password flow.
Installation & Configuration
npm install --save-dev detox
detox init -r jest
Update detox.config.js:
module.exports = {
testRunner: 'jest',
apps: {
android.debug: {
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
build: 'npx react-native run-android --variant=debug',
},
ios.debug: {
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/AppName.app',
build: 'npx react-native run-ios --configuration=Debug',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
},
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_4_API_33',
},
},
},
configurations: {
'ios.sim': {
device: 'devices.simulator',
app: 'apps.ios.debug',
},
'android.emu': {
device: 'devices.emulator',
app: 'apps.android.debug',
},
},
};
Writing the Test
Create e2e/forgotPassword.test.js:
describe('Forgot password flow', () => {
beforeEach(async () => {
await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } });
});
it('should send reset link and navigate to reset screen', async () => {
// start on login screen
await expect(element(by.id('loginScreen'))).toBeVisible();
await element(by.id('forgotPasswordLink')).tap();
// forgot password screen
await expect(element(by.id('forgotPasswordScreen'))).toBeVisible();
await element(by.id('emailInput')).replaceText('tester@example.com');
await element(by.id('sendButton')).tap();
// waiting for API mock – we use a mock server that enqueues a response
await waitFor(element(by.id('successToast')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('successToast'))).toHaveText(
/reset link sent/i
);
// simulate clicking the email link – we use deep link
await device.sendToHome();
await device.openURL({ url: `myapp://reset?token=mocktoken123` });
await waitFor(element(by.id('resetPasswordScreen'))).toBeVisible();
await expect(element(by.id('tokenInput'))).toHaveValue('mocktoken123');
});
it('handles server error gracefully', async () => {
await element(by.id('forgotPasswordLink')).tap();
await element(by.id('emailInput')).replaceText('bad@example.com');
await element(by.id('sendButton')).tap();
// mock server returns 500
await waitFor(element(by.id('errorToast')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('errorToast'))).toHaveText(
/could not send reset link/i
);
await expect(element(by.id('emailInput'))).toHaveValue('bad@example.com');
});
});
Running the Tests
detox test -c android.emu # for emulator
detox test -c ios.sim # for simulator
Detox automatically waits for the app to be idle before each action, reducing flakiness caused by animation timing. By mocking the backend (e.g., with msw or a local Express server that returns predetermined responses), you can validate both success and error branches without relying on a real email service.
How to Test Forgot Password on React Native (Complete Guide): Cross‑Platform Testing with Appium
Appium enables testing on real hardware, which is essential for capturing device‑specific quirks such as keyboard overshoot, native share sheets, or OTP auto‑fill behavior.
Setup
npm install -g appium
appium driver install uiautomator2 # Android
appium driver install xcuitest # iOS
Create a wdio.conf.js (WebdriverIO) or use the Appium client directly. Below is a concise JavaScript example using WebdriverIO:
// wdio.conf.js
exports.config = {
runner: 'local',
specs: ['./test/specs/forgotPassword.js'],
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:app': '/path/to/app-debug.apk',
'appium:deviceName': 'Pixel_4_API_33',
'appium:avd': 'Pixel_4_API_33',
'appium:noReset': true,
}],
services: [['appium', { command: 'launch.js' }]],
framework: 'mocha',
mochaOpts: { ui: 'bdd', timeout: 60000 },
};
Test Spec
// test/specs/forgotPassword.js
const { expect } = require('chai');
describe('Forgot password – Appium', function () {
this.timeout(60000);
it('should allow reset via email link', async () => {
// start on login
const forgotLink = await $('~forgotPasswordLink');
await forgotLink.waitForExist();
await forgotLink.click();
const emailInput = await $('~emailInput');
await emailInput.setValue('tester@example.com');
const sendBtn = await $('~sendButton');
await sendBtn.click();
// wait for toast
const toast = await $('~successToast');
await toast.waitForExist({ timeout: 10000 });
const toastText = await toast.getText();
expect(toastText).to.contain('reset link sent');
// simulate opening the email link via deep link
await driver.execute('mobile: deepLink', {
url: 'myapp://reset?token=apptoken123',
});
const resetScreen = await $('~resetPasswordScreen');
await resetScreen.waitForExist();
const tokenField = await $('~tokenInput');
await expect(tokenField).toHaveValue('apptoken123');
});
it('should show rate‑limit toast after too many attempts', async () => {
await $('~forgotPasswordLink').click();
const email = await $('~emailInput');
const btn = await $('~sendButton');
for (let i = 0; i < 6; i++) {
await email.setValue(`user${i}@example.com`);
await btn.click();
}
const rateToast = await $('~rateLimitToast');
await rateToast.waitForExist({ timeout: 15000 });
const txt = await rateToast.getText();
expect(txt).to.match(/too many attempts/i);
});
});
Run with:
npx wdio run wdio.conf.js
Why real devices matter:
- The Android soft‑keyboard may hide the “Send” button on low‑resolution screens; Appium reveals such layout clashes.
- iOS may present a system‑generated “Password AutoFill” prompt after the reset link opens; verifying that the prompt does not interfere with the token field is only possible on actual hardware.
- Network‑switching behavior (e.g., moving from Wi‑Fi to LTE) can be exercised reliably with
adb shell svc wifi disableor the Network Link Conditioner, something simulators approximate less faithfully.
How to Test Forgot Password on React Native (Complete Guide): Leveraging Autonomous, Persona‑Driven Exploration (SUSA)
While scripted tests validate known paths, autonomous explorers uncover surprises by behaving like real users with varied goals, patience levels, and mental models. SUSA (the autonomous QA platform) uploads an APK or points at a web URL, then explores the app using a set of persona profiles—curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user, and more—each with its own tap, scroll, typing speed, and error‑retry tendencies.
How SUSA Approaches the Forgot‑Password Flow
- Entry point discovery – SUSA scans all reachable screens from the launch activity. It identifies the “Forgot password?” link by its accessibility label or text content, even if the link is hidden inside a drawer or a nested modal.
- Persona‑specific interaction –
- *Impatient*: taps the send button twice quickly, expecting instant feedback.
- *Novice*: may leave the email field blank, then tap elsewhere, observing whether validation appears.
- *Adversarial*: attempts SQL‑injection strings (
' OR 1=1--) or extremely long payloads to probe backend sanitization. - *Accessibility*: enables TalkBack before starting, then verifies that every announcement is meaningful and that live regions update errors.
- State tracking – SUSA builds a graph of visited screens and notes dead ends (e.g., a screen where no further action is possible). If the forgot‑password flow leads to a screen with no visible “Back” button, SUSA flags it as a potential UX trap.
- Observed outcomes – For each persona, SUSA records:
- Whether a reset email was actually sent (by monitoring outbound SMTP or a test mailbox).
- Time taken from tap to success/error state.
- Any crashes, ANRs, or unhandled promise rejections captured via logcat/console.
- Deviations from expected UI (e.g., missing spinner, toast not appearing).
What SUSA Finds That Scripts Miss
- Hidden navigation: A version of the app placed the forgot‑password link inside a collapsed accordion that only expands after a long press; scripted tests that always tapped the visible link never discovered that the link was inaccessible to users relying on default tap behavior.
- Race condition on token parsing: When the reset link arrived via a third‑party email app, the deep link handler sometimes received the intent before the Redux store was fully hydrated, causing the token to be cleared. SUSA’s adversarial persona, which introduced a 500 ms delay between tapping the link and returning to the app, consistently reproduced the bug.
- Accessibility live‑region omission: Error messages were rendered as plain
Textcomponents withoutaccessibilityLiveRegion="polite". Screen‑reader users never heard the validation failure; SUSA’s accessibility persona logged the missing announcement. - Rate‑limit bypass via rapid retry: The backend limited requests per IP, but the frontend allowed immediate re‑tap after a failure. SUSA’s impatient persona sent six requests in under two seconds, revealing that the server responded with 200 for the fifth request (limit not enforced) before finally throttling on the sixth.
Integrating SUSA into CI
SUSA offers a CLI (susatest-agent) that can be added to a pipeline step:
pip install susatest-agent
susatest run \
--apk ./android/app/build/outputs/apk/release/app-release.apk \
--personas curious,impatient,adversarial,accessibility \
--timeout 300 \
--output junit.xml
The JUnit report can be consumed by Jenkins, GitHub Actions, or GitLab CI to gate merges if any critical severity (crash, ANR, security finding) is detected.
By combining SUSA’s exploratory power with targeted automated checks, you gain confidence that both the documented script and the real‑world user experience are solid.
How to Test Forgot Password on React Native (Complete Guide): Accessibility & WCAG Checks
Accessibility is not an afterthought; it is a legal requirement in many jurisdictions and a key driver of adoption. The forgot‑password flow must satisfy WCAG 2.1 AA criteria for perceivable, operable, understandable, and robust content.
Automated Audits with axe‑core
React Native projects can use @axe-core/react-native to run automated checks:
npm install --save-dev @axe-core/react-native
Create a test file:
import { runAXE } from '@axe-core/react-native';
import { render } from '@testing-library/react-native';
import ForgotPasswordScreen from '../src/screens/ForgotPasswordScreen';
describe('ForgotPasswordScreen accessibility audit', () => {
it('should have no WCAG violations', async () => {
const { container } = render(<ForgotPasswordScreen />);
const results = await runAXE(container);
expect(results.violations).toHaveLength(0);
});
});
Run it as part of your Jest suite; any violation will fail the test and provide a detailed description (e.g., “button lacks accessible name”).
Manual Screen‑Reader Verification
- Enable TalkBack (Android) or VoiceOver (iOS).
- Navigate to the forgot‑password screen.
- Swipe right to hear each element:
- Email field label should announce “Email address, text field, required”.
- The send button should announce “Send reset link, button”.
- After an error, the live region should announce “Invalid email format”.
- Verify that the user can move focus away from a field without being trapped (no focus loops).
Color Contrast & Touch Targets
- Use the “Contrast Checker” tool (WebAIM) on static mocks or run the
react-native-color-contrastpackage on runtime styles. - Ensure that touch targets (inputs, buttons) are at least 48 dp × 48 dp. In StyleSheet, enforce:
const styles = StyleSheet.create({
button: {
minWidth: 48,
minHeight: 48,
justifyContent: 'center',
alignItems: 'center',
},
});
Dynamic Type & Font Scaling
Test with the largest Dynamic Type setting (iOS) or font scale (Android). Verify that text does not overflow, truncate incorrectly, or cause overlapping layouts. A quick test:
adb shell settings put system font_scale 2.0 # Android
or in the iOS Simulator: Settings → Accessibility → Display & Text Size → Larger Text.
Handling of Accessibility‑Focused Personas in SUSA
When SUSA runs with the accessibility persona, it automatically enables TalkBack/VoiceOver, attempts to complete the flow using only spoken feedback, and asserts that every spoken message is actionable (e.g., “Press send reset link button” rather than an ambiguous “Button”). Any missing or vague announcement is logged as an accessibility defect.
How to Test Forgot Password on React Native (Complete Guide): Security & Privacy Considerations
A forgot‑password endpoint is a high‑value target for attackers seeking to harvest tokens, enumerate users, or Denial‑of‑Service the service. Below are concrete checks to harden the flow.
Token Generation & Transmission
- Use a cryptographically random, URL‑safe base64 string of at least 32 bytes.
- Store the token hash (SHA‑256) in the database alongside an expiry timestamp (e.g., 15 minutes). Never store the raw token.
- When sending the email, include only the token as a query parameter; avoid embedding user‑identifiable data (e.g., user ID) in the URL.
Code snippet (Node/Express example):
import crypto from 'crypto';
import { hash } from 'bcryptjs';
function generateToken() {
return crypto.randomBytes(32).toString('base64url');
}
async function hashToken(plainToken) {
return await hash(plainToken, 10);
}
// In route handler:
const plain = generateToken();
const hashed = await hashToken(plain);
// store hashed + expiresAt
// email user: https://myapp.com/reset?token=${plain}
Rate Limiting & Brute‑Force Protection
- Implement per‑IP and per‑account limits on the
/forgotendpoint (e.g., 5 requests per 15 minutes). - Return the same generic response regardless of whether the email exists to prevent enumeration.
- For the reset endpoint (
/reset), enforce a stricter limit (e.g., 3
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