Retour au Centre de connaissances
Démarrage

Selenium Shadow DOM : recherches et racines détachées

Diagnostiquez les recherches Shadow DOM avec Selenium et un test Python local : limites du document, racines imbriquées ou fermées et remplacement du composant.

BotBrowser Team

Documentation

Vous voulez la documentation structurée pour Démarrage ?

Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.

Lorsque Selenium ne trouve pas un élément Shadow DOM visible dans DevTools, vérifiez le contexte de recherche avant de changer le sélecteur ou le délai. Localisez l'hôte du composant, obtenez sa shadow_root native, puis cherchez dans cette racine ; recommencez pour chaque hôte imbriqué. BotBrowser permet de déclarer les paramètres de l'environnement du navigateur pour comparer les reproductions, mais il ne franchit pas les frontières des composants pour un sélecteur et ne garantit pas qu'un composant ait terminé son rendu.

Résumé

  • Une requête du document ne traverse pas automatiquement les racines shadow. Un sélecteur CSS de descendants plus long cherche toujours dans le mauvais arbre.
  • Parcourez chaque hôte avec la commande native shadow_root de Selenium. Le changement de cadre et le parcours des racines shadow traitent des frontières différentes.
  • Une racine fermée fait renvoyer null à l'accesseur JavaScript .shadowRoot ; cela ne prouve pas que l'accès natif de WebDriver doit échouer.
  • Après le remplacement de l'hôte d'un composant, retrouvez tout le chemin des hôtes et des racines, puis vérifiez le résultat applicatif. Réutiliser une ancienne racine peut lever DetachedShadowRootException.

Sommaire

Classer la frontière en échec avant de changer le sélecteur

La documentation Selenium sur la recherche d'éléments utilise un élément hôte suivi de sa racine shadow comme contexte de recherche distinct. L'hôte est un élément ordinaire dans l'arbre qui le contient ; la racine attachée contient un autre arbre. Un bouton correspondant dans cette racine n'est pas un descendant ordinaire accessible à une recherche CSS du document. DevTools peut afficher les deux arbres ensemble sans rendre leurs contextes de recherche WebDriver interchangeables.

Conservez la commande, le sélecteur et le contexte de l'exception initiale. Une NoSuchElementException issue de driver.find_element() et la même exception issue de root.find_element() laissent ouvertes des questions différentes : la première peut utiliser le mauvais arbre, tandis que la seconde peut avoir atteint la bonne racine avant de chercher trop tôt ou avec le mauvais sélecteur enfant. Une capture seule ne distingue pas ces cas. Consignez l'hôte identifié et la réussite ou l'échec de l'obtention de sa racine native.

ObservationPremière vérificationCe qu'établit une correction réussie
La requête du document ne renvoie aucun bouton correspondantLe bouton est-il dans un arbre shadow créé par l'application ?La racine de l'hôte voulu contient la cible
host.shadow_root lève NoSuchShadowRootExceptionBon hôte, moment de l'attachement et prise en charge par le piloteUne racine est obtenue depuis cet hôte
La requête de la racine lève NoSuchElementExceptionSélecteur enfant, autre hôte imbriqué ou disponibilitéLa racine voulue contient l'enfant nécessaire
Un ancien élément lève StaleElementReferenceExceptionSon hôte ou son document a-t-il été remplacé ?Un nouveau chemin identifie le composant actuel
Une ancienne racine lève DetachedShadowRootExceptionL'hôte de cette racine a-t-il été détaché ?Une nouvelle racine appartient à l'hôte connecté actuel
La recherche réussit, mais click() échoueIdentité de la cible, état d'interaction et obstructionL'application enregistre l'action voulue

Les commandes de racines shadow de WebDriver distinguent l'obtention d'une racine de la recherche d'un élément en son sein. Un simple <div> sans racine attachée est un élément valide, mais pas un hôte shadow valide pour cette commande. À l'inverse, une racine existante peut ne contenir aucun élément .save correspondant. Ces échecs sont distincts ; les intercepter tous deux en annonçant « Shadow DOM non pris en charge » supprime cette distinction utile.

Conserver les cadres et les racines imbriquées dans le chemin

Un iframe change le document courant par driver.switch_to.frame(...). Un hôte shadow change le contexte de recherche par host.shadow_root ; il ne nécessite pas de changement de cadre. Si un composant se trouve dans un iframe, entrez d'abord dans le bon cadre, puis parcourez ses hôtes. Si un iframe se trouve dans une racine shadow, localisez cet élément cadre à travers la racine avant d'y basculer. Conservez l'ordre réel des frontières au lieu d'appliquer un changement de cadre générique à chaque élément absent.

