Skip to content

Stratégie Test IDs — Applications Mobile (React Native / Expo)

Convention et guide d'implémentation des identifiants de test pour les tests e2e sur les applications mobile (mobile-client, mobile-pro).


1. Objectif

Fournir des points d'ancrage stables aux outils de test e2e (Appium, Detox, Maestro) via la prop testID de React Native. Ces identifiants sont indépendants du style, du texte affiché et de la hiérarchie des vues — ils survivent aux refactors UI.


2. Outil e2e recommandé

OutilUsageSélecteur
AppiumTests cross-platform (iOS + Android)resource-id (Android), accessibilityIdentifier (iOS)
MaestroTests rapides, scripting YAMLtestID directement
DetoxTests grey-box React Nativeby.id('testID')

React Native mappe automatiquement testID vers les attributs natifs de chaque plateforme.


3. Convention de nommage

Format

<screen>.<section>.<element>
  • screen : nom de l'écran en kebab-case (ex: send-amount, change-pin, login)
  • section : zone logique de l'écran (ex: form, header, keypad, list)
  • element : élément ciblé (ex: phone-input, submit-btn, error-text)

Règles

  1. Tout en minuscules
  2. Séparateur de niveau : point (.)
  3. Séparateur de mots : tiret (-)
  4. Pas de préfixe d'app (cmms:, pro:) — le contexte de test détermine l'app
  5. Maximum 3 niveaux de profondeur
  6. Les éléments répétés (listes) utilisent un suffixe dynamique : list.item-${index} ou list.item-${id}

Exemples

login.form.phone-input
login.form.pin-input
login.form.submit-btn
login.form.error-text

send-amount.keypad.key-5
send-amount.keypad.key-delete
send-amount.display.amount-text
send-amount.quick-amounts.btn-5000
send-amount.action.continue-btn

change-pin.form.current-pin
change-pin.form.new-pin
change-pin.form.confirm-pin
change-pin.form.submit-btn
change-pin.form.error-text

settings.list.item-change-pin
settings.list.item-sessions
settings.list.item-logout

transactions.list.item-${transactionId}
transactions.filter.date-picker
transactions.filter.type-select

4. Support testID dans @nex/ui-mobile

Les composants du design system acceptent déjà la prop testID et la propagent sur leurs sous-éléments internes. Pas de wrapper <View testID> nécessaire — passer la prop directement suffit.

Composants composites (propagation par suffixe)

ComposanttestID de baseSuffixes propagés
NxInputTextInput.container (wrapper), .label, .error, .hint
NxButtonTouchableOpacity.label (texte), .loader (spinner)
NxPhoneInputView racine.country-selector, .number-input, .modal, .country-<code> (ex: .country-cm)
NxPageHeaderSafeAreaView.back-btn, .title

Composants leaf (forwarding simple)

Les composants suivants acceptent testID et le placent sur leur élément racine : NxText, NxIcon, NxIconButton, NxBadge, NxAvatar, NxImage, NxSvgIcon, NxCard, NxRow, NxColumn, NxScreen.

Exemple d'usage

tsx
// Input avec label et erreur
<NxInput
  testID="login.form.email-input"
  label="Email"
  error={emailError}
/>
// => testID="login.form.email-input"          sur le TextInput
//    testID="login.form.email-input.container" sur le wrapper
//    testID="login.form.email-input.label"    sur le label
//    testID="login.form.email-input.error"    sur le message d'erreur

// Sélection d'un pays précis
<NxPhoneInput testID="login.phone.phone-input" ... />
// => testID="login.phone.phone-input.country-cm" sur l'item Cameroun dans la modal

// Bouton avec loader
<NxButton testID="login.form.submit-btn" loading>Valider</NxButton>
// => testID="login.form.submit-btn"         sur le TouchableOpacity
//    testID="login.form.submit-btn.loader" sur l'ActivityIndicator

Règle

Lors de l'ajout de nouveaux composants dans @nex/ui-mobile, suivre le même contrat :

  • Prop testID?: string dans l'interface
  • Base sur l'élément le plus sémantiquement pertinent (TextInput pour un input, TouchableOpacity pour un bouton)
  • Sous-éléments importants reçoivent des suffixes descriptifs (.label, .error, .back-btn, etc.)

5. Implémentation sur les écrans

Exemple : AmountScreen (Send Flow)

tsx
// apps/mobile-client/screens/SendFlow/Step3_Amount/AmountScreen.tsx

const AmountScreen: React.FC = () => {
  const { transferData, userBalance, setAmount } = useSendFlowStore();
  const [amountString, setAmountString] = useState("");
  const [error, setError] = useState<string | null>(null);

  return (
    <SafeAreaView testID="send-amount.screen">
      <PageHeader testID="send-amount.header" title="Montant" />

      <View testID="send-amount.display">
        <ThemedText testID="send-amount.display.amount-text">
          {currentAmount > 0 ? formatAmount(currentAmount) : "0"}
        </ThemedText>
        <ThemedText testID="send-amount.display.currency-text">XAF</ThemedText>
        {error && (
          <ThemedText testID="send-amount.display.error-text">{error}</ThemedText>
        )}
      </View>

      {/* Montants rapides */}
      <View testID="send-amount.quick-amounts">
        {quickAmounts.map((item) => (
          <TouchableOpacity
            key={item.value}
            testID={`send-amount.quick-amounts.btn-${item.value}`}
            onPress={() => handleQuickAmountSelect(item.value)}
          >
            <ThemedText>{item.label}</ThemedText>
          </TouchableOpacity>
        ))}
      </View>

      {/* Clavier numérique */}
      <View testID="send-amount.keypad">
        {keys.map((key) => (
          <TouchableOpacity
            key={key}
            testID={`send-amount.keypad.key-${key}`}
            onPress={() => handleKeyPress(key)}
          >
            <ThemedText>{key}</ThemedText>
          </TouchableOpacity>
        ))}
      </View>

      <NxButton
        testID="send-amount.action.continue-btn"
        variant="primary"
        fullWidth
        onPress={handleContinue}
      >
        Continuer
      </NxButton>
    </SafeAreaView>
  );
};

