Guide de l’architecture de test UI automatisé pour plateforme de dialogue IA basée sur Playwright

Ce projet constitue un framework de test d’interface utilisateur automatisé pour une plateforme de questions-réponses intelligentes. Il repose sur Python, Pytest et Playwright pour valider automatiquement les flux métier principaux, couvrant des modules comme la connexion utilisateur, le dialogue IA, le téléchargement de fichiers, la gestion des sessions, les opérations sur les messages et la gestion des utilisateurs en backend. Il intègre également des logs, des captures d’écran et des rapports de test Allure afin d’améliorer la traçabilité des résultats et l’efficacité de la localisation des problèmes. Une tâche d’intégration continue est mise en place via Jenkins.

Stack technique : Python + pytest + playwright + allure + json + Jenkins

2. Structure du projet

ChatAI-UI-Automation-Testing-Framework/              # Racine du projet
├── auth/                                            # Cache d’état de connexion
│   └── auth_state.json                              # Fichier d’état de connexion
│
├── common/                                          # Modules utilitaires
│   ├── get_json_data.py                             # Outil de lecture des données de test JSON
│   ├── log_utils.py                                 # Gestion des logs avec rotation
│   ├── settings.py                                  # Gestion des configurations globales
│   └── userchat_info_write.py                       # Écriture des résultats de l’IA dans un fichier local
│
├── data/                                            # Répertoire des données de test
│   ├── __init__.py
│   ├── ai_dialogue_data.json                        # Données de test pour le dialogue IA
│   ├── login_fail_data.json                         # Données de test pour échec de connexion
│   ├── login_success_data.json                      # Données de test pour connexion réussie
│   └── chatfile/                                    # Ressources pour test de téléchargement
│       ├── bonjour.jpg
│       ├── renard.jpg
│       ├── renard.png
│       └── re_question                              # Contenu pour relance/complément
│
├── logs/                                            # Répertoire des logs
│   └── ui_test.xxxxxxxx.log
│
├── pages/                                           # Couche des objets de page (POM)
│   ├── add_users_page.py                            # Page d’ajout d’utilisateur
│   ├── ai_dialogue_page.py                          # Page de dialogue IA
│   ├── ai_question_answering_process_page.py        # Page du processus de questions-réponses IA
│   ├── exit_login.py                                # Page de déconnexion
│   ├── login_page.py                                # Page de connexion
│   ├── session_manage_page.py                       # Page de gestion des sessions
│   └── upload_file_operation_page.py                # Page de téléchargement de fichiers
│
├── testcases/                                       # Couche des cas de test
│   ├── addusers/
│   │   ├── __init__.py
│   │   └── test_add_users.py                        # Test d’ajout d’utilisateur
│   │
│   ├── aichat/
│   │   ├── test_ai_chat_process.py                  # Test du flux principal de questions-réponses IA
│   │   ├── test_ai_dialogue.py                      # Test des fonctionnalités de dialogue IA
│   │   ├── test_ai_message_copy.py                  # Test de copie de message
│   │   ├── test_ai_message_regenerate.py            # Test de régénération de message
│   │   └── test_ai_message_stop.py                  # Test d’arrêt de réponse
│   │
│   ├── exit_login/
│   │   └── test_exit_login.py                       # Test de déconnexion
│   │
│   ├── login/
│   │   ├── test_login.py                            # Test de base de la connexion
│   │   ├── test_login_ajax.py                       # Test des requêtes et réponses Ajax
│   │   ├── test_login_params.py                     # Test de connexion paramétré
│   │   └── test_mock_login.py                       # Test de connexion avec simulation d’erreur
│   │
│   ├── session_manage/
│   │   └── test_session_manage.py                   # Test de renommage/suppression de session
│   │
│   └── upload_file_operation/
│       └── test_upload_file_operation.py            # Test de téléchargement de fichier
│
├── test_results/                                    # Répertoire des résultats de test
│   ├── add_users/
│   ├── aichat_process_record/
│   ├── ai_dialogue_record/
│   ├── exit_login/
│   ├── fail_screenshots/
│   ├── login/
│   ├── session_manage/
│   └── upload_file/
│
├── allure-results/                                  # Répertoire des résultats Allure bruts
├── conftest.py                                      # Configuration globale des fixtures
├── pytest.ini                                       # Fichier de configuration Pytest
├── requirements.txt                                 # Dépendances du projet
└── .gitignore

3. Flux de code cœur

Ce projet utilise l’organisation pytest + playwright + POM pour structurer les tests UI automatisés. Le flux global est le suivant : conftest.py initialise le navigateur et le contexte de manière centralisée, l’état de connexion est réutilisé via storage_state ; le répertoire pages encapsule les éléments et les opérations métier ; le répertoire testcases contient les cas de test ; les données de test sont stockées dans des fichiers JSON ; les résultats sont enregistrés avec Allure, logs et captures d’écran.

3.1 Configuration globale préalable

Pour éviter la répétition de code dans chaque cas de test, les opérations communes comme le lancement du navigateur ou la création du contexte sont gérées via des fixtures dans conftest.py.

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,
            args=["--start-maximized"]
        )
        yield browser
        browser.close()