Les composants imbriqués nécessitent une nouvelle obtention de racine native à chaque frontière. La racine extérieure peut localiser un hôte intérieur, mais un seul sélecteur CSS tel que #outer #inner #save ne traverse pas les deux racines. Préférez un chemin court et nommé : document, hôte extérieur, racine extérieure, hôte intérieur, racine intérieure, bouton, afin qu'un échec identifie la frontière manquante. Examinez le nombre de correspondances quand l'application peut afficher plusieurs copies ; le bon type de racine ne prouve pas que le composant sélectionné représente l'enregistrement voulu.

Une fois le parcours des racines réussi, l'interaction reste une question distincte. Le diagnostic Selenium de element not interactable distingue les contrôles cachés, les centres de clic interceptés et le caractère modifiable. Une recherche dans la racine qui renvoie un bouton ne prouve pas qu'il est visible, activé ou dégagé. Conservez les preuves de recherche avant ces vérifications propres à chaque commande ; sinon, un échec de clic risque d'être présenté comme un arbre shadow inaccessible.

Reproduire localement les racines du document, imbriquées et remplacées

Installez Selenium dans un environnement virtuel Python avec python -m pip install selenium, puis enregistrez le code suivant dans shadow_roots.py. Il utilise une page data: locale intégrée, pas une cible tierce. webdriver.Chrome() nécessite un navigateur installé et un pilote compatible ; la gestion des pilotes de Selenium peut en télécharger un pendant la préparation. Hors connexion, configurez explicitement un exécutable compatible connu et Service. Une erreur de démarrage avant la navigation n'apporte aucune preuve sur la recherche dans les racines shadow.

from urllib.parse import quote

from selenium import webdriver
from selenium.common.exceptions import (
    DetachedShadowRootException,
    NoSuchElementException,
    NoSuchShadowRootException,
    StaleElementReferenceException,
)
from selenium.webdriver.common.by import By

HTML = """
<!doctype html><meta charset="utf-8">
<div id="open"></div><div id="outer"></div><div id="closed"></div>
<script>
function populate(host, mode) {
  const root = host.attachShadow({mode});
  root.innerHTML = `<button id="save"
    onclick="this.getRootNode().host.dataset.saved='yes'">Save</button>`;
  return root;
}
populate(document.getElementById('open'), 'open');
const outer = document.getElementById('outer').attachShadow({mode:'open'});
outer.innerHTML = '<div id="inner"></div>';
populate(outer.getElementById('inner'), 'open');
populate(document.getElementById('closed'), 'closed');
window.replaceOpen = () => {
  window.oldHost = document.getElementById('open');
  const fresh = document.createElement('div');
  fresh.id = 'open'; fresh.dataset.version = 'new';
  populate(fresh, 'open'); window.oldHost.replaceWith(fresh);
};
</script>
"""


def rejects(exception, command):
    try:
        command()
    except exception:
        return
    raise AssertionError(f"Expected {exception.__name__}")


options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=options)
try:
    driver.get("data:text/html;charset=utf-8," + quote(HTML))
    assert driver.find_elements(By.CSS_SELECTOR, "#save") == []
    rejects(NoSuchElementException,
            lambda: driver.find_element(By.CSS_SELECTOR, "#save"))

    host = driver.find_element(By.ID, "open")
    root = host.shadow_root
    button = root.find_element(By.CSS_SELECTOR, "#save")
    button.click()
    assert host.get_attribute("data-saved") == "yes"

    outer = driver.find_element(By.ID, "outer").shadow_root
    rejects(NoSuchElementException,
            lambda: outer.find_element(By.CSS_SELECTOR, "#save"))
    inner = outer.find_element(By.CSS_SELECTOR, "#inner")
    inner.shadow_root.find_element(By.CSS_SELECTOR, "#save").click()
    assert inner.get_attribute("data-saved") == "yes"

    closed = driver.find_element(By.ID, "closed")
    assert driver.execute_script("return arguments[0].shadowRoot", closed) is None
    try:
        closed_root = closed.shadow_root
    except NoSuchShadowRootException:
        closed_native = "unavailable"
        assert closed.get_attribute("data-saved") is None
    else:
        closed_root.find_element(By.CSS_SELECTOR, "#save").click()
        assert closed.get_attribute("data-saved") == "yes"
        closed_native = "saved"

    driver.execute_script("window.replaceOpen()")
    assert driver.execute_script("return window.oldHost.isConnected") is False
    rejects(StaleElementReferenceException, button.click)
    rejects(DetachedShadowRootException,
            lambda: root.find_element(By.CSS_SELECTOR, "#save"))
    current = driver.find_element(By.ID, "open")
    current.shadow_root.find_element(By.CSS_SELECTOR, "#save").click()
    assert current.get_attribute("data-version") == "new"
    assert current.get_attribute("data-saved") == "yes"
    print(f"open=saved nested=saved closed_native={closed_native} reacquired=saved")
