Retour au blog
    25 octobre 2026Tutorial

    Tests mobiles sans script en JSON : un seul fichier pour Android et iOS

    Réponse courte : votre application a renommé « Sign in » en « Log in », et chaque test enregistré qui touche ce bouton échoue maintenant sur Android, puis sur iOS. Un test conservé sous forme d'enregistrement ne se corrige en général qu'en l'enregistrant à nouveau. Nous conservons plutôt un test mobile dans un fichier JSON : des actions et des sélecteurs, aucune coordonnée. Vous pouvez l'écrire à la main, le faire écrire par un agent IA, le relire dans une pull request et le corriger en modifiant une ligne. Avec "platform": "mobile", le même fichier s'exécute sur un téléphone Android et sur un iPhone.

    Cet article est la référence que nous aurions voulu avoir en le construisant : la forme du format, un extrait pour chaque action, la façon dont les deux plateformes partagent un seul fichier, et ce que répond le validateur quand quelque chose ne va pas. Chaque extrait ci-dessous a été vérifié avec le validateur en production avant publication.

    Pourquoi les enregistrements se dégradent et pas les fichiers

    Un test enregistré contient en général trois choses qui cassent : une position à l'écran pour chaque toucher, un instantané de l'écran sur lequel il a été enregistré, et un modèle d'appareil qu'il suppose sans le dire. Changez la mise en page, le téléphone ou l'OS, et l'enregistrement ne correspond plus.

    Un flux JSON ne garde rien de tout cela. Chaque étape désigne un élément par ce qu'une personne en dirait (son texte ou son identifiant), et chaque exécution cherche cet élément sur l'écran tel qu'il est à cet instant. Rien de ce qui a été enregistré sur Android n'est jamais comparé à un iPhone, puisque rien n'est enregistré du tout.

    Trois conséquences pratiques :

    • Vous pouvez le lire. Dans un diff, la personne qui relit voit tap "Add to cart", pas un bloc binaire.
    • Vous pouvez le corriger. Un bouton renommé, c'est un mot à changer, pas un nouvel enregistrement.
    • Tout le monde peut l'écrire. Une testeuse dans son éditeur, un script qui le génère à partir d'un tableur ou un agent IA qui vient d'observer l'application produisent tous le même fichier.

    Le construire en quatre gestes

    Les quatre gestes : écrire le fichier à la main ou avec un agent IA, le valider, l'importer depuis l'onglet Tests ou avec flow_import, puis le rejouer sur un téléphone Android et un iPhone. Quand l'application change, l'exporter, modifier une ligne et le mettre à jour en version suivante.

    1. Commencez par launchApp. Nommez l'application, pour que l'exécution démarre toujours sur un écran connu.
    2. Ajoutez une étape par geste d'une personne, et désignez chaque élément par ce qui est à l'écran : le mot sur le bouton, le libellé du champ. Pas de coordonnées, pas de tailles d'écran, pas d'attente pour les animations.
    3. Validez. Chaque erreur nomme l'étape, le champ et la correction : vous ne devinez jamais ce que le validateur attendait.
    4. Importez-le et rejouez-le sur un téléphone Android et un iPhone.

    Si vous savez décrire le test à un collègue, vous savez l'écrire. Un agent IA peut aussi l'écrire, à partir de la même description.

    Le tout sur un seul écran

    Voici un flux complet qui se connecte, trouve un produit et l'ajoute au panier, sur l'un ou l'autre téléphone :

    json
    {
      "name": "Sign in and add to cart",
      "platform": "mobile",
      "steps": [
        { "action": "launchApp", "appId": { "android": "com.example.shop", "ios": "com.example.Shop" } },
    
        { "action": "tap", "selectors": [{ "using": "id", "value": "email_input" }, { "using": "text", "value": "Email" }] },
        { "action": "type", "value": "{{secret.SHOP_EMAIL}}" },
        { "action": "tap", "selectors": [{ "using": "id", "value": "password_input" }, { "using": "text", "value": "Password" }] },
        { "action": "type", "value": "{{secret.SHOP_PASSWORD}}" },
        { "action": "tap", "selectors": [{ "using": "text", "value": "Sign in" }] },
        { "action": "assert", "assert": { "kind": "exists" }, "selectors": [{ "using": "text", "value": "Home" }], "timeoutMs": 15000 },
    
        { "action": "scroll", "selectors": [{ "using": "text", "value": "Running shoes" }] },
        { "action": "tap", "selectors": [{ "using": "text", "value": "Running shoes" }] },
        { "action": "tap", "selectors": [{ "using": "text", "value": "Add to cart" }] },
        { "action": "assert", "assert": { "kind": "textContains", "expected": "1 item" }, "selectors": [{ "using": "id", "value": "cart_badge" }] }
      ]
    }

    Onze étapes, pas de code, pas de coordonnées, et aucun « si Android » nulle part. La suite de l'article décrit ce que chacune de ces lignes peut contenir.

    L'enveloppe du flux

    ChampValeurRemarques
    nametexte libreUn nom déjà utilisé crée un second flux. Rien n'est écrasé.
    platform"android", "ios" ou "mobile"mobile est un seul flux qui s'exécute sur les deux.
    category"test" (par défaut) ou "fragment"Un fragment est un morceau réutilisable, une connexion par exemple.
    tmsLinkfacultatifRelie le flux à son cas de test dans TestRail, Jira ou Azure DevOps.
    stepstableauJusqu'à 500, étapes imbriquées comprises.

    Les sélecteurs : comment une étape trouve son élément

    Un sélecteur s'écrit { "using": ..., "value": ... }. Vous pouvez en indiquer plusieurs. Si l'un ne trouve rien, ou correspond à plus d'un contrôle, le suivant est essayé. La section suivante explique dans quel ordre.

    json
    "selectors": [
      { "using": "id",   "value": "com.example.shop:id/checkout" },
      { "using": "text", "value": "Checkout" }
    ]

    Trois types fonctionnent sur les deux téléphones :

    • text est le texte visible ou le libellé d'accessibilité.
    • label se comporte de la même façon. Prenez le mot qui vous parle le plus.
    • id est le resource-id Android ou l'identifiant d'accessibilité iOS. Si vos développeurs posent le même identifiant sur les deux plateformes, c'est le sélecteur le plus stable que vous ayez.

    La comparaison de texte ignore la casse, les espaces superflus et la largeur des caractères, mais reste exacte pour le reste. "sign IN" trouve « Sign in ». "Network and Internet" ne trouve pas « Network & internet ».

    Quand le libellé varie selon la version, la région ou la version de l'OS, faites du sélecteur un motif. Le texte entier doit correspondre, sans tenir compte de la casse :

    json
    { "using": "text", "value": "(Allow|Allow While Using App|While using the app)", "regex": true }

    regex ne s'applique qu'à text et label. Gardez des motifs simples : le validateur refuse ceux qui pourraient bloquer, comme les répétitions imbriquées ou les références arrière.

    Quatre autres types de sélecteurs existent. Ils ne sont valides que là où l'étape s'exécute sur un seul OS, comme l'explique la section « Un fichier, deux plateformes » plus bas.

    usingPlateformeExemple de valeur
    accessibilityIdAndroid."Open navigation drawer"
    valueiOS."Back"
    classL'une ou l'autre, dans un bloc de plateforme."android.widget.Switch" / "XCUIElementTypeSwitch"
    xpathL'une ou l'autre, dans un bloc de plateforme."//*[@text='Save']"

    Un xpath doit sélectionner par attribut, comme //*[@text='Save']. Un chemin purement positionnel tel que //LinearLayout[3]/Button[1] est refusé : il décrit où se trouvait l'élément le jour où vous l'avez écrit.

    Une règle qui fait gagner des heures : l'exécution ne choisit jamais au hasard entre deux contrôles. Une ligne « Paramètres » et le texte « Paramètres » qu'elle contient forment un seul contrôle, compté une fois. Cinq boutons « Supprimer » sont cinq contrôles. Si aucun sélecteur ne les ramène à un seul, l'étape échoue en indiquant combien correspondent, et vous ajoutez un sélecteur plus précis. Un test qui touche le premier de cinq boutons « Supprimer » passe, jusqu'au jour où il supprime la mauvaise ligne.

    Comment une étape s'exécute sur le téléphone

    Comment une étape s'exécute : lire l'arbre d'éléments en direct toutes les 500 ms, classer les sélecteurs selon la plateforme, garder le sélecteur qui désigne un seul contrôle, faire défiler vers lui s'il est hors écran, puis agir en son centre. Si le délai expire, 5 secondes par défaut, l'étape échoue en disant pourquoi.

    Rien dans le fichier ne dit où se trouve un élément. Chaque étape qui a des sélecteurs trouve son élément sur l'écran en direct, en cinq temps :

    1. Lire l'écran. L'exécution lit l'arbre d'éléments du téléphone en direct, celui que voient les services d'accessibilité. Elle le relit toutes les 500 ms jusqu'à ce que l'étape réussisse ou que le délai expire.
    2. Classer les sélecteurs. Chaque plateforme les essaie dans son propre ordre, toujours le même. Sur Android : d'abord le texte visible, puis le libellé d'accessibilité, puis le resource-id. Sur iPhone : d'abord l'identifiant d'accessibilité, puis le libellé, puis la valeur. L'ordre dans lequel vous les écrivez ne sert qu'à départager.
    3. Garder un seul contrôle. Un sélecteur qui désigne un seul contrôle l'emporte. Les copies d'un même contrôle comptent pour un. Deux contrôles différents rendent ce sélecteur ambigu, et le suivant est essayé.
    4. L'amener à l'écran. Une longue liste garde des lignes que l'œil ne voit pas. Si l'élément existe mais est hors écran, l'exécution fait défiler vers lui, jusqu'à cinq fois.
    5. Agir en son centre. Le toucher tombe au milieu de l'élément tel qu'il est dessiné maintenant, sur ce téléphone, à cette taille d'écran. Un toucher qui ne viserait qu'un grand conteneur, comme la fenêtre entière, est refusé : il passerait sans rien presser.

    Si le délai expire (5 secondes, ou le timeoutMs de l'étape), l'étape échoue en disant pourquoi : l'élément est introuvable, ou plusieurs contrôles correspondent, et elle dit combien. Une étape optional est simplement sautée.

    Voici ce que fait chaque action une fois son élément connu :

    ActionCe que fait l'exécution
    launchAppDémarre l'application par son nom de paquet ou son bundle id. Aucune recherche d'élément.
    tap, doubleTap, longPressTrouve l'élément, puis touche son centre. longPress maintient pendant durationMs.
    typeAucune recherche. Saisit dans le champ qui a le focus. Un {{secret.KEY}} est rempli pour cette exécution seulement, et l'étape enregistrée garde la référence.
    scrollFait défiler jusqu'à ce que l'élément soit à l'écran.
    waitFait une pause de la durée indiquée. Rien d'autre.
    assertLit l'écran toutes les 500 ms jusqu'à ce que la condition soit vraie ou que le délai expire.
    ifVérifie sa condition de la même façon, puis exécute then ou else. platformIs ne lit pas l'écran.
    pressButton, keyAppuie sur le bouton matériel ou la touche. Aucune recherche.

    Il s'exécute sur le téléphone, pas à travers une grille. Un flux mobile ne passe pas par la grille Selenium ou Appium qu'utilisent les suites d'automatisation. L'exécution pilote le téléphone directement depuis la machine où il est branché. Pas de hub par lequel transiter, pas de nœud à attendre, pas de file de sessions distantes : rien ne s'interpose entre l'étape et l'écran. Chaque étape, c'est une lecture de l'écran et un geste, et le temps entre deux étapes est celui de l'application, pas celui de l'infrastructure.

    Chaque action, avec un extrait

    launchApp : lancer l'application testée

    json
    { "action": "launchApp", "appId": { "android": "com.example.shop", "ios": "com.example.Shop" } }

    Un nom de package et un bundle id sont deux chaînes différentes, donc un flux mobile nomme les deux. Un flux mono-plateforme peut utiliser une simple chaîne, et Android peut préciser une activité de lancement :

    json
    { "action": "launchApp", "appId": "com.example.shop", "appActivity": ".MainActivity" }

    Placez-la en première étape. Sinon, le validateur vous prévient que la relecture démarrera sur l'écran que le téléphone affiche à ce moment-là.

    tap

    json
    { "action": "tap", "selectors": [{ "using": "text", "value": "Sign in" }] }

    doubleTap

    json
    { "action": "doubleTap", "selectors": [{ "using": "id", "value": "product_image" }] }

    longPress

    json
    { "action": "longPress", "selectors": [{ "using": "text", "value": "Size 42" }], "durationMs": 800 }

    durationMs est facultatif, de 100 à 10 000.

    type : saisir du texte dans le champ actif

    json
    { "action": "tap", "selectors": [{ "using": "text", "value": "Search" }] },
    { "action": "type", "value": "running shoes" }

    type n'a pas de sélecteur. Il saisit dans le champ qui a le focus, donc touchez d'abord le champ. C'est l'ordre que suit une personne, et le clavier est ainsi affiché avant l'envoi du texte.

    Les mots de passe et autres secrets ne vont jamais dans le fichier. Enregistrez-les une fois dans Variables, puis désignez-les par leur nom :

    json
    { "action": "tap", "selectors": [{ "using": "text", "value": "Password" }] },
    { "action": "type", "value": "{{secret.SHOP_PASSWORD}}" }

    Si le champ que vous venez de toucher ressemble à un champ de mot de passe et que la valeur est un texte littéral, le flux est refusé au lieu d'être enregistré. Les flux mobiles n'ont pas de variables d'environnement : {{secret.KEY}} est la seule forme de référence qu'ils résolvent.

    scroll : faire apparaître un élément

    json
    { "action": "scroll", "selectors": [{ "using": "text", "value": "Running shoes" }] }

    Vous indiquez ce que vous voulez atteindre, pas la distance à faire défiler. L'exécution fait défiler jusqu'à ce que l'élément soit à l'écran, donc l'étape résiste à une liste qui s'allonge ou à un téléphone plus grand.

    wait

    json
    { "action": "wait", "value": 1500 }

    En millisecondes, de 1 à 120 000. À utiliser en dernier recours. Une attente fixe est soit trop courte sur un réseau lent, soit du temps perdu sur un réseau rapide. Pour attendre un écran, vérifiez quelque chose qu'il affiche et donnez à cette étape un timeoutMs plus long :

    json
    { "action": "assert", "assert": { "kind": "exists" }, "selectors": [{ "using": "text", "value": "Home" }], "timeoutMs": 15000 }

    assert : quatre types

    json
    { "action": "assert", "assert": { "kind": "exists" },    "selectors": [{ "using": "text", "value": "Order placed" }] },
    { "action": "assert", "assert": { "kind": "notExists" }, "selectors": [{ "using": "text", "value": "Something went wrong" }] },
    { "action": "assert", "assert": { "kind": "textEquals",   "expected": "Payment received" }, "selectors": [{ "using": "id", "value": "order_status" }] },
    { "action": "assert", "assert": { "kind": "textContains", "expected": "1 item" }, "selectors": [{ "using": "id", "value": "cart_badge" }] }

    textEquals et textContains exigent expected. exists et notExists ne doivent pas en avoir. La comparaison de texte est la même que celle des sélecteurs : tolérante, mais exacte.

    pressButton : boutons physiques

    json
    { "action": "pressButton", "button": "home" }

    Les boutons sont home, volumeUp, volumeDown et lock, et les quatre fonctionnent sur les deux téléphones.

    key : retour Android

    json
    { "action": "key", "key": "back" }

    ou n'importe quel code de touche Android :

    json
    { "action": "key", "keyCode": 66 }

    Les iPhone n'ont pas de touche retour, donc key est réservé à Android. Dans un flux mobile, il doit se trouver dans un bloc de plateforme, l'objet de la section suivante.

    if : bifurquer selon ce qui est à l'écran

    json
    {
      "action": "if",
      "condition": { "check": "exists", "timeoutMs": 3000 },
      "selectors": [{ "using": "text", "value": "(Allow|Allow While Using App|While using the app)", "regex": true }],
      "then": [
        { "action": "tap", "selectors": [{ "using": "text", "value": "(Allow|Allow While Using App|While using the app)", "regex": true }] }
      ]
    }

    Ce bloc gère le dialogue d'autorisation qui apparaît après une installation neuve et disparaît à la deuxième exécution. check accepte les quatre types d'assertion. then et else contiennent chacun une liste d'étapes, et les blocs s'imbriquent jusqu'à cinq niveaux.

    Deux options acceptées par toutes les étapes

    json
    { "action": "tap", "selectors": [{ "using": "text", "value": "Not now" }], "optional": true, "timeoutMs": 2000 }
    • optional: true saute l'étape quand son élément est absent, au lieu de faire échouer l'exécution. Utile pour une bannière promotionnelle qui n'apparaît que parfois.
    • timeoutMs (de 100 à 120 000) est la durée pendant laquelle l'étape cherche son élément.

    Un fichier, deux plateformes

    La règle est courte : des sélecteurs partagés par défaut, et un bloc pour les vraies différences.

    Un seul fichier de flux mobile au centre, un téléphone Android à gauche et un iPhone à droite. Le sélecteur text se résout en texte visible ou content-desc sur Android, et en libellé ou valeur sur iOS. Le sélecteur id se résout en resource-id sur Android et en identifiant d'accessibilité sur iOS. launchApp nomme les deux identifiants d'application, et un bloc platformIs envoie Android dans then et iOS dans else.

    La plupart des étapes ne mentionnent jamais de plateforme. text et id se résolvent sur les deux téléphones, et launchApp accepte les deux identifiants d'application. Quand les deux applications diffèrent vraiment, encadrez cette partie dans platformIs :

    json
    {
      "action": "if",
      "condition": { "check": "platformIs", "expected": "android" },
      "then": [ { "action": "key", "key": "back" } ],
      "else": [ { "action": "tap", "selectors": [{ "using": "value", "value": "Back" }] } ]
    }

    else désigne l'autre téléphone. Dans chaque branche, le flux est mono-plateforme : l'étape key, réservée à Android, et le sélecteur value, réservé à iOS, y deviennent permis. Il n'existe pas de surcharge de plateforme étape par étape. Chaque différence vit dans un bloc visible, ce qui montre à la personne qui relit exactement où les deux applications divergent.

    Le validateur suit l'OS sur lequel chaque étape peut s'exécuter. Une branche qui ne peut jamais s'exécuter est une erreur, par exemple un bloc ios dans un flux réservé à Android. Un sélecteur propre à une plateforme placé hors d'un bloc l'est aussi.

    Ce que répond le validateur quand vous vous trompez

    Voici un flux qui contient quatre erreurs :

    json
    {
      "name": "Broken on purpose",
      "platform": "mobile",
      "steps": [
        { "action": "launchApp", "appId": "com.example.shop" },
        { "action": "tap", "selectors": [{ "using": "accessibilityId", "value": "login_button" }] },
        { "action": "tap", "selectors": [{ "using": "text", "value": "Password" }] },
        { "action": "type", "value": "hunter2" },
        { "action": "key", "key": "back" }
      ]
    }

    Et voici la réponse du validateur, telle quelle (elle est en anglais) :

    text
    Not valid: 4 errors. Nothing was saved.
    - steps[0].appId: a package name and a bundle id differ; give {"android":"com.example.app","ios":"com.example.App"}
    - steps[1].selectors[0].using: "accessibilityId" is Android-only; put this step in an "if" with
      condition {"check":"platformIs","expected":"<android|ios>"}, or make the flow single-platform
    - steps[3].value: this types into a password field; a literal value is not stored.
      Use {{secret.KEY}} and save the secret under Variables
    - steps[4].action: "key" is Android-only; put this step in an "if" with
      condition {"check":"platformIs","expected":"<android|ios>"}, or make the flow single-platform

    Chaque erreur nomme l'étape, le champ et la correction. Un champ qu'une action n'utilise pas est lui aussi une erreur, pas une donnée ignorée en silence : une faute comme "selector" au lieu de "selectors" est détectée avant la première exécution. C'est cette précision qui rend le format utilisable par des agents : la réponse est assez précise pour être corrigée sans qu'un humain la lise.

    Le téléverser : dans l'application, ou par votre IA

    Dans l'application. Ouvrez Test Manager → Tests et cliquez sur Import JSON. Déposez le fichier .json (1 Mo au maximum), choisissez-le, ou collez-le dans la zone de texte. Le flux est vérifié avant tout enregistrement. Les erreurs s'affichent sous la forme step N · field · message, et Import reste désactivé tant qu'elles ne sont pas corrigées. Les avertissements, comme un {{secret.KEY}} pas encore enregistré, ne bloquent rien. Cliquez sur Import : le flux devient un nouveau test avec son propre identifiant T###.

    Par votre assistant IA. Connectez Claude, Cursor ou un autre client MCP à RobotActions et donnez-lui le fichier :

    Valide sign-in-and-add-to-cart.json avec flow_validate. S'il est valide, enregistre-le tel quel avec flow_import.

    flow_validate fait le même essai à blanc que la boîte de dialogue, et flow_import le même enregistrement. Dans les deux cas, le fichier passe sans modification.

    Comme un flux est un simple fichier, tout ce que vous faites déjà avec des fichiers fonctionne ici. Rangez les flux à côté du code de l'application, relisez-les dans les pull requests et téléversez la version relue.

    Le faire écrire par un agent

    Un agent utilise les mêmes outils, plus un : flow_validate (un essai à blanc), flow_import (enregistrer) et flow_replay_start (l'exécuter sur un appareil). Une demande aussi simple que celle-ci suffit :

    Ouvre l'application de boutique sur un téléphone Android et reconstitue le parcours connexion + ajout au panier. Écris-le comme flux mobile, valide-le, enregistre-le, puis rejoue-le sur un téléphone Android et sur un iPhone et dis-moi quelles étapes ont échoué.

    Un agent qui a l'appareil sous les yeux lit le vrai arbre des éléments avant d'écrire un sélecteur : il écrit id: email_input parce que cet identifiant existe, pas parce qu'il semble plausible. Si flow_validate renvoie une erreur, celle-ci indique à l'agent quel champ modifier. L'agent n'a jamais à deviner ce que voulait le validateur.

    Le corriger quand l'application change

    C'est là qu'un fichier l'emporte sur un enregistrement. Quand une version renomme « Sign in » en « Log in » :

    1. Cliquez sur l'icône Export JSON du test. Vous obtenez <name>.json : le flux, avec son recordingId et sa version.
    2. Changez un mot.
    3. Cliquez sur l'icône Update from JSON du test et chargez le fichier. La boîte de dialogue indique ce qui va changer, par exemple replaces 11 steps with 11 · v3 → v4. Cliquez sur Replace steps.

    Un assistant IA fait la même chose avec flow_export, une modification, puis flow_update avec ce recordingId et expectedVersion.

    diff
    -    { "action": "tap", "selectors": [{ "using": "text", "value": "Sign in" }] },
    +    { "action": "tap", "selectors": [{ "using": "text", "value": "Log in" }] },

    Le test garde son identifiant T###, son lien vers le cas de test et son historique de relectures. Si quelqu'un d'autre a enregistré le test après votre export, vous êtes prévenu avant tout écrasement : deux personnes ne peuvent pas annuler le travail l'une de l'autre sans le savoir. Chaque mise à jour est versionnée (les dix dernières sont conservées), et Version history → Revert dans l'éditeur de flux en annule une.

    Si les deux libellés vont coexister un temps, pendant un déploiement progressif par exemple, ne choisissez pas. Acceptez les deux :

    json
    { "using": "text", "value": "(Sign|Log) in", "regex": true }

    Quand une étape échoue à la relecture, le message est aussi précis que celui du validateur. Il indique que l'élément est introuvable, ou que plusieurs correspondent, et quel sélecteur a été essayé. La plupart des corrections portent sur un sélecteur, donc sur une ligne.

    Enregistrement et JSON : le même flux

    Vous n'avez pas à choisir entre enregistrer et écrire. Un flux enregistré sur un appareil réel s'exporte dans ce JSON : vous pouvez enregistrer un premier jet à la main, puis peaufiner les sélecteurs dans un éditeur.

    Une limite mérite d'être connue. Les étapes qui n'ont de sens que comme positions à l'écran, comme un balayage libre ou un toucher à une coordonnée brute, ne peuvent pas s'écrire en sélecteurs. Elles sont signalées comme omises à l'export d'un flux enregistré, et flow_update refuse de réécrire ce flux depuis le JSON plutôt que de les perdre. Vous gardez l'enregistrement, et rien ne disparaît dans votre dos.

    La même idée pour un navigateur

    Les flux web utilisent la même enveloppe avec "platform": "web" et un jeu d'actions adapté au navigateur (navigate, tap, type avec un sélecteur, hover, key, back, dialog, des assertions de page comme urlContains). Ils acceptent aussi les variables d'environnement ({{BASE_URL}}). Un même flux peut donc tourner sur la préproduction et la production.

    json
    {
      "name": "Browse skills",
      "platform": "web",
      "steps": [
        { "action": "navigate", "url": "{{BASE_URL}}" },
        { "action": "tap", "selectors": [{ "using": "role", "value": "link=Browse the skills" }] },
        { "action": "assert", "assert": { "kind": "urlContains", "expected": "/skills" } }
      ]
    }

    Où se situe RobotActions

    Le format est le nôtre, mais les habitudes qui le sous-tendent valent dans n'importe quel outil : désigner les éléments plutôt que des positions, garder les secrets hors des fichiers de test, faire de chaque différence de plateforme un bloc explicite, et échouer clairement sur une correspondance ambiguë. Sur RobotActions, un flux s'exécute sur de vrais téléphones Android et iPhone dans le cloud, seul ou au sein d'une suite, et renvoie à son cas dans TestRail, Jira ou Azure DevOps.

    Écrivez votre premier flux dans un éditeur et importez-le, ou demandez à votre agent de l'écrire, puis rejouez-le sur les deux téléphones. Essayez gratuitement · Sans carte de crédit →

    À lire aussi

    Prêt à tester sur de vrais appareils ?

    Connectez-vous avec Google ou GitHub et accédez à de vrais appareils iOS et Android depuis votre navigateur — gratuit à l'essai.

    👋 Bonjour! Besoin d'aide? Discutez avec nous!

    Discutez avec nous

    En ligne

    Avant de commencer

    Partagez vos coordonnées pour que nous puissions vous recontacter.