Cette fixture ne lance le navigateur qu’une seule fois par session de test, améliorant l’efficacité.

3.2 Réutilisation de l’état de connexion

Pour les modules nécessitant une connexion, chaque cas de test n’a pas besoin de se reconnecter. Playwright permet de sauvegarder l’état de connexion via storage_state.

@pytest.fixture(scope="session")
def auth_state(browser):
    if AUTH_FILE.exists():
        return str(AUTH_FILE)

    context = browser.new_context(
        no_viewport=True,
        base_url=BASE_URL
    )
    page = context.new_page()
    login = LoginPage(page)
    login.login("email", "password", url="/auth")
    expect(page.locator("#chat-input")).to_be_visible(timeout=10000)
    context.storage_state(path=str(AUTH_FILE))
    context.close()
    return str(AUTH_FILE)

Lors de la première exécution, le fichier auth_state.json est créé. Les exécutions suivantes le réutilisent dircetement.

@pytest.fixture(scope="function")
def logged_in_page(browser, auth_state):
    context = browser.new_context(
        no_viewport=True,
        base_url=BASE_URL,
        storage_state=auth_state
    )
    page = context.new_page()
    page.goto("/")
    yield page
    context.close()

Ainsi, chaque cas de test reçoit un objet page déjà connecté, se concentrant sur la validation fonctionnelle.

3.3 Encapsulation en objets de page (POM)

Le modèle POM sépare la logique de localisation des éléments de la logique de test. Les pages sont encapsulées dans le répertoire pages.

class LoginPage:
    def __init__(self, page):
        self.page = page
        self.email_input = page.locator("#email")
        self.password_input = page.get_by_placeholder("Entrez votre mot de passe")
        self.login_button = page.get_by_text("Connexion", exact=True)

    def navigate(self, url):
        self.page.goto(url)

    def input_email(self, email):
        self.email_input.fill(email)

    def input_password(self, password):
        self.password_input.fill(password)

    def click_login_button(self):
        self.login_button.click()

    def login(self, email, password, url="/auth"):
        self.navigate(url)
        self.input_email(email)
        self.input_password(password)
        self.click_login_button()

Cette approche facilite la maintenance : si un élément change, seule la classe de page est modifiée.

3.4 Paramétrage des données de test

Les données de test sont stockées dans des fichiers JSON dans le répertoire data et lues via une méthode utilitaire.

def get_json_data(file_path):
    file_path = BASE_DIR / file_path
    with open(file_path, "r", encoding="utf-8") as f:
        data = json.load(f)
    data_list = []
    for item in data:
        data_list.append(tuple(item.values()))
    return data_list

Les cas de test utliisent @pytest.mark.parametrize pour exécuter plusieurs jeux de données.

@pytest.mark.parametrize(
    "email, password, expected",
    get_json_data("data/login_fail_data.json")
)
def test_login_fail(login_page, email, password, expected):
    login_page.login(email, password)
    expect(login_page.page.get_by_text(expected)).to_be_visible()

3.5 Flux principal des questions-réponses IA

Ce module teste la sélection du modèle, la saisie de la question, l’envoi et la génération de la réponse.

class AIQuestionAnsweringProcessPage:
    def __init__(self, page):
        self.page = page
        self.model_select = page.locator("button[aria-haspopup='listbox']")
        self.model_option = page.get_by_role("option", name="Sélectionner un modèle")
        self.chat_input = page.locator("#chat-input > [data-placeholder='Que puis-je faire pour vous ?']")
        self.send_button = page.locator("#send-message-button")
        self.message_items = page.locator("div[role='listitem']")

    def user_chat_process(self, text):
        expect(self.chat_input).to_be_visible(timeout=10000)
        self.model_select.click()
        self.model_option.click()
        self.chat_input.fill(text)
        self.send_button.click()

Le cas de test vérifie principalement la présence de la réponse plutôt que son contenu exact.

def test_ai_chat_process(logged_in_page):
    ai_page = AIQuestionAnsweringProcessPage(logged_in_page)
    ai_page.user_chat_process("Présentez Playwright")
    expect(ai_page.message_items.last).to_be_visible(timeout=60000)

3.6 Conception des assertions

Le projet combine les assertions expect de Playwright et les assertions natives Python pour valider l’état des éléments, les résultats métier et les requêtes réseau.

Pour la validation fonctionnelle :

expect(page.locator("#chat-input")).to_be_visible(timeout=10000)

Pour les flux IA, l’accent est mis sur les changements d’état plutôt que sur le contenu exact :

expect(ai_page.message_items.last).to_be_visible(timeout=60000)

Pour les formulaires, on vérifie la présence de messages d’erreur :

expect(login_page.email_empty_error).to_be_visible()
expect(login_page.password_error).to_be_visible()

Pour les requêtes Ajax, on utilise des assertions Python :

assert req.value.method == "POST"
assert "application/json" in req.value.header_value("content-type")
assert req.value.post_data_json == {
    "email": "admin@example.com",
    "password": "AaAa123456"
}

Pour les réponses :

assert res.value.status == 200

Enfin, pour les scénarios d’erreur simulés :