finally:
    driver.quit()

Exécutez python shadow_roots.py. Un pilote prenant en charge la branche des racines fermées affiche open=saved nested=saved closed_native=saved reacquired=saved. Si l'obtention native d'une racine fermée signale qu'aucune racine n'existe, le test enregistre plutôt closed_native=unavailable ; cette branche ne compte pas comme une interaction réussie avec une racine fermée. Toute autre exception inattendue reste un échec. Les valeurs id="save" délibérément répétées appartiennent à des arbres shadow différents, ce qui permet de tester la portée sans changer le sélecteur enfant.

Le gestionnaire du bouton définit data-saved sur son propre hôte, ce qui rend le résultat propre au composant. Les assertions des racines ouvertes et imbriquées échoueraient si une action visait silencieusement un autre composant. Après remplacement, data-version=new distingue le nouvel hôte de l'ancien hôte détaché. Une application en production a besoin de sa propre identité d'opération ou d'enregistrement ; copier le marqueur du cas de test sans contrat applicatif ne prouverait pas un véritable enregistrement.

La recherche du document manque un enfant shadow ; host.shadow_root change le contexte, les hôtes imbriqués nécessitent une autre racine et un hôte remplacé impose de retrouver le chemin.

Ce que les cas locaux établissent

L'exécution préparatoire de cinq cas du 2026-11-11 utilisait Selenium 4.52.0, Chrome standard 157.0.8103.79 et ChromeDriver 157.0.8103.50 sur macOS arm64. La recherche du document n'a pas trouvé un enfant d'une racine ouverte ; le parcours natif a permis de cliquer dessus. Un autre cas a traversé deux racines imbriquées. L'accesseur JavaScript de la racine fermée a renvoyé None, tandis que l'accès natif a trouvé son bouton et permis de cliquer dessus. Un hôte ordinaire a levé NoSuchShadowRootException, tandis qu'un enfant absent dans une racine existante a provoqué NoSuchElementException.

Le remplacement d'un hôte a rendu son bouton mémorisé périmé et sa racine mémorisée détachée ; retrouver l'hôte et la racine a permis d'atteindre le nouveau composant et d'enregistrer. Ce sont des observations synthétiques sur le navigateur standard, pas une exécution des capacités de BotBrowser ni une preuve sur l'implémentation des composants d'un site externe. Conservez les versions du navigateur, du pilote et des liaisons Selenium avec chaque reproduction, surtout quand la différence concerne la prise en charge des racines fermées plutôt que la syntaxe du sélecteur.

Séparer les accesseurs de racines fermées de l'accès natif WebDriver

L'accesseur shadowRoot de la spécification DOM renvoie null lorsqu'une racine est absente ou que son mode est fermé. Par conséquent, execute_script("return arguments[0].shadowRoot", host) ne distingue pas ces deux cas à lui seul. Un résultat JavaScript vide est une observation sur cet accesseur, pas la preuve que l'hôte n'a aucune racine interne. DevTools peut montrer la racine tandis qu'un script renvoie null, et les deux résultats peuvent être corrects.

L'algorithme natif Get Element Shadow Root de WebDriver travaille avec la racine shadow interne de l'élément, au lieu d'imposer cet accesseur JavaScript. Le changement de prise en charge des racines shadow fermées de ChromeDriver, intégré le 2024-10-14, a explicitement ajouté cette prise en charge. Les implémentations antérieures peuvent se comporter différemment. Consignez le résultat réel de la commande et les versions d'implémentation ; ni « fermé fonctionne toujours » ni « fermé est toujours inaccessible à Selenium » ne constitue une règle valable pour tous les pilotes.

Séparer les preuves de la commande et de l'implémentation

