Accepter les dialogues d'autorisation au démarrage — puis désactiver en cours de test
Toutes les suites mobiles rencontrent ce problème dès le premier jour. L'application démarre, iOS demande les notifications, puis la localisation, puis le suivi publicitaire, et chaque test qui ne porte pas sur les autorisations meurt sur un dialogue qu'il n'a jamais demandé.
Vous positionnez donc appium:autoAcceptAlerts et la suite passe au vert.
Puis quelqu'un écrit le seul test qui porte justement sur les autorisations — celui qui vérifie que la demande de localisation apparaît, appuie sur Refuser et contrôle l'état vide — et découvre que la capacité qui avait sauvé la suite est désormais ce qui le bloque. L'alerte a disparu avant que le test ait pu la voir.
Voici le tableau exact de ce qui relève de la session, de ce qui se modifie à l'exécution, et de ce qu'il faut employer sur chaque plateforme.
Niveau 1 : les capacités de session
Elles se définissent à la création de la session. Ce sont celles vers lesquelles tout le monde se tourne en premier — et, comme le montre la section suivante, ce n'est pas le niveau que vous voulez réellement piloter.
iOS (XCUITest). appium:autoAcceptAlerts
— « Accepter automatiquement toutes les alertes iOS si elles apparaissent. Cela inclut les
alertes d'autorisation d'accès à la vie privée (localisation, contacts, photos). false par
défaut. » appium:autoDismissAlerts en est l'image inversée. Notez ce que recouvre la
formulation : les alertes système, y compris les demandes d'autorisation, et pas seulement les
UIAlertController de votre propre application.
Android (UiAutomator2). Il n'existe pas d'équivalent d'autoAcceptAlerts, parce que les
autorisations d'exécution Android ne sont pas vraiment des alertes que l'on accepte — ce sont des
droits que l'on peut accorder avant même que le dialogue n'apparaisse. C'est
appium:autoGrantPermissions :
« Accorder ou non automatiquement toutes les autorisations demandées par l'application au
démarrage d'un test. »
Trois contraintes qui mordent en pratique :
- Le
targetSdkVersiondu manifeste doit être ≥ 23 et l'appareil doit être sous Android 6 ou plus. - Les applications dont le
targetSdkVersionest ≤ 22 « doivent être réinstallées pour que les autorisations soient accordées » — par exemple avecappium:fullReset. - Les droits sont accordés au moment de l'installation. Si votre session réutilise une application déjà installée, la capacité n'a rien sur quoi agir. C'est de loin la raison la plus fréquente pour laquelle on rapporte qu'« autoGrantPermissions ne fonctionne pas ».
Et pour les autorisations spéciales — accès aux notifications, capture audio ou vidéo — le
mécanisme d'octroi ordinaire ne suffit pas ; la documentation renvoie vers
mobile: changePermissions avec la cible appops. Nous y venons plus bas.
Une réserve iOS à intégrer une bonne fois : la capacité appium:permissions, celle qui permet
de prérégler l'état des autorisations par service, « permet de définir les autorisations pour le
bundle applicatif indiqué sur Simulateur uniquement ». Sur un iPhone réel, elle n'est pas
disponible. L'état des autorisations sur appareil iOS réel se gère par les dialogues, ou par
l'état dans lequel l'appareil a été laissé.
Les deux réglages qui pilotent réellement le comportement
Les capacités ne sont pas le mécanisme. Elles sont une commodité qui en initialise un, et le mécanisme sous-jacent est modifiable à l'exécution — ce qui veut dire que la bascule en cours de session que tout le monde croit impossible tient en deux lignes.
Quand le pilote construit sa session d'automatisation iOS sous-jacente, il fait exactement ceci :
if (driver.opts.autoAcceptAlerts) {
wdaCaps.defaultAlertAction = 'accept';
} else if (driver.opts.autoDismissAlerts) {
wdaCaps.defaultAlertAction = 'dismiss';
}appium:autoAcceptAlerts est donc une façade pour un réglage nommé defaultAlertAction. Et
defaultAlertAction est enregistré dans la table des réglages, ce qui permet de l'écrire à tout
moment pendant la session. Le pilote transmet toute clé de réglage qu'il ne traite pas
spécialement directement au point d'entrée « settings » de l'agent : un simple appel à
setSettings l'atteint donc.
// Turn auto-accept off, mid-session
getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", ""));
// ... assert on the dialog, tap what you want ...
// Turn it back on
getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", "accept"));Vous verrez "off" employé comme valeur de désactivation, et cela fonctionne — mais par accident
plutôt que par conception. Le traitement est une simple comparaison à accept et dismiss ;
toute autre valeur retombe dans une branche qui journalise 'off' default alert action is unsupported et ne fait rien. Le comportement voulu, avec une ligne d'avertissement par alerte.
La chaîne vide est la valeur explicitement testée, qui sort immédiatement sans bruit dans les
journaux. (null fonctionne aussi, mais ImmutableMap refuse les valeurs nulles, ce qui explique
précisément pourquoi "off" est ce vers quoi on se tourne en Java.)
Cela mérite d'être su justement parce que rien de tout cela ne figure dans la documentation des
réglages du pilote : defaultAlertAction n'y est pas listé, et la
réponse du forum Appium sur cette question
est « retirez autoAccept et gérez tout vous-même ». Cette réponse n'est plus à jour. Le réglage
existe, il se relit, et c'est la forme correcte de la chose.
autoClickAlertSelector, et pourquoi il est consulté en premier
Le second réglage est
autoClickAlertSelector :
Sélecteur personnalisé pour un bouton d'alerte. Permet de localiser automatiquement un élément dans la hiérarchie d'une alerte et d'appuyer dessus dès qu'une nouvelle alerte est détectée.
Ce réglage est prioritaire sur les capacités
appium:autoAcceptAlertsetappium:autoDismissAlerts.
« Prioritaire » est à prendre au pied de la lettre : quand une alerte apparaît, le sélecteur est
consulté en premier, et s'il est défini le gestionnaire appuie puis sort sans jamais consulter
defaultAlertAction — y compris lorsque l'appui échoue. Il est strictement plus expressif que le
booléen, puisqu'il désigne quel bouton recevra l'appui, au lieu d'accepter ce que l'OS considère
comme le choix par défaut :
await driver.updateSettings({
autoClickAlertSelector: "**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]",
});Pour le désactiver, affectez-lui une chaîne vide. Ce n'est pas une astuce : une valeur vide fait l'objet d'un traitement particulier en amont de toute analyse, et elle démonte en outre le moniteur d'alertes au lieu de simplement ne rien apparier :
getAppiumDriver().setSettings(ImmutableMap.of("autoClickAlertSelector", ""));Ne le désactivez pas en fournissant un sélecteur valide qui n'apparie rien. Cela laisse le moniteur en fonctionnement et fait payer une requête de chaîne de classes sur chaque alerte, sans bénéfice. Notez aussi que le « une erreur est levée si le sélecteur fourni est invalide » de la documentation ne concerne que les valeurs non vides — l'étape d'analyse n'est jamais atteinte pour une chaîne vide.
Les deux ensemble
Parce que le sélecteur court-circuite l'action d'alerte, désactiver complètement le traitement automatique suppose d'effacer les deux, dans cet ordre :
private void pauseAlertHandling() {
getAppiumDriver().setSettings(ImmutableMap.of("autoClickAlertSelector", ""));
getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", ""));
}
private void resumeAlertHandling() {
getAppiumDriver().setSettings(ImmutableMap.of("defaultAlertAction", "accept"));
}Effacer uniquement defaultAlertAction alors qu'un sélecteur est encore défini ne change
strictement rien, puisque c'est le sélecteur qui traite vos alertes. C'est le mode de défaillance
à surveiller si vous avez configuré les deux : la désactivation semble sans effet, et on croit que
le réglage a été ignoré.
Branchez cette paire sur @BeforeEach / @AfterEach et les tests dédiés aux autorisations vivent
dans la même session et la même suite que tout le reste, sans second jeu de capacités.
Vous pouvez aussi démarrer la session avec le réglage plutôt qu'avec la capacité booléenne, grâce
au préfixe appium:settings[...] d'Appium :
{
"platformName": "iOS",
"appium:automationName": "XCUITest",
"appium:settings[autoClickAlertSelector]": "**/XCUIElementTypeButton[`label CONTAINS[c] 'allow'`]"
}Même effet qu'autoAcceptAlerts, mais le comportement réside désormais quelque part que vous
pouvez modifier sans démonter la session.
Niveau 2, Android : révoquer et réaccorder à l'exécution
Le levier d'exécution d'Android ne concerne pas du tout les alertes — il porte sur l'octroi
lui-même. Le mobile: changePermissions du pilote UiAutomator2 « modifie les autorisations d'un
paquet à l'exécution » :
| Argument | Valeurs | Remarques |
|---|---|---|
permissions | nom d'autorisation, tableau, ou all | all ne fonctionne qu'avec target: pm |
appPackage | nom de paquet | par défaut, l'application testée |
action | grant (défaut) / revoke pour pm ; allow / deny / ignore / default pour appops | |
target | pm (défaut) ou appops | appops exige l'option de sécurité serveur adb_shell du pilote |
Toute la suite démarre donc avec les autorisations accordées, et le seul test qui a besoin du dialogue commence par les révoquer et laisse l'application demander :
// Start clean: revoke, so the next launch actually prompts
driver.executeScript("mobile: changePermissions", Map.of(
"permissions", "android.permission.ACCESS_FINE_LOCATION",
"action", "revoke"));
driver.executeScript("mobile: activateApp", Map.of("appId", "com.mycompany.myapp"));
// ... assert the system dialog, tap Deny, assert your empty state ...
// Hand it back for the rest of the suite
driver.executeScript("mobile: changePermissions", Map.of(
"permissions", "all", "action", "grant"));Et pour vérifier l'état plutôt que de le supposer, mobile: getPermissions accepte un type
valant denied, granted ou requested (la valeur par défaut) et renvoie une liste de noms
d'autorisations. « Est-ce que l'octroi a bien été appliqué ? » cesse d'être une séance de débogage
pour devenir une assertion.
Notez le motif « révoquer puis relancer ». Révoquer une autorisation qu'un processus en cours détient déjà n'est pas quelque chose que l'application encaisse élégamment — Android peut tuer le processus. Faites-le avant d'activer l'application, pas au milieu d'un parcours, sauf si tuer l'application est précisément ce que vous testez.
Niveau 3 : traiter une alerte à la main
Quand vous devez simplement gérer le dialogue qui est devant vous :
iOS — mobile: alert prend une action valant accept, dismiss ou getButtons, plus un
buttonLabel facultatif. getButtons est sous-employé et vraiment pratique : vérifiez le jeu
exact de boutons proposé par l'OS avant de vous engager à appuyer sur l'un d'eux, ce qui permet
d'attraper les différences de formulation entre versions d'iOS au lieu d'échouer sur un libellé
codé en dur.
const buttons = await driver.execute("mobile: alert", { action: "getButtons" });
// e.g. ["Don't Allow", "Allow Once", "Allow While Using App"]
await driver.execute("mobile: alert", { action: "accept", buttonLabel: "Allow Once" });Android — mobile: acceptAlert et mobile: dismissAlert, tous deux avec un buttonLabel
facultatif. La documentation est d'une honnêteté rafraîchissante sur leur fiabilité : « Cette
méthode peut ne pas toujours être fiable, car il n'existe pas de standard unique décrivant à quoi
ressemblent les alertes Android dans la représentation d'accessibilité. » Passez un buttonLabel
explicite quand vous le connaissez, et préférez changePermissions à l'appui sur un dialogue pour
tout ce qui touche aux autorisations.
Deux autres réglages iOS bons à connaître pendant que vous y êtes :
acceptAlertButtonSelector/dismissAlertButtonSelector— des chaînes de classes qui changent le bouton sur lequel appuient les commandes W3C standard Accept Alert / Dismiss Alert. Pour « gérer des boutons d'acceptation au texte arbitraire », c'est-à-dire les dialogues internes à l'application dont les boutons disent J'ai compris et Plus tard.respectSystemAlerts— « Basculer ou non automatiquement l'application active vers le springboard système si un élément d'alerte native est détecté. »falsepar défaut. Si votre traitement d'alertes fonctionne partout sauf quand une demande système se superpose à votre application, activez-le avant de commencer à soupçonner le pilote.
Une politique qui tient dans le temps
| Situation | À employer |
|---|---|
| Les demandes d'autorisation sont du bruit dans 95 % des tests | iOS : appium:settings[autoClickAlertSelector]. Android : appium:autoGrantPermissions |
| Un test doit vérifier la demande elle-même | iOS : mettre autoClickAlertSelector et defaultAlertAction à "", puis restaurer. Android : mobile: changePermissions → revoke, puis relancer |
| Autorisations spéciales (notifications, capture) | Android : mobile: changePermissions avec target: appops |
| La formulation varie selon la version d'OS | mobile: alert → getButtons, vérifier, puis agir |
| Dialogue interne, boutons non standard | acceptAlertButtonSelector / dismissAlertButtonSelector |
| Simulateur iOS, état préréglé avant lancement | appium:permissions (Simulateur uniquement) |
Le fil conducteur : préférez le réglage à la capacité, même quand les deux conviendraient aujourd'hui. La capacité est une décision que l'on prend une fois par session et avec laquelle on vit ensuite ; le réglage est une décision que l'on peut reprendre test par test. Une suite mobile finit toujours par contenir le test qui a besoin de l'inverse du comportement par défaut, et découvrir cela après avoir construit 400 tests autour d'une capacité est un moment coûteux pour apprendre que la couche en dessous était modifiable depuis le début.
Ce qui change sur un appareil réel
Deux choses, et les deux jouent en votre faveur une fois que vous les connaissez.
Le fait qu'appium:permissions soit réservé au Simulateur signifie que l'état des autorisations
sur un iOS réel vient de l'appareil lui-même — de ce que la session précédente y a laissé. Sur un
parc partagé, c'est une source d'échecs mystérieux : vous ne savez pas de quel état vous avez
hérité. Sur un appareil dédié, cet état est le vôtre et il est stable d'une exécution à
l'autre : « accordées au début de la suite » devient donc quelque chose que vous établissez une
fois et sur quoi vous pouvez compter, au lieu de le reconstruire à chaque session.
Le revers, c'est qu'un état stable reste un état. Rendez la condition initiale explicite à la préparation de la suite — en accordant ou en révoquant délibérément — plutôt que de laisser l'exécution N hériter par accident de l'exécution N−1.
Et quand une demande passe malgré tout entre les mailles en CI, ce qui met fin au débat le plus vite, c'est de la voir : une vidéo de l'exécution et l'arbre des éléments au moment de l'échec vous disent en quelques secondes si le dialogue est apparu sans être traité, ou s'il n'est jamais apparu. Ces deux échecs produisent des traces d'exécution identiques et appellent des correctifs totalement différents.
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.