# Migrer des tests Selenium C# vers Playwright .NET : équivalences, pièges et plan

Source : https://consignestudio.fr/guides/migrer-selenium-playwright-csharp/ · publié le 2026-10-27 · Antoine Despres, Consigne

Migrer une suite Selenium C# vers Playwright .NET consiste surtout à remplacer les By par des localisateurs, à supprimer les attentes explicites (Playwright attend avant chaque action) et à rendre le code asynchrone de bout en bout. La plupart des appels ont un équivalent direct ; les fenêtres, les scripts asynchrones et la grille distante demandent une réécriture à la main. Procédez par lots, avec les deux bibliothèques côte à côte le temps de la transition.

## Ce qui change vraiment

Trois différences de fond expliquent presque toutes les réécritures.

1. **Tout est asynchrone.** Chaque appel Playwright renvoie une `Task` : `ClickAsync()`, `FillAsync()`, `TitleAsync()`. Une méthode qui en appelle un devient `async Task`, puis ses appelants aussi, du test jusqu'aux page objects et aux utilitaires.
2. **Playwright attend tout seul.** Avant un clic, il vérifie que l'élément est visible, stable, qu'il reçoit les événements et qu'il est activé ; avant un `FillAsync`, qu'il est visible, activé et modifiable. Si ces conditions ne sont pas réunies dans le délai, l'action échoue par dépassement de délai.
3. **Un localisateur n'est pas un élément.** Un `ILocator` est réévalué à chaque usage : l'exception « élément périmé » de Selenium (`StaleElementReferenceException`) n'a plus d'équivalent. En contrepartie, les localisateurs sont **stricts** : une action lève une exception si plusieurs éléments correspondent.

## Sélecteurs : de By à Locator

| Selenium | Playwright .NET |
|---|---|
| `By.Id("panier")` | `Page.Locator("#panier")` |
| `By.CssSelector(s)` | `Page.Locator(s)` |
| `By.XPath(x)` | `Page.Locator("xpath=" + x)` |
| `By.Name("q")` | `Page.Locator("[name=\"q\"]")` |
| `By.ClassName("c")` | `Page.Locator(".c")` |
| `By.LinkText(t)` | `Page.Locator("a:text-is(\"t\")")`, ou `GetByRole(AriaRole.Link, new() { Name = t })` |
| `FindElement(by)` | `Page.Locator(sel).First` si plusieurs éléments peuvent correspondre |
| `FindElements(by)` | `Page.Locator(sel).AllAsync()` ; `Count` devient `CountAsync()`, `[n]` devient `Nth(n)` |

Une conversion mécanique garde vos sélecteurs CSS et XPath, ce qui permet de migrer sans changer le comportement. La documentation de Playwright recommande ensuite les localisateurs intégrés (`GetByRole`, `GetByLabel`, `GetByText`, `GetByPlaceholder`, `GetByTestId`…), plus robustes aux changements du DOM que les chaînes CSS ou XPath. Faites ce second passage plus tard, page object par page object.

> Attention au `.First` ajouté partout : `FindElement` renvoyait le premier élément trouvé, mais Playwright déconseille `First`, `Last` et `Nth` quand on peut écrire un localisateur qui ne désigne qu'un élément. Gardez-les pour la migration, puis resserrez les sélecteurs.

## Actions et lectures

| Selenium | Playwright .NET | À vérifier |
|---|---|---|
| `Click()`, `Clear()` | `ClickAsync()`, `ClearAsync()` | |
| `SendKeys("texte")` | `FillAsync("texte")` | `FillAsync` remplace le contenu ; `SendKeys` ajoutait à la fin |
| `SendKeys(Keys.Enter)` | `PressAsync("Enter")` | |
| `Text` | `InnerTextAsync()` | texte visible, comme Selenium |
| `Displayed`, `Enabled`, `Selected` | `IsVisibleAsync()`, `IsEnabledAsync()`, `IsCheckedAsync()` | ces lectures n'attendent pas |
| `new SelectElement(e).SelectByText(t)` | `SelectOptionAsync(new SelectOptionValue { Label = t })` | |
| `new Actions(driver).MoveToElement(e)` | `HoverAsync()` | |
| `DoubleClick`, `ContextClick`, `DragAndDrop` | `DblClickAsync()`, `ClickAsync(new() { Button = MouseButton.Right })`, `DragToAsync()` | |
| `Navigate().GoToUrl(u)` | `Page.GotoAsync(u)` | |
| `Title`, `PageSource` | `Page.TitleAsync()`, `Page.ContentAsync()` | |
| `ExecuteScript("return …")` | `Page.EvaluateAsync<T>("…")` | le type de retour est à préciser |
| `GetScreenshot().SaveAsFile(f)` | `Page.ScreenshotAsync(new() { Path = f })` | |