Sur une machine en échec, exécutez le même cas de test contrôlé avec .shadow_root native et conservez la classe d'exception. Si elle diffère de l'environnement consigné, comparez les versions du navigateur, du pilote et des liaisons Selenium avant de réécrire la page. Un hôte périmé, incorrect ou une course lors de l'attachement peuvent aussi faire échouer l'obtention de la racine ; le mot fermé ne remplace pas l'identification du nœud sélectionné. Le test crée chaque racine de façon synchrone pour ne pas confondre prise en charge et chronologie applicative.

Le résultat consigné pour la racine fermée concerne un composant créé par l'application avec attachShadow({mode:'closed'}). Il ne prouve pas que chaque contrôle interne du navigateur expose son arbre shadow d'agent utilisateur à la même commande d'automatisation. La prise en charge native d'un cas de test ne dit rien non plus du modèle d'autorisation de l'application ni de l'opportunité d'exercer une opération. Limitez le diagnostic aux composants que vous contrôlez ou dont le test est autorisé.

Utiliser CSS dans une racine connue

L'API ShadowRoot de Selenium pour Python fournit des méthodes de recherche d'éléments. Le bouton renvoyé est un WebElement doté de click() ; la racine elle-même n'est pas un élément cliquable. CSS est la stratégie exercée par ce test. Ne supposez pas qu'une expression XPath puisse sortir d'un arbre shadow ou traverser le suivant simplement parce que XPath fonctionne dans le contexte du document. Les erreurs de stratégie non prise en charge et de correspondance absente doivent garder leurs catégories de protocole réelles.

Évitez de remplacer la commande native par un long parcours JavaScript sans consigner ce changement. Une chaîne de scripts utilisant .shadowRoot hérite du comportement de l'accesseur pour les racines fermées et peut échouer à une autre frontière. Un script invoquant directement le gestionnaire du bouton transforme aussi un test d'interaction en activation par script. Une recherche native suivie d'un clic WebDriver normal préserve la distinction entre atteindre le nœud et interagir avec lui.

Retrouver un composant remplacé sans répéter son action

Une référence de racine appartient à une instance précise d'hôte, pas à tous les futurs éléments ayant le même ID. Lorsqu'une application remplace <settings-panel> par un nouvel élément, la chaîne du sélecteur peut rester valide alors que les anciennes références d'hôte et de racine deviennent inutilisables. La définition des racines shadow de WebDriver inclut des vérifications de détachement. Rechercher uniquement le bouton dans une ancienne racine conserve la partie défectueuse du chemin.

Le cas de test conserve délibérément window.oldHost après le remplacement. Cette référence JavaScript maintient l'ancien objet accessible, mais ne le garde pas connecté au document ; isConnected vaut false. C'est un contre-exemple utile à « l'objet existe encore, donc Selenium devrait l'utiliser ». L'ancien bouton provoque une erreur d'élément périmé et une recherche dans l'ancienne racine provoque une erreur de racine détachée avec le pilote consigné. Les deux demandent l'identité actuelle du composant, pas un autre sélecteur de descendants sur l'ancien objet.

Attendre l'obtention, puis exécuter l'action une seule fois

Pour une application qui attache sa racine de manière asynchrone, une attente explicite peut retrouver l'hôte, la racine et l'enfant à chaque interrogation. La fonction auxiliaire suivante suppose que #open identifie un seul composant voulu et que son enfant shadow porte l'ID save. Elle renvoie une cible visible et activée ; elle ne promet pas un point de clic dégagé. Ses nouvelles tentatives concernent uniquement la recherche, de sorte que les interrogations ne puissent pas soumettre plusieurs fois la même opération.

from selenium.webdriver.support.ui import WebDriverWait


def current_save(driver):
    try:
        host = driver.find_element(By.ID, "open")
        root = host.shadow_root
        button = root.find_element(By.CSS_SELECTOR, "#save")
        if button.is_displayed() and button.is_enabled():
            return button
        return False
    except (NoSuchElementException, NoSuchShadowRootException,
            StaleElementReferenceException, DetachedShadowRootException):
        return False


save = WebDriverWait(driver, 5).until(current_save)
save.click()

Une course reste possible entre l'élément renvoyé et le clic qui suit. Si l'application peut remplacer le composant pendant cet intervalle, utilisez une condition d'état stable définie par l'application ou signalez la nouvelle erreur d'élément périmé plutôt que de rejouer silencieusement un achat ou un enregistrement entier. Le diagnostic des attentes et tests stables avec Selenium développe les contrats de disponibilité et de résultat ; ici, l'exigence supplémentaire est de retrouver chaque frontière shadow appartenant au composant actuel.