expect(page.get_by_text("Erreur interne du serveur")).to_be_visible(timeout=5000)

3.7 Validation des requêtes et réponses Ajax

Playwright permet d’écouter les requêtes réseau pour valider les appels d’API.

def test_login_request(login_page):
    login_page.navigate("/auth")
    login_page.input_email("email")
    login_page.input_password("password")

    with login_page.page.expect_request("**/api/v1/auths/signin") as req:
        login_page.click_login_button()

    assert req.value.method == "POST"
    assert "application/json" in req.value.header_value("content-type")

Validation de la réponse :

def test_login_response(login_page):
    login_page.navigate("/auth")
    login_page.input_email("email")
    login_page.input_password("password")

    with login_page.page.expect_response("**/api/v1/auths/signin") as res:
        login_page.click_login_button()

    assert res.value.status == 200

3.8 Simulation de scénarios d’erreur (Mock)

Pour couvrir les erreurs serveur, on utilise route.fulfill pour simuler des réponses anormales.

def handle_500(route):
    route.fulfill(
        status=500,
        content_type="application/json",
        body='{"detail":"Erreur interne du serveur"}'
    )

Cas de test correspondant :

def test_login_api_500(login_page):
    page = login_page.page
    login_page.navigate("/auth")
    login_page.input_email("email")
    login_page.input_password("password")
    page.route("**/api/v1/auths/signin", handler=handle_500)
    login_page.click_login_button()
    expect(page.get_by_text("Erreur interne du serveur")).to_be_visible(timeout=5000)

3.9 Gestion des logs et des résultats

Un module de logs centralisé enregistre les informations clés pendant l’exécution. Les cas échoués déclenchent automatiquement une capture d’écran.

logs.info("Début du test de connexion")
logs.info("Assertion du statut de la réponse de connexion réussie")

Les résultats sont stockés dans test_results et allure-results pour générer un rapport Allure.

pytest --alluredir=allure-results
allure serve allure-results

4.0 Mécanisme de capture d’écran et de log en cas d’échec

Le hook pytest_runtest_makereport est utilisé pour capturer automatiquement une capture d’écran et enregistrer les logs en cas d’échec.

@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()

    if report.when == "call":
        if report.failed:
            logs.error(f"Échec du cas de test : {item.nodeid}")
            logs.error(f"Détails :\n{report.longrepr}")

            page = (
                item.funcargs.get("before_all")
                or item.funcargs.get("logged_in_page")
            )

            if page:
                screenshot_dir = Path("test_results/fail_screenshots")
                screenshot_dir.mkdir(parents=True, exist_ok=True)
                current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
                screenshot_path = screenshot_dir / f"fail_{current_time}.png"
                page.screenshot(path=str(screenshot_path), full_page=True)
                logs.error(f"Capture d’écran sauvegardée : {screenshot_path}")

        elif report.passed:
            logs.info(f"Cas de test réussi : {item.nodeid}")

4.1 Génération automatique des titres Allure

Pour éviter de répéter les décorateurs Allure, le hook pytest_runtest_call lit les docstrings des classes et méthodes de test pour définir automatiquement les titres et features Allure.

def pytest_runtest_call(item: Item):
    if item.parent and getattr(item.parent, "_obj", None):
        if item.parent._obj.__doc__:
            allure.dynamic.feature(item.parent._obj.__doc__)
    if item.function.__doc__:
        allure.dynamic.title(item.function.__doc__)

Exemple de cas de test :

class TestLogin:
    """Test du module de connexion"""

    def test_01_login_success(self, login_page):
        """
        Test de connexion réussie
        email : email enregistré
        password : mot de passe correct
        """
        login_page.navigate(url="/auth")
        login_page.input_email("admin@example.com")
        login_page.input_password("AaAa123456")
        login_page.click_login_button()

4.2 Résumé du flux d’exécution

Lecture de la configuration pytest
        ↓
Exécution des fixtures dans conftest.py
        ↓
Lancement du navigateur et création du contexte
        ↓
Vérification de l’existence du fichier storage_state
        ↓
Réutilisation directe ou connexion puis sauvegarde
        ↓
Appel des méthodes des objets de page (pages)
        ↓
Exécution des cas de test (testcases)
        ↓
Lecture des données JSON (data)
        ↓
Opérations, attentes et assertions via Playwright
        ↓
Validation des appels Ajax et simulations Mock
        ↓
Génération des logs, captures d’écran et rapport Allure

4. Intégration avec GitHub et Jenkins

Développement local et débogage des scripts de test
        ↓
Commit du code sur le dépôt local via Git
        ↓
Push du code vers le dépôt GitHub distant
        ↓
Jenkins récupère le code depuis GitHub
        ↓
Installation des dépendances et de l’environnement Playwright
        ↓
Exécution des commandes pytest
        ↓
Génération des résultats bruts allure-results
        ↓
Jenkins génère le rapport de test via le plugin Allure
        ↓
Archivage des logs, captures d’écran et rapports
        ↓
Consultation des résultats et identification des cas échoués

Étiquettes: Playwright pytest Allure UI Automation Python

Publié le 6 juillet à 20h47