Chaining, filtrowanie i parametry w lokatorach

Prezentacja

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

Różne sposoby szukania elementów i metody w lokatorach

TIP: Cały kod testów z poszczególnych lekcji znajdziesz w specjalnie przygotowanym repozytorium:
👉jaktestowac/playwright-elements-locators

TIP: Ta lekcja jest częścią rozwijanego Programu Testy Automatyczne z Playwright 🎭

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

16 komentarzy

  1. 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” ?

    Avatar Marek Bortkiewicz
    1. 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" }); 
      
      1. 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);
        		});
        
        Avatar Marek Bortkiewicz
  2. 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 🙂

    Avatar Paweł Pietrasiński
    1. 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ć 😉

      Krzysiek Kijas Krzysiek Kijas
  3. 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?

    Avatar Mateusz Sowa
    1. 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 🙂

      Krzysiek Kijas Krzysiek Kijas
  4. 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

    Avatar Andrzej S
  5. 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 🙂

    Avatar Damian Krotowski
    1. 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! 🙇‍♂️

      Krzysiek Kijas Krzysiek Kijas
    1. 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 🙂

      Krzysiek Kijas Krzysiek Kijas
      1. 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 🙂

        Avatar Damian Krotowski
        1. 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 🙂

          Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *