ElementHandle vs. Locator in Playwright

Playwright Framework unterstützt mehrere Verfahren zum Auffinden von Oberflächenelementen auf einer Webseite. Neben den allgemein bekannten Selektoren gibt es in Playwright noch weitere Konzepte wie  ElementHandle und Locator, die die Suche nach DOM-Elementen auf einer Webseite unterstützen und im diesem Artikel näher beleuchtet werden.

Das folgende kleine Beispiel demonstriert die Verwendung von Selector, Locator und ElementHandle Funktionalitäten zum Auffinden einer Taste auf einer Webseite, auf die anschließend geklickt wird.

// clicks the button with ID myButton using Selector
page.click("button#myButton"); 

// Creates a Locator used to click on the same button as above using Locator
Locator buttonLocator = page.locator("button#myButton");
buttonLocator.click();

// Creates a ElementHandle used to click on the same button as above using ElementHandle 
ElementHandle buttonHandle = page.querySelector("button#myButton");
buttonHandle.click();

Doch was ist der Unterschied zwischen einem Locator und ElementHandle? Beide Playwright-Klassen stellen eine Sicht auf die Elemente einer Seite dar und werden jeweils mit Page.locator(selector) bzw. Page.querySelector(selector) / Page.evaluateHandle(expression) erstellt. Der Unterschied zwischen Locator und ElementHandle besteht jedoch darin, dass ElementHandle auf ein bestimmtes (bereits gefundenes) Element verweist, während Locator die Logik für die Suche nach einem Element enthält.

Im folgenden Code, der auch auf der Playwright-Webseite zu finden ist, wird jeweils ein ElementHandle und Locator erstellt und anschließend die Methoden hover() und click() aufgerufen. Im Beispiel des Locators wird das zugrundeliegende DOM-Element zweimal auf der Seite anhand des Selectors gesucht.

ElementHandle handle = page.querySelector("#myButton");
handle.hover();
handle.click();
 
Locator locator = page.locator("#myButton");
locator.hover();
locator.click();

Unerwartetes Verhalten und Timeouts

Sehen wir uns eine einfache Webseite an, die einen Button mit der ID myButton und dem Attribut text="Subscribe" beinhaltet. Auf die Tabelle kommen wir später zurück.

Element untersuchen gibt uns Auskunft über die Attribute des Buttons

Wir wollen nun zur Verdeutlichung der Unterschiede zwischen ElementHandle und Locator den Button nicht anhand seiner ID selektieren, sondern anhand des Attributs text="Subscribe".

Locator buttonLocator = page.locator("button[text=\"Subscribe\"]");
ElementHandle buttonHandle = page.querySelector("button[text=\"Subscribe\"]");

// Perform some clicks on the button
buttonLocator.click();
buttonHandle.click();
buttonLocator.click();
buttonLocator.click();
buttonHandle.click();
buttonHandle.click();

// change text attribute inside button
page.evalOnSelector("#myButton", "b => b.setAttribute(\"text\", \"Unsubscribe\")");

// we can still perform clicks
buttonHandle.click();
buttonHandle.click();

// this causes a timeout
buttonLocator.click()

Nach sechsmaligen Aufruf der Methode click(), die uns beide Klassen (ElementHandle und Locator) bieten, wollen wir das Attribut des Buttons ändern. Dazu rufen wir page.evalOnSelector auf, und übergeben als Argumente den Selektor des Buttons und den Ausdruck, der ausgeführt werden soll. Nach dem Aufruf ändert Playwright den Wert des Attributs text auf „Unsubscribe“.

Bei den nächsten click()-Aufrufen passiert aber ein wesentlicher Unterschied. ElementHandle verweist wie zuvor auf dasselbe DOM-Element – das Klicken kann also problemlos erfolgen. Beim nächsten buttonLocator.click()-Aufruf versucht Playwright dagegen das DOM-Objekt mit dem Selektor button zu finden, denn der Locator wird bei jeder Verwendung (wie hover() oder click()) auf der Webseite gesucht. Das führt zu einem Timeout, da das Element nicht mehr anhand der vorhin definierten Logik gefunden werden kann!