Exemple : ChangePinCodeScreen

tsx
// apps/mobile-pro/screens/Settings/ChangePinCode/ChangePinCodeScreen.tsx

const ChangePinCodeScreen = () => {
  return (
    <SafeAreaView testID="change-pin.screen">
      <NxPageHeader testID="change-pin.header" title="Changer le code PIN" />

      <ScrollView testID="change-pin.form">
        <SimpleInput
          testID="change-pin.form.current-pin"
          label="Code PIN actuel"
          type="password"
          pinLength={6}
          value={currentPin}
          onChangeText={setCurrentPin}
        />

        <SimpleInput
          testID="change-pin.form.new-pin"
          label="Nouveau code PIN"
          type="password"
          pinLength={6}
          value={newPin}
          onChangeText={setNewPin}
        />

        <SimpleInput
          testID="change-pin.form.confirm-pin"
          label="Confirmer le nouveau code PIN"
          type="password"
          pinLength={6}
          value={confirmPin}
          onChangeText={setConfirmPin}
        />

        {error && (
          <NxText testID="change-pin.form.error-text" variant="caption" color="error">
            {error}
          </NxText>
        )}

        <NxButton
          testID="change-pin.form.submit-btn"
          variant="primary"
          fullWidth
          loading={isLoading}
          disabled={!isFormValid || isLoading}
          onPress={handleChangePin}
        >
          Valider le nouveau PIN
        </NxButton>
      </ScrollView>
    </SafeAreaView>
  );
};

6. Éléments à cibler en priorité

Ne pas mettre un testID sur tout. Cibler les éléments avec lesquels les tests interagissent :

CatégorieExemplesPriorité
Champs de saisieTextInput, PIN input, phone inputHaute
Boutons d'actionSubmit, continuer, annuler, confirmerHaute
Messages d'erreurValidation inline, erreurs APIHaute
Écrans (container root)SafeAreaView principalHaute
Éléments de navigationTabs, liens, retourMoyenne
Éléments de listeLignes de transaction, items de settingsMoyenne
Textes d'affichageSolde, montant, statutMoyenne
ModalesConfirmation, sélection paysMoyenne
Éléments décoratifsIcônes, séparateurs, badgesBasse (éviter)

7. Helper utilitaire (optionnel)

Un helper pour assurer la cohérence et simplifier la création de test IDs :

tsx
// packages/shared-utils/src/test-ids.ts

/**
 * Construit un testID selon la convention screen.section.element
 * Usage : buildTestId('login', 'form', 'phone-input') => 'login.form.phone-input'
 */
export function buildTestId(...parts: (string | undefined)[]): string | undefined {
  const filtered = parts.filter(Boolean);
  if (filtered.length === 0) return undefined;
  return filtered.join(".");
}

/**
 * Crée un scope de testID pour un écran donné
 * Usage :
 *   const tid = createTestIdScope('login');
 *   tid('form', 'phone-input') => 'login.form.phone-input'
 *   tid('form', 'submit-btn') => 'login.form.submit-btn'
 */
export function createTestIdScope(screen: string) {
  return (...parts: string[]): string => {
    return [screen, ...parts].join(".");
  };
}

Utilisation dans un écran

tsx
const LoginScreen = () => {
  const tid = createTestIdScope("login");

  return (
    <SafeAreaView testID={tid("screen")}>
      <NxPhoneInput testID={tid("form", "phone-input")} />
      <NxInput testID={tid("form", "pin-input")} />
      <NxButton testID={tid("form", "submit-btn")}>Connexion</NxButton>
    </SafeAreaView>
  );
};

8. Stripping en production

React Native ignore testID en production par défaut sur Android. Sur iOS, accessibilityIdentifier reste présent mais n'est pas visible par l'utilisateur.

Si nécessaire, un plugin Babel peut strip les props testID en build de production :

js
// babel.config.js (optionnel, uniquement si nécessaire)
module.exports = function (api) {
  const plugins = [];

  if (process.env.NODE_ENV === "production") {
    plugins.push(["react-native-production-testid-strip"]);
  }

  return {
    presets: ["babel-preset-expo"],
    plugins,
  };
};

Note : Ce stripping n'est pas recommandé dans un premier temps. Les testID ont un impact négligeable sur les performances et peuvent être utiles pour le debug en production (crash reporting, analytics).


9. Parcours critiques à couvrir en priorité

  1. Authentificationlogin.* (phone, PIN, biométrie, erreurs)
  2. Transfert P2Psend-*.* (sélection contact, montant, récap, confirmation)
  3. Cash-in / Cash-outcash-in.*, cash-out.*
  4. Consultation soldehome.wallet.*
  5. Historique transactionstransactions.list.*, transactions.detail.*
  6. Paramètres sécuritéchange-pin.*, sessions.*
  7. KYCkyc.* (upload documents, vérification)

NxPay — Plateforme fintech CEMAC