Après l'action, attendez séparément son véritable résultat. Une réponse réseau, un enregistrement sauvegardé ou une confirmation du composant doivent se rapporter à l'opération qui vient d'être exécutée. Un data-saved=yes permanent laissé par une interaction précédente ne suffit pas pour des tests d'enregistrements répétés. Lorsque les soumissions en double comptent, conservez une identité d'opération et examinez le résultat avant de décider si un autre clic est sûr. Une boucle générique d'interception et de nouvelle tentative ne peut pas prendre cette décision métier.

Diagnostiquer les délais persistants avec la frontière en échec

La fonction auxiliaire transforme délibérément les échecs attendus d'obtention en une nouvelle interrogation ; un dépassement final du délai doit donc conserver ailleurs le contexte de diagnostic. Consignez si la dernière tentative a échoué à la recherche de l'hôte, à l'obtention de la racine ou à la recherche de l'enfant, puis capturez une image et des attributs sûrs du composant. Ne journalisez pas tout le balisage shadow contenant des identifiants ou des données client. Exécuter temporairement une seule obtention sans interception peut aussi exposer l'exception de protocole sous-jacente dans un cas de test contrôlé.

Si aucune racine ne s'attache, examinez le signal d'initialisation de l'application et les échecs antérieurs de script ou de réseau. Si elle s'attache mais que la cible reste absente, examinez l'état voulu et le sélecteur dans cette racine. Si la cible apparaît et que l'interaction échoue, passez au diagnostic de visibilité et d'obstruction. Ces branches rendent un dépassement de délai exploitable ; augmenter simplement WebDriverWait(driver, 5) masque l'état qui devait changer.

Comparer l'environnement après le chemin du composant

Les versions du navigateur et les environnements de rendu peuvent changer le comportement ou la disposition d'un composant ; conservez-les une fois les identités du document et de la racine établies. BotBrowser permet de déclarer des paramètres d'écran et de polices via sa référence de configuration des profils, mais il ne corrige pas un sélecteur de racine et ne garantit pas la prise en charge native des racines fermées pour toutes les combinaisons de pilotes. La documentation d'intégration couvre le lancement et la connexion ; ce parcours de diagnostic traite les contextes de recherche après que la connexion fonctionne déjà.

Le cas local ne charge aucun script distant, n'authentifie aucun compte et n'exerce aucune hydratation de framework. Il isole le parcours et la durée de vie des références. La mise à niveau asynchrone des éléments personnalisés, la navigation ou la duplication adaptative d'une application nécessitent une reproduction contrôlée supplémentaire avec les marqueurs d'état correspondants. Gardez ce test ajouté séparé des assertions synchrones du cas initial, afin qu'une affirmation de prise en charge du pilote ne devienne pas accidentellement une promesse de disponibilité de l'application.

Questions fréquentes

Pourquoi un bouton visible ne renvoie-t-il aucune correspondance ?

La visibilité dans DevTools n'identifie pas l'arbre de recherche courant de Selenium. Confirmez le cadre, puis obtenez la racine de l'hôte et cherchez dedans. Si la racine extérieure contient un autre hôte, obtenez cette racine intérieure avant de chercher le bouton.

Java ou C# évitent-ils la frontière rencontrée en Python ?

Changer de liaisons modifie la syntaxe, pas les contextes de recherche du document et des racines shadow. Conservez le même chemin d'hôtes et de racines ainsi que la véritable commande WebDriver quand vous comparez les langages, et consignez les versions des liaisons si leurs API exposées diffèrent.

Une racine fermée constitue-t-elle un contrat de testabilité ?

Le mode fermé limite l'accesseur JavaScript de la page ; il n'exprime pas une règle universelle sur la prise en charge de l'automatisation. Pour un composant que vous contrôlez, préférez un contrat de test explicite et pris en charge, puis vérifiez le pilote réel plutôt que de dépendre d'un contournement d'accesseur non consigné.

Sources

Conclusion pratique

Conservez le contexte en échec, parcourez explicitement chaque racine native et retrouvez tout le chemin après le remplacement de l'hôte. Exécutez le cas local avant d'attribuer un échec au mode fermé ou à l'environnement du navigateur. Une recherche réussie établit une référence de cible ; le résultat de l'opération actuelle du composant établit si l'action voulue a fonctionné.

#Selenium#Shadow DOM#WebDriver#Débogage De Tests

Cet article fait partie du guide : Débuter avec l'automatisation du navigateur : carte initiale

Exécutez la même vérification dans BotBrowser

Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.