How to Test Forgot Password on React Native (Complete Guide)

How to Test Forgot Password on React Native (Complete Guide)

January 27, 2026 · 15 min read · How-To Guides

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 IDCategoryDescriptionExpected ResultPriority
FP‑01Happy PathUser 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‑02Error Path – Invalid EmailUser submits malformed email (missing @).Inline validation error appears, no network request sent.P0
FP‑03Error Path – Non‑Existent EmailUser submits email not registered.Generic message “If the address exists, you will receive an email” to avoid enumeration.P0
FP‑04Error Path – Backend 500Simulate server error on POST /forgot.Error toast displayed, UI retains input, retry button enabled.P1
FP‑05Error Path – Backend 429 (Rate Limit)Simulate too many requests.Warning about too many attempts, lockout timer shown, further submissions blocked.P1
FP‑06Edge Case – Empty InputUser taps submit with empty field.Same as FP‑02 (validation error).P0
FP‑07Edge Case – Whitespace OnlyUser submits spaces or tabs.Trimmed value treated as empty → validation error.P0
FP‑08Edge Case – Very Long EmailUser pastes 300‑character string.Input rejects or truncates per UI limit, no crash.P1
FP‑09Edge Case – Special CharactersEmail contains +, -, _, . (allowed) and Unicode.Accepted if conforms to RFC 5322 subset used by backend.P1
FP‑10Edge Case – Network Loss Mid‑RequestDisable Wi‑Fi/cellular after submit.App shows offline toast, retains state, retries on reconnect.P1
FP‑11Edge Case – Intermittent Slow ResponseThrottle network to 2 seconds delay.UI shows spinner, does not timeout prematurely.P2
FP‑12Accessibility – Screen ReaderTalkBack/VoiceOver reads each field, error messages, and button states.All announcements are clear, live regions update errors.P1
FP‑13Accessibility – Color ContrastVerify contrast ratio ≥ 4.5:1 for text and ≥ 3:1 for icons.Passes automated contrast check.P2
FP‑14Accessibility – Touch TargetButtons and input fields ≥ 48 dp.Meets guideline.P2
FP‑15Security – Token in URLReset link contains one‑time token; ensure token not logged.No token appears in console, network logs, or crash reports.P0
FP‑16Security – Rate Limiting on Reset EndpointAttempt to brute‑force token guess.After N failures, endpoint returns 429 or locks account for duration.P0
FP‑17Security – Email Enumeration ProtectionCompare responses for existing vs. non‑existing email.Identical timing and message content.P0
FP‑18Localization – RTL LayoutRun with Arabic/Hebrew locale.Layout mirrors correctly, no clipped text.P2
FP‑19Device Fragmentation – Low‑End AndroidTest on Android Go device (≤ 1 GB RAM).UI responsive, no frame drops > 16 ms.P2
FP‑20Deep Link Handling – Reset Link ClickClick 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.

  1. Environment preparation
  1. Baseline happy‑path validation
  1. Error‑path execution
  1. Edge‑case exploration
  1. Accessibility spot‑check
  1. Security sniffing
  1. Documentation

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:

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

  1. 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.
  2. Persona‑specific interaction
  1. 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.
  2. Observed outcomes – For each persona, SUSA records:

What SUSA Finds That Scripts Miss

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

  1. Enable TalkBack (Android) or VoiceOver (iOS).
  2. Navigate to the forgot‑password screen.
  3. Swipe right to hear each element:
  1. Verify that the user can move focus away from a field without being trapped (no focus loops).

Color Contrast & Touch Targets


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

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

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