Consigne

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

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.

Publié le 27 octobre 2026 · 6 min · par Antoine Despres, Consigne

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

SeleniumPlaywright .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.

Actions et lectures

SeleniumPlaywright .NETÀ vérifier
Click(), Clear()ClickAsync(), ClearAsync()
SendKeys("texte")FillAsync("texte")FillAsync remplace le contenu ; SendKeys ajoutait à la fin
SendKeys(Keys.Enter)PressAsync("Enter")
TextInnerTextAsync()texte visible, comme Selenium
Displayed, Enabled, SelectedIsVisibleAsync(), 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, DragAndDropDblClickAsync(), ClickAsync(new() { Button = MouseButton.Right }), DragToAsync()
Navigate().GoToUrl(u)Page.GotoAsync(u)
Title, PageSourcePage.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).

// 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.

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 :

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 :

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.

Automatiser la conversion

Bascule 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

Autres guides