Si un test saisit une valeur en plusieurs fois et compte sur l'ajout, utilisez `PressSequentiallyAsync`, qui tape caractère par caractère.

## Attentes : supprimer plus que convertir

Une suite Selenium contient souvent trois couches d'attente : l'attente implicite, des `WebDriverWait` avec `ExpectedConditions`, et des `Thread.Sleep`. Avec Playwright :

* **`ElementIsVisible`, `ElementToBeClickable`** : l'action elle-même attend. Si vous avez besoin d'attendre sans agir, `Locator(sel).WaitForAsync(new() { State = WaitForSelectorState.Visible })`.
* **`InvisibilityOfElementLocated`** : `WaitForAsync` avec l'état `Hidden` ou `Detached`.
* **`UrlContains`, `TitleIs`** : `Page.WaitForURLAsync(...)`, ou mieux une assertion web.
* **`Until(d => …)` avec une condition libre** : `Page.WaitForFunctionAsync` si la condition se lit dans la page, sinon une boucle de sondage avec le même délai.
* **`Thread.Sleep`** : à remplacer par une attente sur un état précis. Un `Task.Delay` est une traduction littérale, pas une solution.
* **Attente implicite** : elle n'existe pas. Attention aux endroits où Selenium l'appliquait et où Playwright n'attend pas : compter des éléments, lire une liste, tester une visibilité.

Les **assertions web** remplacent avantageusement les attentes suivies d'un `Assert` : elles réessaient jusqu'au délai (5 secondes par défaut selon la documentation).

```csharp
// Selenium
Assert.That(driver.Title, Is.EqualTo("Accueil"));
Assert.AreEqual("3", panier.Text);

// Playwright
await Expect(Page).ToHaveTitleAsync("Accueil");
await Expect(panier).ToHaveTextAsync("3");
```

## Cadres, dialogues, fenêtres et fichiers

**Cadres.** Selenium change de contexte avec `SwitchTo().Frame(...)` puis `SwitchTo().DefaultContent()`. Playwright n'a pas d'état global : on localise à travers le cadre.

```csharp
var cadre = Page.FrameLocator("#paiement");
await cadre.Locator("#numero").FillAsync("4111");
```

Le piège : une méthode utilitaire appelée après un `SwitchTo().Frame` travaillait implicitement dans le cadre. Il faut désormais lui passer le `IFrameLocator`.

**Alertes et confirmations.** Playwright ferme les dialogues automatiquement si aucun gestionnaire n'est posé. Pour accepter, posez le gestionnaire **avant** l'action qui ouvre le dialogue, et traitez-le dans le gestionnaire, sinon l'action reste bloquée :

```csharp
Page.Dialog += async (_, dialog) => await dialog.AcceptAsync();
await Page.GetByRole(AriaRole.Button, new() { Name = "Supprimer" }).ClickAsync();
```

**Fenêtres et onglets.** `WindowHandles` et `SwitchTo().Window(h)` n'ont pas d'équivalent direct. On récupère la nouvelle page au moment où l'action l'ouvre :

```csharp
var onglet = await Context.RunAndWaitForPageAsync(async () =>
{
    await Page.GetByText("Ouvrir le rapport").ClickAsync();
});
await Expect(onglet).ToHaveTitleAsync("Rapport");
```

Pour une fenêtre surgissante ouverte par la page, `Page.RunAndWaitForPopupAsync` suit le même modèle.

**Envoi de fichier.** `SendKeys(chemin)` sur un `input type="file"` devient `SetInputFilesAsync(chemin)` ; un tableau envoie plusieurs fichiers, un tableau vide vide la sélection. Si le sélecteur de fichier s'ouvre par un clic sur un autre élément, utilisez `Page.RunAndWaitForFileChooserAsync` puis `SetFilesAsync`.

