Ce que signifie réellement le test de bout en bout
Le test de bout en bout (e2e) pilote le système réel du point de vue de l’utilisateur. Au lieu d’appeler une fonction ou une route de manière isolée, un test e2e ouvre l’application, effectue les mêmes étapes qu’une personne réelle et vérifie que le résultat attendu par l’utilisateur s’est bien produit.
Cette définition a deux conséquences. Premièrement, le test traverse toutes les couches du système : navigateur, code frontend, HTTP, API, base de données et retour. Deuxièmement, les assertions portent sur le comportement et non sur l’implémentation. Vous vérifiez qu’une confirmation est visible, et non qu’une fonction particulière a été appelée.
Comme l’ensemble de la stack est sollicité, les tests e2e sont ce qui se rapproche le plus d’une garantie que l’utilisateur peut mener à bien une tâche. Ce sont également les tests les plus lents et les plus coûteux que vous écrirez, c’est précisément pour cela qu’il faut en écrire peu et les choisir avec soin.
Où se situent les tests e2e dans la pyramide des tests
La pyramide des tests est une règle empirique qui définit la proportion de chaque type de test à rédiger. La base est large : de nombreux tests unitaires rapides. Le milieu est plus étroit : des tests d’intégration couvrant quelques composants. Le sommet est réduit : une poignée de parcours de bout en bout (end-to-end).
- Les tests unitaires s’exécutent en quelques millisecondes, sans infrastructure, et permettent de verrouiller la logique pure et les cas limites.
- Les tests d’intégration s’exécutent en quelques secondes, sollicitent une route ou un module avec de vrais collaborateurs, et détectent les bugs de câblage.
- Les tests end-to-end s’exécutent en plusieurs dizaines de secondes, tournent sur une stack déployée, et prouvent qu’un utilisateur peut accomplir une tâche critique.
Cette forme est importante car le coût augmente et le feedback ralentit à mesure que l’on monte dans la pyramide. Un bug qu’un test unitaire peut détecter doit être trouvé par un test unitaire. Réservez les tests e2e pour les parcours où un chemin rompu signifie un produit cassé : inscription, connexion, paiement, publication, invitation. Si un test n’a pas besoin d’un navigateur pour être pertinent, il ne devrait probablement pas en utiliser un.
Un test de cohérence utile : pour chaque test e2e, demandez-vous quel test unitaire ou d’intégration aurait pu détecter le même bug. Si la réponse est « un test facile à écrire », alors le test e2e se trouve dans la mauvaise couche.
Choisir les parcours qui valent la peine d’être automatisés
Vous ne pouvez pas tout automatiser. Choisissez donc les parcours où un échec coûte le plus cher et où un bug est le plus susceptible de passer à travers les couches de tests inférieures.
Évaluez chaque parcours candidat selon deux axes : l’impact business et le risque d’intégration. C’est là où l’impact est élevé et le risque important que les tests e2e justifient pleinement leur coût.
- Impact élevé, risque élevé — inscription, connexion, paiement, réinitialisation du mot de passe. Automatisez-les en priorité.
- Impact élevé, risque faible — la page d’accueil marketing. Couvrez-la avec un smoke test, pas avec un parcours complet.
- Impact faible, risque élevé — un export administrateur utilisé mensuellement. Un seul test suffit, ou une vérification scriptée.
- Impact faible, risque faible — les commutateurs de paramètres et les états cosmétiques. Laissez cela aux tests unitaires et aux tests de composants.
Les parcours qui traversent le plus de frontières sont les plus précieux, car ce sont ceux qu’un test unitaire ne peut pas atteindre. Un processus de paiement sollicite le panier, la tarification, le paiement, le service de commande et l’e-mail — précisément les connexions qui cassent silencieusement lorsqu’une équipe renomme un champ.
Notez cette liste et revoyez-la chaque trimestre. L’ensemble des parcours critiques évolue à mesure que le produit grandit, et le flux important d’hier peut devenir le code mort d’aujourd’hui.
Choisir son outil : Playwright ou Cypress
Deux outils dominent aujourd’hui les tests de navigateur, et les deux sont excellents.
Playwright pilote Chromium, Firefox et WebKit via une seule API. Par défaut, il exécute les tests en parallèle via des processus workers, supporte plusieurs projets de navigateurs, propose des locators avec auto-attente et inclut un trace viewer qui enregistre l’exécution image par image. Son storageState simplifie la réutilisation de l’authentification, et sa fixture request vous fournit un client API au sein du même test. C’est aujourd’hui le choix par défaut le plus courant pour les nouvelles suites de tests.
Cypress s’exécute directement dans la boucle d’événements (event loop) du navigateur, ce qui rend le débogage instantané : vous pouvez inspecter le DOM à tout moment et naviguer dans le temps à travers les commandes via le runner. Son API est conviviale et ses messages d’erreur sont excellents. Le compromis réside dans le support multi-navigateurs et le parallélisme, qui ont historiquement nécessité plus de configuration.
Le choix de l’outil importe rarement autant que la discipline appliquée. Une suite Playwright bien écrite et une suite Cypress bien écrite fonctionnent toutes les deux. Choisissez-en un, apprenez son modèle d’attente, et concentrez vos efforts sur l’isolation et les locators.
L’anatomie d’un test
Presque tous les tests e2e sont composés des trois mêmes étapes, généralement appelées arrange, act et assert.
test("a signed-in user can upgrade to Pro", async ({ page }) => {
// arrange: the account and plan already exist
// act: perform the journey
await page.goto("/pricing");
await page.getByRole("button", { name: "Choose Pro" }).click();
// assert: the user sees the result
await expect(
page.getByRole("heading", { name: "You're on Pro" }),
).toBeVisible();
});
Dans la mesure du possible, gardez l’étape arrange en dehors de l’interface utilisateur. Créer un utilisateur en remplissant un formulaire d’inscription rallonge la durée d’exécution de votre suite de tests et couple chaque test au flux d’inscription. Créez-le plutôt via l’API, puis commencez le test en étant déjà connecté.
Une autre bonne habitude à prendre est de limiter chaque test à un seul parcours. Lorsqu’un test couvre cinq éléments et échoue, vous savez seulement que l’un de ces cinq éléments est cassé. Lorsqu’il n’en couvre qu’un seul, le nom du test en échec constitue directement le diagnostic.
Rédiger un parcours fluide et lisible
Un test e2e est lu bien plus souvent qu’il n’est écrit. Un lecteur doit être capable de comprendre ce qu’il protège sans avoir besoin d’ouvrir l’application.
Nommez le test en fonction du résultat pour l’utilisateur, et non de l’implémentation. « un utilisateur connecté peut passer à l’offre Pro » est préférable à « test clic bouton paiement ». Utilisez le présent et le vocabulaire de l’utilisateur.
Structurez le test comme une séquence d’actions utilisateur, une par ligne, avec des lignes vides entre les différentes phases.
test("an admin can invite a teammate", async ({ page }) => {
await page.goto("/team");
await page.getByRole("button", { name: "Invite" }).click();
await page.getByLabel("Email").fill("[email protected]");
await page.getByRole("button", { name: "Send invite" }).click();
await expect(page.getByText("Invitation sent")).toBeVisible();
});
Extrayez les parcours répétitifs dans des helpers, tout en veillant à ce que le test reste lisible. Un helper nommé upgradeToPro(page) est une bonne pratique ; un helper qui masque l’intégralité du test ne l’est pas. Le test doit toujours laisser apparaître les étapes.
Enfin, commentez le « pourquoi » et non le « quoi ». Le code indique déjà quel bouton a été cliqué ; un commentaire n’est utile que s’il explique pourquoi le test existe ou pourquoi une étape semble inhabituelle.
Locators : adressez-vous à l’utilisateur, pas au DOM
Un locator est la méthode utilisée par le test pour trouver un élément. L’amélioration la plus significative que vous puissiez apporter à une suite e2e est de choisir vos locators de la même manière qu’un utilisateur le ferait.
Playwright les classe du plus recommandé au moins recommandé :
getByRole— boutons, liens, titres et champs de saisie par rôle accessible et nom.getByLabel— contrôles de formulaire par leur label.getByText— texte visible.getByPlaceholder,getByAltText,getByTitle— autres attributs visibles.getByTestId— un test id stable lorsque rien d’autre orienté utilisateur ne convient.
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByLabel("Email").fill("[email protected]");
await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();
Cet ordre n’est pas esthétique. Les locators basés sur le rôle et le label échouent lorsqu’un élément est inaccessible, le test faisant ainsi office de vérification d’accessibilité. Ils résistent également aux refactorisations : renommer une classe CSS ne les impacte pas, alors que cela casserait un .btn.btn--primary > span.
Utilisez getByTestId de manière délibérée, et non par défaut. Un test id est un contrat que vous devez maintenir, et une suite remplie de test ids ne vous indique en rien si l’interface est cohérente.
Gérer l’authentification
Se connecter via l’interface utilisateur avant chaque test est la cause principale de suites e2e lentes et instables. Une connexion implique un formulaire, un aller-retour réseau et une redirection, répétés des centaines de fois.
La solution consiste à s’authentifier une seule fois et à réutiliser la session. Playwright appelle cette session sauvegardée storageState — il s’agit des cookies et du local storage sérialisés dans un fichier.
// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.E2E_EMAIL!);
await page.getByLabel("Password").fill(process.env.E2E_PASSWORD!);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.context().storageState({ path: "playwright/.auth/user.json" });
});
Un projet de configuration (setup project) écrit le fichier, et les projets de navigateur en dépendent et le chargent via use.storageState. Les tests nécessitant un utilisateur différent utilisent un fichier d’état distinct.
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{
name: "chromium",
use: { storageState: "playwright/.auth/user.json" },
dependencies: ["setup"],
},
],
});
Vous devriez tout de même conserver un test qui effectue la connexion via l’interface. Le formulaire de connexion est un parcours utilisateur à part entière, et le raccourci ne doit jamais être l’unique moyen de le tester.
Rendre les tests déterministes
Le déterminisme est la clé. Un test qui réussit par intermittence est pire qu’un test qui échoue systématiquement, car il apprend à l’équipe à ignorer les alertes.
Trois règles permettent de couvrir la majorité des cas.
Ne jamais utiliser sleep. waitForTimeout(3000) est soit trop court et instable, soit trop long et lent. Attendez plutôt que la condition soit remplie : await expect(locator).toBeVisible(). Les assertions conçues pour le web réessaient jusqu’à ce qu’elles réussissent ou qu’un délai d’expiration soit atteint, ce qui est exactement le comportement recherché.
Contrôler le réseau. Simulez les tiers et les dépendances instables avec page.route, afin qu’un environnement de test de paiement lent ne fasse pas échouer votre suite de tests. Effectuez des assertions sur les requêtes émises par votre application lorsque c’est le comportement testé.
await page.route("**/api/recommendations", (route) =>
route.fulfill({ json: { items: [] } }),
);
Figer le temps et le hasard. Un test qui dépend de la date du jour ou d’un identifiant aléatoire échouera tôt ou tard. Injectez une horloge ou fixez les valeurs lues par le test.
Il y a également le navigateur lui-même : les animations, l’autofocus et les transitions créent des conditions de concurrence (race conditions) qui ressemblent à des bugs applicatifs. Désactivez les animations dans l’environnement de test quand c’est possible, et privilégiez les assertions sur l’état final.
Couverture multi-navigateurs et responsive
Un parcours qui fonctionne sous Chromium peut échouer sous WebKit, et une mise en page adaptée à un ordinateur portable peut s’avérer inutilisable sur un téléphone. La couverture multi-navigateurs et multi-viewports est l’un des rares aspects où les tests e2e apportent une valeur ajoutée irremplaçable.
Playwright transforme cela en un simple problème de configuration. Définissez un projet par navigateur et réutilisez les mêmes tests.
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});
Tous les tests n’ont pas besoin d’être exécutés sur tous les navigateurs. Lancez la suite complète sur Chromium pour un feedback rapide, et exécutez les parcours critiques sur l’ensemble des projets selon un planning précis ou avant chaque mise en production. Cela permet de garder des pull requests rapides sans sacrifier la couverture.
Lorsqu’un bug est spécifique à un navigateur, ajoutez un test de régression dans le projet concerné. Un commentaire incluant un lien vers le ticket d’incident a bien plus de valeur qu’une note vague du type « Safari est bizarre ».
Gérer les données de test
Les données sont la raison pour laquelle les suites e2e deviennent discrètement instables. Deux tests qui créent tous deux un utilisateur nommé [email protected] réussiront s’ils sont lancés séparément, mais échoueront s’ils sont exécutés ensemble.
Effectuez le seed via l’API. C’est plus rapide que via l’UI, cela signale clairement les erreurs lorsque le seeding échoue, et cela permet de garder le test concentré sur le parcours utilisateur plutôt que sur la configuration.
const res = await request.post("/api/test/users", {
data: { email: `ada+${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();
Isolez les données par test ou par worker. L’utilisation d’emails uniques, d’enregistrements limités à un tenant ou d’un namespace frais par worker fonctionne parfaitement. L’important est qu’aucun test ne dépende de la même ligne en base de données.
Nettoyez ce que vous créez, ou rendez le nettoyage inutile en limitant la portée des données à l’exécution d’un test et en supprimant l’ensemble du scope à la fin. Les enregistrements résiduels s’accumulent, ralentissent la base de données et finissent par provoquer des échecs qui n’ont aucun lien avec la modification actuelle.
Exécuter les tests sur une stack réelle
Un test e2e a besoin d’un environnement pour s’exécuter : un frontend, une API et une base de données, tous utilisant la même version du code. L’option la plus reproductible est une stack docker compose qui lance l’ensemble avec une seule commande.
docker compose -f docker-compose.e2e.yml up -d --wait
pnpm exec playwright test
docker compose -f docker-compose.e2e.yml down -v
Le flag --wait est important : il force Compose à ne s’arrêter que lorsque les health checks sont validés, ce qui élimine la race condition du type “connexion refusée lors du premier test”. Le -v lors de l’arrêt supprime les volumes afin que l’exécution suivante démarre sur une base propre.
Les environnements de preview vont encore plus loin. Une plateforme build la branche, la déploie sur une URL temporaire et l’expose au job de test. C’est ce qui se rapproche le plus de la production pour un run e2e, et cela permet aux équipes produit et QA de naviguer sur le même build que celui utilisé par les tests.
Pointez votre suite de tests vers l’environnement via une variable d’environnement — BASE_URL — et ne codez jamais l’hôte en dur. La même suite doit pouvoir s’exécuter localement, en CI et sur un environnement de preview.
Quand le parcours est une API, et non un navigateur
Tous les parcours de bout en bout n’ont pas besoin d’un navigateur. Un webhook, un job d’arrière-plan, une CLI ou un flux de service à service sont tous des parcours de bout en bout au sens où cela compte : ils sollicitent le système réel.
La fixture request de Playwright est un client API complet, donc un parcours API ressemble presque exactement à un parcours via navigateur.
test("a paid order is fulfilled end to end", async ({ request }) => {
const order = await request.post("/api/orders", {
data: { sku: "pro-plan", quantity: 1 },
});
expect(order.status()).toBe(201);
await request.post("/api/payments/webhook", {
data: { orderId: (await order.json()).id, status: "paid" },
});
const res = await request.get(`/api/orders/${(await order.json()).id}`);
expect(await res.json()).toMatchObject({ status: "fulfilled" });
});
Ces tests sont plus rapides et beaucoup moins instables (“flaky”) que les tests de navigateur ; utilisez-les donc pour les flux qui n’ont réellement pas besoin d’une interface utilisateur. Pour une couverture API pure à un niveau plus bas, Supertest est l’outil le plus léger.
Exécuter les tests e2e en CI
Les tests E2E doivent être lancés lors des pull requests et avant une release, et non à chaque sauvegarde. En CI, quelques flags font la différence entre un garde-fou utile et un processus ignoré.
- Exécuter en mode headless. Il n’y a pas d’affichage ; Playwright et Cypress utilisent tous deux le mode headless par défaut en CI.
- Sharder la suite de tests. Répartissez les fichiers sur plusieurs machines avec
--shard=1/3pour que le temps d’exécution réel reste constant à mesure que la suite s’agrandit. - Relancer une seule fois. Un seul retry permet de distinguer un échec réel d’un problème d’infrastructure, à condition de suivre le taux de retry plutôt que de le laisser masquer l’instabilité (flakiness).
- Uploader les artefacts en cas d’échec. Les traces, captures d’écran et vidéos sont les seuls moyens de diagnostiquer un build rouge en CI sans avoir à le reproduire localement.
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --retries=1
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: report-${{ matrix.shard }}
path: playwright-report/
Les retries sont un filet de sécurité, pas une solution. Si un test nécessite un retry pour passer systématiquement, c’est qu’il présente une véritable race condition ou un bug d’isolation ; le retry ne fait que vous donner du temps pour le trouver.
Déboguer un run en échec
La première question après un échec est toujours la même : qu’est-ce que le navigateur a réellement vu ? Un outil capable d’y répondre sans reproduction locale transforme une investigation de deux heures en une investigation de deux minutes.
Playwright enregistre une trace — captures d’écran, snapshots du DOM, réseau et console — et le trace viewer permet de rejouer le run étape par étape. Conservez les traces en cas d’échec, puis ouvrez le rapport et naviguez jusqu’au moment où l’assertion a échoué.
pnpm exec playwright test --trace on-first-retry
pnpm exec playwright show-trace test-results/checkout/trace.zip
Localement, lancez la suite en mode headed ou en mode UI pour observer le test et le mettre en pause. Cypress propose le même concept via son runner interactif, qui conserve un journal de commandes permettant de voyager dans le temps.
pnpm exec playwright test --ui
pnpm exec playwright test --headed --debug
L’habitude qui rend le débogage rapide consiste à effectuer l’assertion sur l’élément qui pose réellement problème. Un échec à expect(page).toHaveURL(/\/checkout/) vous indique que la navigation n’a jamais eu lieu, ce qui mène à une investigation très différente d’un échec sur le titre de confirmation.
Lutter contre le flakiness
Le flakiness (instabilité des tests) est la taxe à payer pour les tests e2e, et il provient presque toujours de l’une des causes suivantes :
- Un await manquant ou une race condition. Le test a agi avant que l’application ne soit stabilisée. Corrigez cela avec une assertion web-first, et non avec un sleep.
- Des données partagées. Deux tests ont modifié la même ligne. Isolez les données par test ou par worker.
- Une dépendance externe. Un script ou une API tierce était lent. Mockez-le ou excluez-le du chemin critique.
- Un locator fragile. Le test était lié à un balisage qui a changé. Passez à un locator basé sur un rôle ou un label.
- Une animation. L’élément était présent mais pas encore stable. Désactivez les animations dans l’environnement de test.
La meilleure façon de combattre le flakiness est de traiter chaque test instable comme un bug avec une cause précise : mettez-le en quarantaine et corrigez la source du problème. Une suite de tests qui passe au vert uniquement grâce aux tentatives de retry est une suite en laquelle personne n’a confiance, et une suite en laquelle personne n’a confiance est pire que l’absence totale de tests.
Maintenir la santé de votre suite de tests dans le temps
Une suite e2e a naturellement tendance à s’étendre jusqu’à devenir lente et passer au rouge. La maintenir en bonne santé est une pratique continue, et non une configuration ponctuelle.
Passez en revue votre suite de tests comme vous passez en revue votre produit. Lorsqu’une fonctionnalité est supprimée, supprimez son test dans la même pull request. Un test qui ne protège plus rien n’est qu’un coût inutile.
Suivez les indicateurs qui comptent : le temps d’exécution total, le taux de réussite et le taux de retry. Une augmentation du taux de retry est le premier signal que le flakiness s’installe, bien avant que la suite ne devienne visiblement instable.
Assumez la responsabilité de la suite. Une suite e2e partagée sans responsable finit par dériver : les échecs sont relancés, les tests sont ignorés, et en l’espace d’un trimestre, plus personne ne se fie à un run rouge. Mettez en place une rotation ou désignez un petit groupe pour la maintenir au vert, et faites de la correction d’un test flaky une tâche prioritaire plutôt qu’une simple interruption.
Gardez la suite suffisamment rapide pour être exécutée à chaque pull request. Lorsqu’elle devient trop volumineuse, utilisez le sharding, parallélisez-la, ou déplacez les parcours les moins critiques vers un run nocturne. Un quality gate qui prend une heure est un quality gate que les développeurs chercheront à contourner.
Ce qu’il ne faut pas couvrir avec les tests e2e
La tentation est de tout tester via le navigateur car cela semble être le scénario le plus réel. Résistez. L’e2e est la couche la plus coûteuse ; utilisez-la donc uniquement là où elle apporte une réelle valeur ajoutée.
Ne couvrez pas avec l’e2e :
- La logique pure et les cas limites. Le formatage des dates, le calcul des prix, les règles de validation — tout cela relève des tests unitaires qui s’exécutent en quelques millisecondes.
- Chaque chemin d’erreur. Une page 500 justifie un seul parcours ; les vingt manières dont l’API peut échouer relèvent des tests d’intégration.
- Les combinaisons d’entrées exhaustives. L’e2e couvre le parcours représentatif, pas la matrice complète.
- Le comportement des composants. L’ouverture d’un menu déroulant relève d’un test de composant, qui est plus rapide et plus précis.
- Tout ce qui est déjà couvert en dessous. Dupliquer un test unitaire au niveau de la couche e2e augmente les coûts et crée un nouveau mode de panne sans ajouter de confiance supplémentaire.
La règle d’or : si le bug peut être détecté sans navigateur, détectez-le sans navigateur.
Bonnes pratiques
- Écrivez peu de tests e2e et faites en sorte que chacun représente un parcours critique.
- Initialisez vos données via l’API et lancez les tests en étant déjà authentifié.
- Utilisez des locateurs basés sur le rôle, le label et le texte ; ne traitez les test ids qu’en dernier recours.
- Attendez que les conditions soient remplies avec des assertions “web-first”, jamais avec
waitForTimeout. - Isolez les données par test ou par worker pour que l’ordre d’exécution n’ait aucune importance.
- Limitez-vous à un seul parcours par test afin qu’un échec identifie clairement la cause.
- Pointez votre suite de tests vers
BASE_URL; exécutez-la localement, en CI et sur vos environnements de preview. - Capturez les traces et les vidéos en cas d’échec et téléchargez-les en tant qu’artefacts CI.
- Mettez en quarantaine et corrigez les tests instables (flakes) ; ne considérez jamais les tentatives de relance (retries) comme une solution.
- Conservez au moins un test de connexion réel, même lorsque vous réutilisez
storageState.
Erreurs courantes
- Se connecter via l’UI avant chaque test.
- Sélectionner des éléments par classe CSS ou par position dans le DOM.
- Ajouter
waitForTimeoutpour « corriger » une race condition. - Partager un utilisateur ou un enregistrement seedé entre plusieurs tests.
- Exécuter les tests sur un serveur local lancé manuellement plutôt que sur une stack reproductible.
- Tester la logique de niveau unitaire via le navigateur.
- Hard-coder
localhost:3000, rendant la suite de tests exécutable sur une seule machine. - Ne télécharger aucun artefact, puis se retrouver incapable de déboguer un échec en CI.
- Laisser un test instable (flaky) en état d’échec et de relance automatique au lieu de le mettre en quarantaine.
Et après ?
Les tests E2E se situent au sommet de la pyramide, et ils sont d’autant plus efficaces que les couches inférieures sont saines. Consultez Supertest pour mettre en place des tests API rapides couvrant la majeure partie de votre surface HTTP, et Playwright pour explorer l’outil plus en profondeur. Si vous envisagez d’autres alternatives, Cypress présente l’autre runner majeur, et Docker explique comment déployer la stack reproductible dont dépend votre suite de tests.