Powrót do: Playwright Elements – Kluczowe koncepcje automatyzacji testów
Chaining, filtrowanie i parametry w lokatorach
Prezentacja
👉jaktestowac/playwright-elements-locators
Parametry w metodach getBy
Dodatkowe parametry w metodach getBy
pozwalają lepiej sprecyzować i określić szukany element.
Przykładowo, poza rolą możemy jeszcze określić tekst, jaki powinien znajdować się na elemencie:
const elementRole = "button"; const elementText = "Click!"; const buttonLocator = page.getByRole(elementRole, { name: elementText });
Filtrowanie lokatorów
Filtrowanie lokatorów to sposób na wybieranie konkretnych elementów z kilku dostępnych na stronie.
Na przykład, możesz wybrać przyciski lub tekst, które zawierają określone słowa, lub które mają wewnątrz określone elementy (dzieci). Dzięki temu możesz łatwo znaleźć i np. kliknąć właściwy przycisk lub element na stronie.
Przykład filtrowania elementów button po tekście:
test("Single button click (using filter)", async ({ page }) => { const role = "button"; // we can define the locator for the element const buttonLocator = page.getByRole(role).filter({ hasText: "Click me!" }); // print the count of buttons on the page console.log("buttonLocator", await buttonLocator.count()); // check if number of buttons is 1 await expect(buttonLocator).toHaveCount(1); // click the button await buttonLocator.click(); // check if the button is clicked const resultsLocator = page.getByTestId("dti-results"); await expect(resultsLocator).toHaveText("You clicked the button!"); });
Pełna dokumentacja z przykładami: Lokatory i filtry
Łączenie lokatorów (chaining)
Łączenie lokatorów (chaining) jest używane do bardziej precyzyjnego wyszukiwania elementów na stronie.
Łączenie lokatorów pozwala zawężać miejsce, w którym znajduje się szukany przez nas element.
Najpierw wyszukujemy wiersz, który zawiera dany tekst, a następnie w znalezionym wierszu – znajdujemy przycisk:
const rowLocator = page.getByRole("row", { name: "Row 2" }) const buttonLocator = rowLocator.getByRole("button", { name: "Click!" });
Co można też napisać w krótszy sposób:
const buttonLocator = page .getByRole("row", { name: "Row 2" }) .getByRole("button", { name: "Click!" });
Cały test może mieć następującą postać:
test("Single button click (chained getBy)", async ({ page }) => { const buttonLocator = page .getByRole("row", { name: "Row 2" }) .getByRole("button", { name: "Click!" }); // check if number of buttons is 1 await expect(buttonLocator).toHaveCount(1); // click the button await buttonLocator.click(); // check if the button is clicked const resultsLocator = page.getByTestId("dti-results"); await expect(resultsLocator).toHaveText( "You clicked the button! (row 2)" ); });
Pełna dokumentacja z przykładami: Lokatory i operatory.
Kod do lekcji
Początkowy kod (fragmenty)
Początkowa zawartość pliku locator-filters-operations.spec.ts:
import { test, expect } from "@playwright/test"; test.describe("Locator filters", () => { test.beforeEach(async ({ page }) => { await page.goto("/practice/simple-multiple-elements-no-ids.html"); }); test.describe("Finding element - different approaches", () => { test("Single button click using options", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); test("Single button click (using filter and hasText)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); }); });
Kolejna sekcja z testami do locator-filters-operations.spec.ts:
test.describe("Buttons in table - different approaches", () => { test("Single button click (chained getBy)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); test("Single button click (using filter and has)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); });
Cały początkowy kod
Początkowa zawartość pliku locator-filters-operations.spec.ts:
import { test, expect } from "@playwright/test"; test.describe("Locator filters", () => { test.beforeEach(async ({ page }) => { await page.goto("/practice/simple-multiple-elements-no-ids.html"); }); test.describe("Finding element - different approaches", () => { test("Single button click using options", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); test("Single button click (using filter and hasText)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); }); test.describe("Buttons in table - different approaches", () => { test("Single button click (chained getBy)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); test("Single button click (using filter and has)", async ({ page }) => { // TODO: // Arrange: // Act: // Assert: }); }); });
Finalny kod
Finalna zawartość pliku locator-filters-operations.spec.ts:
import { test, expect } from "@playwright/test"; test.describe("Locator filters", () => { // https://playwright.dev/docs/locators#locator-operators test.beforeEach(async ({ page }) => { await page.goto("/practice/simple-multiple-elements-no-ids.html"); }); test.describe("Finding element - different approaches", () => { test("Single button click using options", async ({ page }) => { // Arrange: const elementRole = "button"; const elementText = "Click me!"; const expectedMessage = "You clicked the button!"; const resultsTestId = "dti-results"; // we can define the locators for the element const buttonLocator = page.getByRole(elementRole, { name: elementText }); const resultsLocator = page.getByTestId(resultsTestId); // print the count of buttons on the page console.log("buttonLocator", await buttonLocator.count()); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); }); test("Single button click (using filter and hasText)", async ({ page }) => { // Arrange: const elementRole = "button"; const elementText = "Click me!"; const expectedMessage = "You clicked the button!"; const resultsTestId = "dti-results"; const buttonLocator = page .getByRole(elementRole) .filter({ hasText: elementText }); const resultsLocator = page.getByTestId(resultsTestId); // print the count of buttons on the page console.log("buttonLocator", await buttonLocator.count()); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); }); }); test.describe("Buttons in table - different approaches", () => { test("Single button click (chained getBy)", async ({ page }) => { // Arrange: const elementRole = "button"; const elementText = "Click!"; const parentRole = "row"; const parentText = "Row 2!"; const expectedMessage = "You clicked the button! (row 2)"; const resultsTestId = "dti-results"; const resultsLocator = page.getByTestId(resultsTestId); const buttonLocator = page .getByRole(parentRole, { name: parentText }) .getByRole(elementRole, { name: elementText }); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); }); test("Single button click (using filter and hasText)", async ({ page }) => { // Arrange: const elementRole = "button"; const elementText = "Click here!"; const expectedMessage = "You clicked the button! (Third one!)"; const resultsTestId = "dti-results"; const resultsLocator = page.getByTestId(resultsTestId); const buttonLocator = page.getByRole(elementRole).filter({ hasText: elementText, }); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); }); }); });
Bonusowy test
Wykorzystanie metod getBy i filter:
test("Single button click (using filter and has)", async ({ page }) => { // Arrange: const elementRole = "button"; const parentRole = "row"; const siblingText = "Row 2"; const expectedMessage = "You clicked the button! (row 2)"; const resultsTestId = "dti-results"; const buttonLocator = page .getByRole(parentRole) .filter({ has: page.getByText(siblingText) }) .getByRole(elementRole); const resultsLocator = page.getByTestId(resultsTestId); // print the count of buttons on the page console.log("buttonLocator", await buttonLocator.count()); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); }); test("Single button click (using filter and hasText)", async ({ page }) => { // Arrange: const elementRole = "button"; const elementText = "Click here!"; const expectedMessage = "You clicked the button! (Third one!)"; const resultsTestId = "dti-results"; const resultsLocator = page.getByTestId(resultsTestId); const buttonLocator = page.getByRole(elementRole).filter({ hasText: elementText, }); // Act: await buttonLocator.click(); // Assert: await expect(resultsLocator).toHaveText(expectedMessage); });
Zewnętrzne linki i zasoby
- Nasza aplikacja 🦎GAD do testów: Aplikacja do testów – gdzie będziemy testować koncepty automatyzacji? lekcja o pobraniu, instalacji i uruchomieniu
- Oficjalnym repozytorium: GitHub / 🦎GAD
- Mini kurs, w którym opowiadamy szczegółowo o naszej aplikacji do testów: GAD – poznaj naszą autorską aplikację do nauki automatyzacji (🔒Tylko dla członków Programu Automatyzacja z Playwright)
- Oficjalna dokumentacja: Lokatory i operatory
- Oficjalna dokumentacja: Lokatory i metody
- Oficjalna dokumentacja: Lokatory i filtry
Dokładnie taka praktykę mamy, nie robimy testID do wszystkich elementów, tylko do tych “sekcji”, chyba, ze gdzieś cos potrzebujemy dodatkowo to wiadomo 🙂 Tak, przerobiłem już ten materiał, fajnie porządkuje wiedze 🙂
Najważniejsze, że taka praktyka się u Was sprawdza 😉
A zawsze tak jak piszesz – można dodawać ID jak potrzeba.
Dzięki wielkie i mega się ciesze, że Ci się podoba! 🙇♂️
Hej, pytanie co jest według was lepsze, korzystanie z filtrów, czy może złapać button jako nth?
Hej,
Bardzo dobre pytanie!
Z doświadczenia, częściej bazuje na filtrach, niż na nth.
Chociaż to zależy troche od kontekstu.
Do
nth
potrzebujemy konkretnego indexu, który może się łatwo zmieniać (np. gdy chcemy nacisnąć piąty (5.) przycisk, a na stronie zajdą zmiany i zostanie dodany kolejny przycisk i nasz przycisk będzie pod indexem 6).Metoda
nth
bardzo się przydaje, gdy chcemy przejść przez wszystkie znalezione elementy i kolejno chcemy wykonać na nich takie same akcje.Przykład z życia:
– tworzenie użytkownika, któremu przypisujemy role
– role przypisujemy zaznaczając checkboxy
– w naszym teście chcemy użytkownikowi zawsze przypisać wszystkie role.
Wtedy możliwe ze najlepszym rozwiązaniem będzie właśnie
nth
Natomiast, gdy chcemy precyzyjnie wyszukać jeden element – to metoda filter może być odpowiedniejsza 🙂
W skrajnych przypadkach może się okazać, że ciężko znaleźć jeden element za pomocą metody filter, i jedyną opcją jest metoda
nth
.Wtedy tymczasowo zastosowałbym takie rozwiązanie, ale dodając 2 kroki:
– oznaczając to miejse jako dług techniczny
– dodając zadanie na dodanie na font-endzie ID/test ID aby precyzyjnie wyszukać dany element 🙂
Jasne rozumiem, ja natomiast często spotykam się z sytuacja gdzie nazwa np przycisku jest zmieniana i w moim przypadku częściej problematyczne byłoby szukanie po nazwie. W zespole mamy ustalone, ze strona jest podzielona na różne “sekcje” które mają swoje test ID i w takim wypadku szukanie po nth np button wychodzi stabilniej. Ale w sumie jest tak jak piszesz i chyba nie ma na to złotego środka 🙂
Pytanie czy każda z tych sekcji ma jakieś swoje unikalne ID, atrybut lub nazwę?
Bo jesli tak – to najpierw możemy złapać całą sekcję (za pomocą jakiegoś selektora/getBy), a później tylko w jej obrębie możemy szukać elementu, który nas interesuje 🙂
W sumie coś podobnego realizujemy w Bonus: Szukanie elementów w zagnieżdżonych tabelach https://jaktestowac.pl/lesson/pw5s01l14/ – tam “tniemy” stronę na fragmenty aby wyszukać elementy własnie w tych “fragmentach” 🙂