Die Verwendung von ElementHandles erscheint somit auf ersten Blick naheliegender, in der Praxis ist jedoch die Verwendung von Locatoren viel sinnvoller. Das liegt daran, dass die meisten modernen Webseiten sich sehr häufig dynamisch verändern. Durch die Verwendung von ElementHandles würde man in vielen Fällen ungewollt auf Elemente verweisen, die zum Aktionszeitpunkt gar nicht mehr existieren (sog. Stolen-Elements). Durch Lokatoren suchen wir nach den „richtigen“ Objekten zum Zeitpunkt der Aktion.

Verweis auf dynamische Elemente

Als Nächstes wollen wir den Unterschied anhand einer dynamischen Tabelle zeigen. Angenommen, wir wollen auf die 2. Zeile der Tabelle zugreifen, die den Nachnamen Mustermann enthält. Dafür erstellen wir ein ElementHandle und einen Locator mit Page.querySelector() bzw. Page.locator(), wie bereits bekannt, und geben den Inhalt der DOM-Objekte mit innerText() aus.

Locator secondRowLocator = page.locator("table > tbody > tr:nth-child(2) > td:first-of-type");
ElementHandle secondRowHandle = page.querySelector("table > tbody > tr:nth-child(2) > td:first-of-type");
 
//Prints "Mustermann"
System.out.println("Locator: " + secondRowLocator.innerText());
System.out.println("Handle: " + secondRowHandle.innerText()):

Diese Art von Selektoren nennt man CSS Kombinatoren, mit welchen wir direkt auf die 2. Zeile und 1. Spalte einer Tabelle zugreifen können. Unten ist die HTML-Struktur der Tabelle vorgegeben, um diesen Selektor besser nachvollziehen zu können.

<table id="myTable">
    <!-- HTML fügt automatisch <tbody> ein -->
  <tr style="text-align: center;">  <!-- Erstes <tr>-Kind innerhalb von <tbody>-->
    <td><b>Nachname</b></td> <!-- Erstes Vorkommen eines <td>-Elements innerhalb des <tr>-Knotens -->
    <td><b>Vorname</b></td>   <!-- Zweites Vorkommen eines <td>-Elements -->
  </tr>
  <tr> <!-- Zweites <tr> Kind innerhalb von <tbody> -->
    <td>Mustermann</td> <!-- Erstes Vorkommen eines <td>-Elements innerhalb des ZWEITEN <tr>-Knotens der Tabelle-->
    <td>Max</td>
  </tr>
  <tr>
    <td>Gabler</td>
    <td>Erika</td>
  </tr>
  <!-- HTML schließt hier automatisch tbody mit </tbody> -->
</table>

Anschließend fügen wir mit den untenstehenden Playwright Kommandos 2 weitere Namen hinzu. Beim Drücken der Eingabetaste werden die Namen jeweils an den Anfang der Tabelle angefügt.

page.fill("input[type=\"text\"]", "Max Müller");
page.keyboard().press("Enter");
 
page.fill("input[type=\"text\"]", "Otto Schneider");
page.keyboard.press("Enter");

Das gleiche Verhalten ist wie im Button-Beispiel zu erwarten. secondRowHandle verweist noch auf die ursprüngliche 2. Zeile der Tabelle und secondRowHandle.innerText() gibt uns den Nachnamen Mustermann zurück. Da die Tabelle durch das Einfügen von zwei weiteren Namen gewachsen ist und Playwright bei jedem Methodenaufruf der Locator-Klasse das Objekt auf der Webseite auf das Neue sucht, zeigt natürlich der Selektor table > tbody >tr:nth-child(2) > td:first-of-type auf eine neue, zweite Zeile der Tabelle. Diese beinhaltet den Nachnamen Schneider.

ElementHandles, Locators und Garbage Collection

Wir wissen bereits, dass es eine Zeitüberschreitung geben wird, wenn der Locator nicht mehr zu finden ist. Doch worauf verweist ElementHandle, wenn das zugrundeliegende DOM-Objekt gelöscht wird?

Es stellt sich heraus, dass ElementHandle nicht von der Garbage Collection entsorgt wird, bis das Handle mit dispose() oder automatisch beim Navigieren des ursprünglichen Frames entsorgt wird, z.B. durch Page.navigate().

0 Kommentare

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert