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










A co jeśli chcemy dostać do wiersza w oparciu o więcej niż jedna wartość np w sytuacji wyświetlania grida z pogrupowaniem wierszy . Stosuje się jakieś “and” bądź “or” ?
Hej,
Aktualnie w Playwright nie ma operatorów AND i OR, ale mozna to zrealizować w prosty sposób 🙂
Kombinacje wielu filtrów (efekt AND), czyli jeśli chcesz znaleźć element, który spełnia kilka warunków naraz, możesz po prostu łańcuchowo stosować .filter() i każde kolejne zawęża wynik poprzedniego:
const row = page .getByRole("row") .filter({ hasText: "Group A" }) // pierwszy warunek .filter({ hasText: "Row 3" }); // drugi warunek (AND)albo:
const row = page .getByRole("row", { has: page.getByText("Row 3") }) .filter({ has: page.getByText("Group A") });Realizacje OR (jeden z warunków) możesz zaimplementowac:
1. Przez łączenie lokatorów i wybieranie pierwszego pasującego:
const button = page.locator('button:has-text("Save"), button:has-text("Submit")'); [/j] 2. Dwa warunki i 2 grupy elementów: [js] const elements1 = page .getByRole("row") .filter({ hasText: "Group A" }) const elements2 = page .getByRole("row") .filter({ hasText: "Row 3" });Coś takiego do kombinacji warunków:
************************************************************* test("Single button click (using filter by child - combining)", async ({ page }) => { // Arrange: const elementRole = "row"; const firstSiblingValue = "Row"; const secondSiblingValue = "X"; const thirdSiblingValue = "Z"; const expectedFirstCount = 1; const expectedSecondCount = 4; const expectedThirdCount = 2; // Combining row='Row' AND col='Z' let elementLocator = page .getByRole(elementRole) .filter({ has: page.getByText(firstSiblingValue) }) .and( page .getByRole(elementRole) .filter({ has: page.getByText(thirdSiblingValue, { exact: true }), }), ); await elementLocator.highlight(); // Combining row='Row' OR col='Z' console.log("elementLocator - 'and':", await elementLocator.count()); expect(await elementLocator.count()).toBe(expectedFirstCount); elementLocator = page .getByRole(elementRole) .filter({ has: page.getByText(firstSiblingValue) }) .or( page .getByRole(elementRole) .filter({ has: page.getByText(thirdSiblingValue, { exact: true }), }), ); await elementLocator.highlight(); console.log("elementLocator - 'or':", await elementLocator.count()); expect(await elementLocator.count()).toBe(expectedSecondCount); // Combining row='Row' AND (col='Z' OR col='X') elementLocator = page .getByRole(elementRole) .filter({ has: page.getByText(firstSiblingValue) }) .and( page .getByRole(elementRole) .filter({ has: page.getByText(thirdSiblingValue, { exact: true }) }) .or( page .getByRole(elementRole) .filter({ has: page.getByText(secondSiblingValue, { exact: true }), }), ), ); await elementLocator.highlight(); console.log( "elementLocator - 'and' and 'or':", await elementLocator.count(), ); expect(await elementLocator.count()).toBe(expectedThirdCount); });Cześć :).
Mam pytanie do tej lekcji. Czy gdzieś w kursie realizowany jest podobny przypadek z chainingiem i filtrowaniem po elementach, ale przy użyciu wzroca POM?
Mam w projekcie sytuację, która bardzo pasuje do takiego rozwiązania.
Sprawdzam elementu, które wewnątrz, mają wiele innych elementów. Elementów wewnętrznych, szukam więc tylko wewnątrz większego elementu. Co prawda udało mi się to zaimplementować, ale czasami pojawiają się błędy (nie zawsze), kiedy chcę np z wewnętrznych elementów coś pobrać.
Jeżeli jest taka lekcja to byłoby mi bardzo miło 🙂
Hej!
Na szybko przejrzałem i nie znalazłem bardziej zaawansowanego przypadku 🤔
Ogólnie filtrowanie w POM będzie wyglądało tak samo jak tutaj – tylko w POM moze to być na poziomie konstruktora klasy lub w metodach.
Na jakie błędy natrafiasz?
Masz moze kawałek kodu (lub pseudo kod) który obrazowałby Twój przypadek? 🙂
Dzięki temu łatwiej nam będzie go przeanalizować 😉
Cześć,
po obejrzeniu tego video mam pewną zagwozdkę. Tworzę w firmie framework dla aplikacji web. Praktycznie każda strona jest podzielona na sekcje i zastanawiam się jakie będzie najbardziej optymalne podejście do lokalizowania elementów na stronie (najlepiej utrzymywane w dużej perspektywie czasu). Dodam że nie mamy żadnych id elementów :(.
1) Lokalizowanie elementu jako osobny byt,
2) Lokalizowanie elementu w taki sposób że pierw zlokalizuje sekcje, a później za pomocą chaining element w tej sekcji.
Głównie chodzi o utrzymywalność np. w przypadku jakiś zmian w DOM. Bo jak tak sobie w głowie to układam to fajnie mieć ustrukturyzowane lokatory z jakimś podziałem na sekcje w kodzie, ale zastanawiam się jaki jest to narzut pracy w debugowaniu i naprawie w przypadku zmiany w strukturze DOM, która zmieniła nam lokator sekcji głównej (teraz jak to piszę to w sumie wychodzi mi na to że niewielki bo trzeba po prostu zmieniać jeden lokator (sekcji), ale może jest coś jeszcze? Które podejście jest lepszą praktyką w tworzeniu lokatorów?
Jeśli dobrze zrozumiałem, to sugerowałbym najpierw lokalizować sekcje – i na takich lokatorach później działabym w kodzie. Czyli:
const sectionA = page.locator(".section-a") // w kolejnych metodach: const subsectionA = sectionA.locator('.subsection-a') const subsectionB = sectionA.locator('.subsection-b') // w kolejnych metodach: const buttonA = subsectionB.locator('.button-a')Kierowałbym się w tą stronę o której słusznie wspomniałeś – czyli jesli coś sie zmienić w DOM, to aby była potrzebna zmiana w jednym miejscu.
Tutaj bazuje na bardzo ogólnej koncepcji aplikacji. Możesz przygotować przykładowego POCa – wtedy mozemy na nim omówić kwestie podejść i ich wady i zalety 🙂
Bardzo pomocna lekcja, szczególnie spodobał się narastający format podania materiału 😉 istny spacer po schodach. Oglądając materiały z tego działu zastanawiam się dlaczego tak późno je odkryłem
Dzięki wielkie! 😁
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
nthpotrzebujemy 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
nthbardzo 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
nthNatomiast, 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” 🙂