đź§Ş Tests

testing-selenium-guide

Automatisation de tests avec Selenium WebDriver, couvrant les locators, les waits, le Page Object Model et Selenium Grid.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- testing-selenium-guide --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } testing-selenium-guide -Launch"

🚀 Déjà installé ?

claude "/testing-selenium-guide"

Ou tapez /testing-selenium-guide dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

SeleniumWebDrivertest automatiséPage Object Model

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/testing-selenium-guide ~/.claude/skills/

Payload du plugin : skills/testing-selenium-guide · source éditable : testing-skills/selenium-guide

đź“– Manuel

Selenium WebDriver Guide

1. Configurer l'environnement

Python (recommandé pour démarrer rapidement)

pip install selenium webdriver-manager pytest pytest-html
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")   # headless pour CI
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")

driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=options
)
driver.implicitly_wait(0)  # toujours 0 avec explicit waits

Java (Maven)

<dependency>
  <groupId>org.seleniumhq.selenium</groupId>
  <artifactId>selenium-java</artifactId>
  <version>4.21.0</version>
</dependency>
<dependency>
  <groupId>io.github.bonigarcia</groupId>
  <artifactId>webdrivermanager</artifactId>
  <version>5.9.1</version>
</dependency>

2. Choisir les locators — ordre de priorité

PrioritéStratégieExempleUsage
1data-testid / data-cy[data-testid="submit-btn"]Stable, découplé du DOM
2id#usernameRapide si unique
3name[name="email"]Formulaires
4CSS selector.card > button.primaryPerformance
5XPath//div[@role='dialog']//buttonEn dernier recours
from selenium.webdriver.common.by import By

# Bon
el = driver.find_element(By.CSS_SELECTOR, "[data-testid='login-btn']")

# Acceptable
el = driver.find_element(By.XPATH, "//button[normalize-space()='Connexion']")

# Éviter
el = driver.find_element(By.XPATH, "/html/body/div[3]/div[1]/button")

3. Gérer les waits correctement

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, timeout=10)

# Attendre qu'un élément soit cliquable
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='submit']")))
btn.click()

# Attendre la disparition d'un spinner
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))

# Attendre un texte précis
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "Succès"))

# Custom condition (requête AJAX terminée)
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")

Règle absolue : driver.implicitly_wait(0) + explicit waits exclusivement. Ne jamais mélanger les deux.


4. Page Object Model (POM)

Structure de projet recommandée :

tests/
  pages/
    base_page.py
    login_page.py
    dashboard_page.py
  tests/
    test_login.py
  conftest.py
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def click(self, locator):
        self.wait.until(EC.element_to_be_clickable(locator)).click()

    def type(self, locator, text):
        el = self.wait.until(EC.visibility_of_element_located(locator))
        el.clear()
        el.send_keys(text)

# pages/login_page.py
from selenium.webdriver.common.by import By
from .base_page import BasePage

class LoginPage(BasePage):
    URL = "https://app.example.com/login"
    _USERNAME = (By.ID, "username")
    _PASSWORD = (By.ID, "password")
    _SUBMIT  = (By.CSS_SELECTOR, "[data-testid='login-btn']")
    _ERROR   = (By.CSS_SELECTOR, ".alert-error")

    def open(self):
        self.driver.get(self.URL)
        return self

    def login(self, user, pwd):
        self.type(self._USERNAME, user)
        self.type(self._PASSWORD, pwd)
        self.click(self._SUBMIT)
        return self

    def error_message(self):
        return self.wait.until(EC.visibility_of_element_located(self._ERROR)).text

# tests/test_login.py
def test_login_valide(driver):
    page = LoginPage(driver).open()
    page.login("admin@example.com", "secret")
    assert "dashboard" in driver.current_url

def test_login_invalide(driver):
    page = LoginPage(driver).open()
    page.login("bad@user.com", "wrong")
    assert "Identifiants incorrects" in page.error_message()
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    drv = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
    drv.set_window_size(1920, 1080)
    yield drv
    drv.quit()

5. Interactions complexes

from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

actions = ActionChains(driver)

# Hover + click sur sous-menu
actions.move_to_element(menu).pause(0.3).click(submenu).perform()

# Drag and drop
actions.drag_and_drop(source, target).perform()

# Upload de fichier (input file)
driver.find_element(By.CSS_SELECTOR, "input[type='file']").send_keys("/abs/path/file.pdf")

# Iframe
driver.switch_to.frame(driver.find_element(By.ID, "payment-iframe"))
# ... interactions dans l'iframe ...
driver.switch_to.default_content()

# Nouvelle fenĂŞtre/onglet
original = driver.current_window_handle
driver.switch_to.window([h for h in driver.window_handles if h != original][0])
# ... actions dans le nouvel onglet ...
driver.close()
driver.switch_to.window(original)

# Alert
alert = wait.until(EC.alert_is_present())
alert.accept()    # ou alert.dismiss() / alert.send_keys(...)

6. Selenium Grid — exécution distribuée

Démarrage rapide avec Docker Compose :

# docker-compose.yml
services:
  selenium-hub:
    image: selenium/hub:4.21
    ports: ["4442:4442", "4443:4443", "4444:4444"]

  chrome:
    image: selenium/node-chrome:4.21
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
      SE_NODE_MAX_SESSIONS: "4"
    volumes:
      - /dev/shm:/dev/shm
    deploy:
      replicas: 3

  firefox:
    image: selenium/node-firefox:4.21
    depends_on: [selenium-hub]
    environment:
      SE_EVENT_BUS_HOST: selenium-hub
docker compose up -d
# Dashboard : http://localhost:4444/ui
# Connexion au Grid depuis les tests
from selenium.webdriver.remote.webdriver import WebDriver as RemoteDriver

options = webdriver.ChromeOptions()
driver = RemoteDriver(
    command_executor="http://localhost:4444/wd/hub",
    options=options
)

pytest-xdist pour la parallélisation :

pip install pytest-xdist
pytest -n 4 tests/          # 4 workers en parallèle

7. Intégration CI/CD

# .github/workflows/selenium.yml (GitHub Actions)
jobs:
  selenium:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - run: pytest tests/ --html=report.html --self-contained-html -v
        env:
          HEADLESS: "true"
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: selenium-report
          path: |
            report.html
            screenshots/

Screenshot automatique à l'échec :

# conftest.py
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    if rep.when == "call" and rep.failed:
        driver = item.funcargs.get("driver")
        if driver:
            driver.save_screenshot(f"screenshots/{item.name}.png")

8. Garde-fous et anti-patterns

Anti-patternImpactCorrection
time.sleep(3)Tests lents et fragilesWebDriverWait + ExpectedConditions
implicitly_wait + WebDriverWait simultanésTimeouts imprévisiblesimplicitly_wait(0) partout
Locators sur le texte brut affichéCasse à la moindre traductiondata-testid ou attributs sémantiques
driver.find_elements(...)[0] sans vérificationIndexError silencieuxVérifier .is_displayed() + longueur
Tests avec état partagé (session/cookie)Flakiness entre testsReset complet dans le teardown ou driver.delete_all_cookies()
Assertions dans les Page ObjectsMélange responsabilitésPO = navigation ; test = assertion
XPath absolu (/html/body/div[2]/...)Fragile au moindre refactor HTMLCSS selector ou XPath relatif

Pièges fréquents :


Critères de décision — Selenium vs alternatives

BesoinOutil conseillé
Tests multi-navigateurs (Chrome, Firefox, Safari, Edge)Selenium
Tests uniquement Chromium/Firefox, setup rapidePlaywright
Tests composants React/Vue isolésCypress Component Testing
Mobile natif (Android/iOS)Appium (basé sur WebDriver)
Performance / chargek6, Gatling