## Le projet de test

* Remplacez les paquets `Selenium.*` et les pilotes par `Microsoft.Playwright.NUnit`, `Microsoft.Playwright.MSTest` ou `Microsoft.Playwright.Xunit`.
* Faites hériter les classes de test de `PageTest` : chaque test reçoit une page neuve dans son propre contexte de navigateur. `ContextTest` sert aux scénarios à plusieurs onglets. La création du pilote, `Quit()` et `Dispose()` disparaissent.
* Le navigateur et le mode sans interface se règlent dans un fichier `.runsettings` (`BrowserName`, `LaunchOptions`), ou en ligne de commande avec `dotnet test -- Playwright.BrowserName=firefox`.
* Les navigateurs ne s'installent pas avec le paquet NuGet : après un `dotnet build`, lancez `pwsh bin/Debug/net8.0/playwright.ps1 install` (adaptez `net8.0` à votre cible), y compris sur les agents d'intégration continue.
* Une grille Selenium (`RemoteWebDriver`) se remplace par `BrowserType.ConnectAsync` vers le point d'accès Playwright de votre fournisseur, s'il en propose un.

## Plan de migration par étapes

1. **Mesurer.** Comptez les tests, les page objects, les usages de `SwitchTo().Window`, `ExecuteAsyncScript`, `RemoteWebDriver` et les cadres traversés par des utilitaires : ce sont eux qui coûtent du temps.
2. **Faire cohabiter.** Ajoutez Playwright au projet sans retirer Selenium, pour que tout compile pendant la transition.
3. **Migrer un pilote de 10 à 20 tests représentatifs**, dont au moins un avec cadre, dialogue ou fenêtre. Mesurez le temps passé : c'est votre base d'estimation.
4. **Convertir par page object**, avec ses tests, en propageant `async` / `await`. Ne mélangez pas conversion et refonte des sélecteurs.
5. **Nettoyer les attentes** : retirer les `Sleep`, transformer les attentes suivies d'assertions en assertions web.
6. **Brancher l'intégration continue** : installation des navigateurs, `.runsettings`, rapports.
7. **Retirer Selenium** quand plus aucun fichier ne le référence.

> Une migration ne corrige pas l'instabilité. Un test qui échoue une fois sur dix sous Selenium à cause d'une donnée partagée ou d'un ordre d'exécution échouera aussi sous Playwright. Stabilisez ou isolez ces tests avant de comparer les deux suites.

## Automatiser la conversion

[Bascule](https://consignestudio.fr/migrer-selenium-vers-playwright/) fait l'étape 1 gratuitement : son diagnostic lit une solution C# avec Roslyn, sans la compiler ni l'exécuter, et classe chaque usage de Selenium en automatique, à relire ou à reprendre, avec le fichier et la ligne. L'édition Pro réécrit la suite en Playwright .NET selon les règles décrites plus haut, convertit le `.csproj`, recompile le résultat et signale chaque erreur apparue. Elle ne traite que le C# (NUnit, MSTest, xUnit 2), ne lance pas vos tests et ne convertit pas les fenêtres par identifiant, `ExecuteAsyncScript`, la grille distante ni Appium : ces points restent à écrire par vous. Une migration au forfait, à commencer par un pilote, est aussi proposée.

## Sources

- [Playwright for .NET, Auto-waiting](https://playwright.dev/dotnet/docs/actionability)
- [Playwright for .NET, Locators](https://playwright.dev/dotnet/docs/locators)
- [Playwright for .NET, Actions](https://playwright.dev/dotnet/docs/input)
- [Playwright for .NET, Frames](https://playwright.dev/dotnet/docs/frames)
- [Playwright for .NET, Dialogs](https://playwright.dev/dotnet/docs/dialogs)
- [Playwright for .NET, Pages](https://playwright.dev/dotnet/docs/pages)
- [Playwright for .NET, Assertions](https://playwright.dev/dotnet/docs/test-assertions)
- [Playwright for .NET, Test runners](https://playwright.dev/dotnet/docs/test-runners)
- [Playwright for .NET, Installation](https://playwright.dev/dotnet/docs/intro)
- Bascule, règles de conversion Selenium vers Playwright (docs/regles.md)
- Documentation relue le 2026-10-27
