Rozbudowa testów i lokatory

TIP: Ta lekcja jest częścią rozwijanego Programu Testy Automatyczne z Playwright 🎭
UWAGA: Kursanci zgłaszali nam czasem problemy ze stabilnością i działaniem testów podczas testu pulpitu. Objawiały się one np.

  • brakiem wczytania wiadomości potwierdzającej przelew
  • brakiem wypełnienia pola z kwotą i tytułem przelewu

Wynikały one z próby wykonania akcji na jeszcze nie do końca załadowanej stronie.

Powodem tego mogą być:

  • problemy z hostingiem
  • problemy z połączeniem internetowym
  • nie optymalne rozwiązania zastosowane na testowanej stronie
  • zbyt dużą szybkością akcji, jakie wykonuje Playwright

Rozwiązaniem jest dodanie inteligentnego czekania w postaci:

await page.waitForLoadState("domcontentloaded")

Konstrukcja waitForLoadState czeka, aż pojawi się w przeglądarce zdarzenie domcontentloaded, które oznacza, że zawartość strony została w pełni załadowana.

Na nagraniu jej nie używamy, jednak jeśli Twoje testy kończyłyby się niepowodzeniem, to rzuć okiem pod nagranie do sekcji Stabilizacja Testów. Tam umieściliśmy dodatkowe instrukcje, polecenia i kod 😉

Prezentacja

Rozbudowa testów i lokatory

Dodatkowe materiały

Link do testowanej strony

Uzupełnienie wiedzy o HTML i lokatorach

Jeśli nie miałeś styczności z kodem stron internetowych HTML to skorzystaj z lekcji Kod strony HTML oraz Elementy strony w przeglądarce.

Drobne poprawki kodu

Poprawiamy literówkę w pliku login.spec.ts. Zamiast:

unsuccessful login with to short username

powinno być:

unsuccessful login with too short username

Synchronizujemy się z zawartością pliku README.md z repozytorium poprzedniej Lekcji 02:
https://github.com/jaktestowac/playwright_automatyzacja_wprowadzenie/tree/main/S01_wprowadzenie/L02_testy_logowania

Wielokrotne użycie only

Zastosowanie ograniczenia wykonywania testów, w postaci test.only() w kilku testach spowoduje wykonanie wszystkich testów oznaczonych funkcją only.

Dokładnie sprawdzaj wyniki w konsoli przy uruchamianiu testów, czy przypadkiem nie pozostawiłeś gdzieś testu z funkcją only.

Kasujemy nadmiarowe akcje

Pozbywamy się akcji w testach, które nie wpływają na wykonanie testów.

W naszych testach takie dwa kroki:

await page.getByTestId('login-input').click();
await page.getByTestId('login-input').fill('testerLO');

Mogą być zredukowane do:

await page.getByTestId('login-input').fill('testerLO');

Metoda fill() realizuje zarówno focus na elemencie jak i wypełnienie go danymi.

Czyścimy z testu wszystkie nadmiarowe kroki, dzięki czemu tak teraz wyglądają nasze testy:

test('successful login with correct credentials', async ({ page }) => {
	await page.goto('https://demo-bank.vercel.app/');
	await page.getByTestId('login-input').fill('testerLO');
	await page.getByTestId('password-input').fill('10987654');
	await page.getByTestId('login-button').click();

	await expect(page.getByTestId('user-name')).toHaveText('Jan Demobankowy');
});

test('unsuccessful login with too short username', async ({ page }) => {
	await page.goto('https://demo-bank.vercel.app/');
	await page.getByTestId('login-input').fill('tester');
	await page.getByTestId('password-input').click();

	await expect(page.getByTestId('error-login-id')).toHaveText('identyfikator ma min. 8 znaków');
});

test('unsuccessful login with too short password', async ({ page }) => {
	await page.goto('https://demo-bank.vercel.app/');
	await page.getByTestId('login-input').fill('testerLO');
	await page.getByTestId('password-input').fill('1234');
	await page.getByTestId('password-input').blur();

	await expect(page.getByTestId('error-login-password')).toHaveText('hasło ma min. 8 znaków');
});

Testy pulpitu

Tworzymy nowy plik z testami pulpit.spec.ts.

Dodajemy podstawowe struktury do pliku:


import { test, expect } from '@playwright/test';

test.describe('Pulpit tests', () => {

});

Nagrywamy nowy scenariusz według kroków:

  1. Zalogowanie się
  2. Skorzystanie z opcji Szybki przelew
    • do Chuck Demobankowy
    • kwota 150
    • tytułem Przelw środków
  3. Akceptacja okna potwierdzenia
  4. Weryfikacja wiadomości o przelewie

Wklejamy nagrany test do przygotowanego pliku, oraz usuwamy nadmiarowe akcje (czyli np. nadmiarowe kliknięcia w element).

Lokatory

Lokator: jest to sposób w jaki znajdujemy dany element ze strony internetowej.

Selektor: jest to adres danego elementu

TIP: Jeśli nie wiesz jak działają selektory koniecznie zobacz Kod strony HTML oraz Elementy strony w przeglądarce. W tych lekcjach poruszamy również temat atrybutów elementów na stronie oraz szukania elementów po wartości atrybutów 😉

Lokator getByTestId()

Zastosowanie:

await page.getByTestId('login-input').fill('testerLO');

Lokator getByTestId()

Zastosowanie:

await page.getByTestId('login-input').fill('testerLO');

Lokator getByTestId() działa w oparciu o wartość atrybutu zlokalizowanego w kodzie strony data-testid. Przykład elementu z data-testid do wpisywania loginu:


Taki test id musi zostać dodany przez twórców aplikacji do kodu danej strony. Najczęściej ten atrybut jest usuwany przed wprowadzeniem aplikacji na produkcję.
Usunięcie tego atrybutu to jedynie kosmetyczny zabieg w większości przypadków. Jedynie w przypadku bardzo rozbudowanych stron taki atrybut może mieć cząstkowy wpływ na ich wydajność.

Ostatecznie dodawanie i usuwanie takiego atrybutu to konwencja techniczna zależna od realiów danego projektu.

Więcej o usuwaniu takiego atrybutu z punkty widzenia deweloperskiego poczytasz tutaj:
https://dpericich.medium.com/removing-data-test-attributes-from-react-production-dom-5ea4ea018acc

Dodanie takiego atrybutu do kodu strony zazwyczaj jest poprzedzone zespołowym uzgodnieniem konwencji: jak będzie się on nazywał, jak będą wyglądać jego wartości.

W Playwright domyślnie rozpoznawany jest format nazwy data-testId ale można go dowolnie nazwać w kodzie aplikacji i ustawić w playwright.config.ts (klucz testIdAttribute):

// Set custom test id attribute from @playwright/test config:
import { defineConfig } from '@playwright/test';

export default defineConfig({
 use: {
   testIdAttribute: 'data-pw'
 },
});

Więcej o tym czym jest test id poczytasz we wpisach (język angielski):

Oficjalna dokumentacja: https://playwright.dev/docs/locators#locate-by-test-id

Dlaczego warto używać test id: https://medium.com/@automationTest/why-your-development-team-should-use-data-testid-attributes-a83f1ca27ebb

Implementacja automatycznego test id w React: https://www.educative.io/answers/what-is-the-data-testid-attribute-in-testing

Lokator getByRole()

Zastosowanie:

await page.getByRole('button', { name: 'wykonaj' }).click();

getByRole() działa w oparciu o typy elementów i ich cechy.

Przykład elementu związanego z przyciskiem wykonaj:


Selektorem dla tego elementu działającym w oparciu o typ element button i wartość jego tekstu będzie:

getByRole('button', { name: 'wykonaj' })

Lokator locator()

Zastosowanie:

await page.locator('#widget_1_transfer_receiver').selectOption('2');

locator() wykorzystuje między innymi adresy zapisane w formacie CSS.

Przykład elementu z css związanego z wybieraniem odbiorcy przelewu:


Selektorem dla tego elementu działającym w oparciu o wartość atrybutu id będzie:

 #widget_1_transfer_receiver
TIP: W Playwright podczas nagrywania stosowane są lokatory według priorytetu niezawodności zawartym w algorytmach tego frameworka. Dlatego otrzymujemy różne typy lokatorów w nagrywanych testach.

Asercja

Posiadając nagrany kod:

await page.getByRole('link', { name: 'Przelew wykonany! Chuck Demobankowy - 150,00PLN - pizza' }).click();

Można zidentyfikować element:

Przelew wykonany! Chuck Demobankowy - 34,00PLN - pizza

Używając atrybutu id elementu z tekstem, który został nagrany można zbudować taką asercję:

await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Chuck Demobankowy - 150,00PLN - pizza');

Aby upewnić się, że nasz test działa poprawnie, a dokładnie jego asercja warto ją uszkodzić celowo, aby wywołać niepowodzenie testu.

Nazwa testu

quick payment with correct data

Stabilizacja testów

UWAGA: Kursanci zgłaszali nam czasem problemy ze stabilnością i działaniem testów podczas testu pulpitu. Objawiały się one np.

  • brakiem wczytania wiadomości potwierdzającej przelew
  • brakiem wypełnienia pola z kwotą i tytułem przelewu

Wynikały one z próby wykonania akcji na jeszcze nie do końca załadowanej stronie.

Powodem tego mogą być:

  • problemy z hostingiem
  • problemy z połączeniem internetowym
  • nie optymalne rozwiązania zastosowane na testowanej stronie
  • zbyt dużą szybkością akcji, jakie wykonuje Playwright

Rozwiązaniem jest dodanie inteligentnego czekania w postaci:

await page.waitForLoadState("domcontentloaded")

Konstrukcja waitForLoadState czeka, aż pojawi się w przeglądarce zdarzenie domcontentloaded, które oznacza, że zawartość strony została w pełni załadowana.

Jak skorzystać z tej konstrukcji?

W naszym teście pulpitu problem ten może występować, po operacji logowania. Wtedy ładowana jest nowa strona pulpitu.

Dlatego po kliknięciu w przycisk login musimy dodać await page.waitForLoadState("domcontentloaded"):

import { test, expect } from '@playwright/test';


test.describe('Pulpit tests', () => {
  test('quick payment with correct data', async ({ page }) => {
    await page.goto('https://demo-bank.vercel.app/');
    await page.getByTestId('login-input').fill('testerLO');
    await page.getByTestId('password-input').fill('password');
    await page.getByTestId('login-button').click();

    // wait for page to fully load:
    await page.waitForLoadState("domcontentloaded")

    await page.locator('#widget_1_transfer_receiver').selectOption('2');
    await page.locator('#widget_1_transfer_amount').fill('150');
    await page.locator('#widget_1_transfer_title').fill('pizza');

    await page.getByRole('button', { name: 'wykonaj' }).click();
    await page.getByTestId('close-button').click();

    await expect(page.locator('#show_messages')).toHaveText(
      'Przelew wykonany! Chuck Demobankowy - 150,00PLN - pizza'
    );
  });
});

Tego typu konstrukcja może zapewnić nas, że strona w następnym kroku (przy wyborze danych z elementy select) jest w pełni załadowana.

Cały kod

Aktualny kod z lekcji znajdziesz w dedykowanym repozytorium: https://github.com/jaktestowac/playwright_automatyzacja_wprowadzenie.

42 komentarze

  1. Hej, proszę o wyjaśnienie dlaczego w poniższym kodzie:

    import { test, expect } from '@playwright/test';
    
    test.describe('Pulpit tests', () => {
       test.only('test', async ({ page }) => {
        await page.goto('https://demo-bank.vercel.app/');
        await page.getByTestId('login-input').fill('testerLO');
        await page.getByTestId('password-input').fill('10987654');
        await page.getByTestId('login-button').click();
    
        await page.locator('#widget_1_transfer_receiver').selectOption('2');
        await page.locator('#widget_1_transfer_amount').fill('150');
        await page.locator('#widget_1_transfer_title').fill('Zwrot środków');
        
        await page.locator('#execute_btn').click();
        await page.getByTestId('close-button').click();
        await page.getByRole('link', { name: 'Przelew wykonany! Chuck' }).click();
          });
    });
    

    vscode wykrył trzy problemy:
    1) dla linijki “test.describe(‘Pulpit tests’, () => { ” Problem: “TypeError: _test.test.only.describe is not a functionplaywright”

    2) Dla linijki “test.only(‘test’, async ({ page }) => {” Problem : “TypeError: _test.test.oonly is not a functionplaywright”

    3) Dla linijki ” await page.getByRole(‘link’, { name: ‘Przelew wykonany! Chuck’ }).click();
    ” Problem : “SyntaxError: tests\pulpit.spec.ts: Missing semicolon. (16:9)playwright”

    Avatar Zbyszek
    1. Hej Zbyszek!
      Sprawdziłem twój kod i wygląda on na poprawny.
      Wygląda to na słynny problem z wtyczką Playwright i przeładowaniem testów.
      Wystarczy przeładować testy.

      Zrealizujesz to w widoku TESTING (po lewej ikona flaszki), TEST EXPLORER i potem pierwszy przycisk zaokrąglona strzałka (Refresh Tests).

      Mam nadzieję, że to pomoże – daj znać!
      Pozdrawiam

      Przemek Przemek
  2. Panowie muszę Wam dać niestety minusa za tą lekcję. Jako tester automatyzujący (Selenium Java) stwierdzam, że tutaj jest błąd:
    await page.getByRole(‘button’, { name: ‘wykonaj’ }).click();
    Fajnie, że jest taka możliwość, fajnie ze to było pokazane ale nie rozumiem czemu ten element nie został zlokalizowany po ID gdyż ten element takie posiada. Co do ID. To nie ma znaczenia czy to Cypress, PW czy Selenium:

    ID elementów są zazwyczaj unikalne i niezmienne w ramach jednej aplikacji. To oznacza, że testy automatyczne są mniej podatne na zmiany w strukturze DOM, co zwiększa ich stabilność.

    Selektory oparte na ID są zwykle szybkie do przetworzenia przez przeglądarki, ponieważ ID są indeksowane w DOM. Dzięki temu testy mogą działać szybciej.

    Jednoznaczność: ID są unikalne w ramach jednej strony, co eliminuje niejednoznaczność przy wybieraniu elementów. Dzięki temu testy są bardziej precyzyjne i łatwiejsze do debugowania.

    Także jeśli mamy do wyboru ID to wybierajmy to ID.

    Avatar Radek
    1. Hej,
      Dzięki za komentarz i podkreślenie wartości ID w testach!
      Zgadzam się, że powinniśmy lokalizować elementy po ID (albo data-testid). Zazwyczaj ID są unikalne, przez co łatwiej (i szybciej) możemy zlokalizować dany element.

      Na początku bazujemy na kodzie, jaki wygenerował Playwright (czyli na await page.getByRole(‘button’, { name: ‘wykonaj’ }).click(); ) i jest to przykład jak jeszcze można podchodzić do lokalizacji elementów 🙂 Tak jak słusznie wspominasz – nie jest on optymalny bo m.in bazuje na tekście, jaki jest zawarty w elemencie i który może się zmieniać.
      Ciężko mi powiedzieć, dlaczego w tym przypadku, gdy element posiada ID, tamta wersja codegen wygenerowała akurat taki kod do lokalizacji elementu. Codegen jest z każdą wersją poprawiany, dlatego też może w kolejnych wydaniach będzie inaczej (lepiej) generował kod.

      W 27:40 robimy refaktoryzację tego kawałka kodu – i zamieniamy getByRole na znajdowania elementu po ID, czyli na await page.locator('#execute_btn') 🙂

      Również tak jak Przemek wspominał, pracujemy nad dodatkowym materiałem, w którym poruszymy różne praktyki znajdowania elementów (wraz z ich wadami i zaletami).

      Krzysiek Kijas Krzysiek Kijas
      1. Tak, zgadza się Przemek robi refaktor w 27:40 i tutaj dla mnie powinno być powiedziane, że istnieje taka możliwość jak getByRole() ale zostawione już dalej jako ID, a filmie mimo iż zostało zmienione na ID to Przemek wrócił do getByRole() i to był moja uwaga. Tak jak mówię – fajnie, że taka funkcja istnieje bo nie zawsze możemy po ID wyszukać, ale jak mamy ID to zdecydowanie szukajmy po ID i takie podejście propagujmy od początku 😉

        Co do CodeGen to ja tutaj nie mam do Was uwag, bo to jest mechanizm PW na który nie mamy wpływu.

        BTW liczę, że w nowej wersji kursu w nowym kwartale? To będzie już poprawione, bo mam zamiar zakupić kurs 🙂

        Avatar Radek
        1. Zgodzę się, że dobre praktyki mogły być tu bardziej uwypuklone 🙂
          Więcej o tym temacie, wadach i zaletach pokryjemy w materiale dedykowanym lokatorom i selektorom 🙂 Wstępnie planujemy podłączyć go do tego kursu, jednak tu jeszcze jesteśmy na etapie planowania struktury.

          Jeśli wszystko nam się zepnie, to materiał ten pojawi sie jeszcze w tym kwartale. Natomiast kolejne otwarcie pełnego Programu o Automatyzacji z Playwright planujemy jeszcze w tym roku 🙂

          Krzysiek Kijas Krzysiek Kijas
    2. Ja bym polemizowała z tym szukaniem po id. To znaczy również uważam, że jest najszybsze i najskuteczniejsze jeśli takowe posiadamy, ale… nie jest zgodne z zaleceniami z dokumentacji PlayWrighta. Według dokumentacji Playwrightowej powinniśmy najpierw lokalizować po czymś co użytkownik realnie widzi na stronie, a jeśli to nie wystawczy to dopiero wspomagać się innymi możliwościami.

      https://playwright.dev/docs/locators
      https://playwright.dev/docs/other-locators

      Czy mi się to podoba? Raczej nie.. Ale może muszę podejść do tematu z bardziej otwartą głową.

      Avatar Milena Zahorska
      1. Hej,
        Słuszna uwaga 😉 Faktycznie w dokumentacji twórcy zalecają takie podejście (z getBy) jednak przy jego stosowaniu warto rozważyć jego wady i zalety 🙂

        – getBy bywa mało precyzyjny i trzeba dodawać dodatkowe informacje o szukanych elementach
        – uwzględnianie tekstu, który jest wyświetlony w elemencie, podczas szukania elementu może wpływać na niestabilność testów
        – getBy moze miec zastosowanie jesli z jakiegoś powodu nie mamy id ani testid

        Dużo zależy jakie testy piszemy w Playwright. Jeśli są to rozbudowane testy e2e różnych funkcjonalności dla skomplikowanej aplikacji, to raczej nie chcielibyśmy aby test się wysypywał na przycisku, któremu został zmieniony tekst. W takim przypadku naprawa testu może być kosztowna (od analizy błędu, rerun, az po MR i sprawdzenie na CI/CD).
        Przy mniejszych aplikacjach – możemy zaryzykować, jednak musimy mieć na uwadze koszty z naprawą testów lub późniejsza zmianą na ID/data-testid 🙂

        W jednym z dużych projektów, za pomocą Playwrighta testowaliśmy rozbudowane scenariusze e2e. Tam zastosowaliśmy podejście z data-testid (bo takie atrybuty dodawaliśmy również na front-endzie) – dzieki temu mieliśmy mniej pracy przy kosmetycznych aktualizacjach aplikacji (zmiana tekstu, języka etc).
        Style i to co widzi użytkownik sprawdzaliśmy na niższych poziomach – w testach jednostkowych/modułowych front-endu 🙂

        Reasumując – nie ma jednoznacznie dobrego podejścia, które sprawdzi się wszędzie 😉 Warto protestować oba z nich i zobaczyć jakie się z nimi wiążą zyski i koszta 🙂

        Krzysiek Kijas Krzysiek Kijas
  3. Hej,
    Mam drobny problem z asercją w trakcie testu – próbowałem twardej i miękkiej – await w tej asercji nie jest potrzebny ale próbowałem i z awaitem i mam problem ponieważ mimo iż w consoli za pomocą lokatora i odpowiednich indexów tabeli oraz wartości obiektu innerText lub innerHTML nie mogę wyjąć odpowiedniej opcji do porównania
    Mój test:

    test.describe("Pulpit tests", () => {
      test("correct transfer to Michael Scott", async ({ page }) => {
        // Login
        await page.goto("https://demo-bank.vercel.app/");
        await page.getByTestId("login-input").fill("sgfbrghs");
        await page.getByTestId("password-input").fill("sergsger");
        await page.getByTestId("login-button").click();
    
        // Transfer Person preparation
        await page.locator("#widget_1_transfer_receiver").selectOption("3");
        expect.soft(page.locator("#widget_1_transfer_receiver")[0][3].innerText).toEqual('Michael Scott');
    
        // Ammount entering
        await page.locator("#widget_1_transfer_amount").fill("111");
        // Transfer title 
        await page.locator("#widget_1_transfer_title").fill("account transfer");
        // Confirmation button
        await page.getByRole("button", { name: "wykonaj" }).click();
        // Messages
        await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Michael Scott - 111,00PLN - account transfer');
        // Close the pop-up
        await page.getByTestId("close-button").click();
      });
    });
    

    Wydzielony kawałek posiada asercję zostwiłem na miękkiej jednak błąd przy teście w raporcie html’a jest przy zanestowanym indexie w odpowiedzzi – czy macie jakiś pomysł jak to obejść

    Konsola:
    $$("#widget_1_transfer_receiver")[0][3].innerText;
    'Michael Scott'
    Raport HTML:
    await page.locator("#widget_1_transfer_receiver").selectOption("3");
    > 17 |     await expect(page.locator("#widget_1_transfer_receiver")[0][3].innerText).toEqual('Michael Scott');
         |                                                                ^
      18 |     // Ammount entering
      19 |     await page.locator("#widget_1_transfer_amount").fill("111");
    

    Czy macie również z tym kłopot??
    Z góry dziękuję za odpowiedź.

    Pozdrawiam,
    Jakub K

    Avatar Jakub Kruszyński
    1. Hej,
      Dobre pytanie!

      W dużym uproszczeniu – $$ i mechanizm Playwright do znajdowania elementów działa odrobinę inaczej.

      Poniżej rozpisałem 4 sposoby na wykonanie tej asercji. 3 dobre (zaleznie od kontekstu) i sposób 4 – nie do końca zalecany. Pomija on mechanizm auto-wait, który jest wbudowany w Playwright gdy asercję bazujemy na obiekcie lokatora.

      Jeśli mamy kilka elementów i musimy sprawdzić jeden z nich, to musimy wykorzystać konstrukcję element.nth(3), a jeśli chcemy z niego wybrać tekst – to await options.nth(3).innerText() (z await, bo to jest metoda asynchroniczna!)

      Rzuć okiem na poniższy kod – z różnymi opcjami, opisami i asercjami – na konsoli też możesz prześledzić jakie wyniki otrzymasz na poszczególnych etapach tego przykładu 🙂

      test("correct transfer to Michael Scott", async ({ page }) => {
          // Login
          await page.goto("https://demo-bank.vercel.app/");
          await page.getByTestId("login-input").fill("sgfbrghs");
          await page.getByTestId("password-input").fill("sergsger");
          await page.getByTestId("login-button").click();
          // Transfer Person preparation
          await page.locator("#widget_1_transfer_receiver").selectOption("3");
      
          // solution 1: good:
      
          await expect
            .soft(page.locator("#widget_1_transfer_receiver"))
            .toContainText("Michael Scott");
      
          // solution 2: good:
      
          await expect
            .soft(
              page.locator("#uniform-widget_1_transfer_receiver > span:nth-child(1)")
            )
            .toHaveText("Michael Scott");
      
          // solution 3: good:
      
          await expect
            .soft(page.locator("#widget_1_transfer_receiver > option:nth-child(4)"))
            .toHaveText("Michael Scott");
      
          // solution 4: not good - we skip auto-wait mechanism built in construction 'await expect(locator)':
      
          const options = page.locator("#widget_1_transfer_receiver > option");
          const elementNumber3 = options.nth(3); 
          console.log("elementNumber3:", elementNumber3); // returns: Locator@#widget_1_transfer_receiver > option >> nth=3
          const elementNumber3Text = await elementNumber3.innerText();
          console.log("elementNumber3Text:", elementNumber3Text); // returns: Michael Scott
      
          // but we can write it also like this in assertion (but it may be hard to debug!)
          expect.soft(await options.nth(3).innerText()).toEqual("Michael Scott");
      
          // ---------------------------------------------------------------------------
      
          const element = page.locator("#widget_1_transfer_receiver");
          console.log("element:", element);
          console.log(await element.textContent()); // returns all the text from the element
      
          const element0 = page.locator("#widget_1_transfer_receiver")[0];
          console.log("element0:", element0); // returns undefined!
      
          const arrayOfLocators = page.locator("#widget_1_transfer_receiver");
          const elementsCount = await arrayOfLocators.count();
          console.log("elementsCount:", elementsCount); // returns 1 - because there is only one element of that type!
      
          // but:
      
          const arrayOfLocatorsReal = page.locator(
            "#widget_1_transfer_receiver > option"
          );
          const arrayOfLocatorsRealElementsCount = await arrayOfLocatorsReal.count();
          console.log(
            "arrayOfLocatorsRealElementsCount:",
            arrayOfLocatorsRealElementsCount
          ); // returns 4 - because there are 4 options in the select!
      
          // other option is to use nth-child:
      
          const optionWithMichaelScott = page.locator(
            "#widget_1_transfer_receiver > option:nth-child(4)"
          );
          // why 4rth? because it's the 4th option in the select:
          // wybierz odbiorcę przelewu
          // Jan Demobankowy
          // Chuck Demobankowy
          // Michael Scott
          console.log(
            "optionWithMichaelScott textContent:",
            await optionWithMichaelScott.textContent()
          ); // returns Michael Scott
          console.log(
            "optionWithMichaelScott innerText:",
            optionWithMichaelScott.innerText
          ); // return [AsyncFunction: innerText]
      
          // ---------------------------------------------------------------------------
      
          // Amount entering
          await page.locator("#widget_1_transfer_amount").fill("111");
          // Transfer title
          await page.locator("#widget_1_transfer_title").fill("account transfer");
          // Confirmation button
          await page.getByRole("button", { name: "wykonaj" }).click();
          // Messages
          await expect(page.locator("#show_messages")).toHaveText(
            "Przelew wykonany! Michael Scott - 111,00PLN - account transfer"
          );
          // Close the pop-up
          await page.getByTestId("close-button").click();
        });
      
      Krzysiek Kijas Krzysiek Kijas
      1. Hej,
        Pięknie dziękuję za Tipy:
        Rezultaty:
        Jak pierwszy raz puściłem z solucją nr 1 – strony nie miałem otwartej za żadnym razem – to otrzymałem błąd: ale z czasem na kolejnej asercji z prekroczeniej czasu:

        > 25 |     await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Michael Scott - 111,00PLN - account transfer');
                 |                                                  ^
              26 |     // Close the pop-up
              27 |     await page.getByTestId("close-button").click();
              28 |   });
        
        

        ale jak puściłem solucję 2 i potem znów 1nkę to wsio zaczęło przechodzić bez problemu włącznie z pozostałymi solucjami – dziękuję bardzo za różne sposoby rozwiązania problemu super info i wyczerpujące.

        Pozdrawiam i spokojności życzę,
        Jakub K

        Avatar Jakub Kruszyński
        1. Hej,
          Ciesze się, że wszystko śmiga 🙂

          Błąd związany z weryfikacją wiadomosci:

          > 25 |     await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Michael Scott - 111,00PLN - account transfer');
          
          

          to znany błąd – czasem występuje w tej aplikacji. Na razie postanowiliśmy go zostawić bo nie występuje zbyt czesto, a kroki do jego reprodukcji nie są jasne.
          Zatem jeśli test wysypał Ci się tylko na weryfikacji wiadomości, to znaczy, że poprzednie kroki działają poprawnie 🙂

          Krzysiek Kijas Krzysiek Kijas
  4. Hej, wspominacie krótko o dobrej praktyce usuwania test-dataid z aplikacji produkcyjnej. Macie może jakiś ciekawy materiał na ten temat, który możecie podlinkować?

    Avatar Arkadiusz Jemielity
    1. Hej Arek!
      Rozszerzyłem opis tutaj https://jaktestowac.pl/lesson/pw1s01l03/#Lokator_getByTestId() i podlinkowałem jeden artykuł:

      https://dpericich.medium.com/removing-data-test-attributes-from-react-production-dom-5ea4ea018acc

      Warto zauważyć, że sprzątanie po test id jest zależne od technologi użytej w projekcie i często używane są specjalne paczki, które czyszczą kod budowany na produkcję:
      https://www.npmjs.com/package/babel-plugin-jsx-remove-data-test-id

      Czyszczenie więc to bardziej zagadnienie deweloperskie, uzależnione od technologii i związane ze skryptami Ciągłej Integracji.

      Przemek Przemek
  5. Hej,
    Ja miałem taki problem, że test przechodził albo w wersji headed albo z opcją trace włączoną.
    Po spędzonej godzinie i degugingu m.in. logów z API PW, okazało się, że w trybie headless event ‘domcontentloaded’ jest później (różnica kilkadziesiąt milisekund) niż w wersji headed, trace, etc. i powoduje to, że w momencie zamknięcia tego okna z potwierdzeniem zrobienia przelewu, wiadomość nie była wczytywana.

    Pomogło dodanie `page.waitForLoadState(“domcontentloaded”)` zaraz po kliknięciu knefla logowania, a przed krokiem wyboru odbiorcy przelewu.

    Avatar baciek
    1. Hej,
      Dzięki wielkie za zgłoszenie! Przeanalizowałem potencjalne powody, jednak nie udało mi się odkryć głównego. Główną trudnością była niewielka powtarzalność tego błędu. Mogła wystąpić kombinacja poniższych powodów:

      problemy z hostingiem
      problemy z połączeniem internetowym
      nie optymalne rozwiązania zastosowane na testowanej stronie
      zbyt dużą szybkością akcji, jakie wykonuje Playwright

      Zaktualizowałem lekcje i dodałem dodatkowy kod z konstrukcją page.waitForLoadState(“domcontentloaded”), który powinien zaopiekować ten problem 🙂

      Krzysiek Kijas Krzysiek Kijas
        1. Słuszne spostrzeżenie!
          Zarówno w JavaScript, jak i TypeScript, średniki są opcjonalne – mozna, ale nie trzeba ich stosować 😉
          Oba podejścia mają swoje wady i zalety (oraz zwolenników).
          W zależności od projektu mozna wypracować reguły, że np. zawsze je stosujemy, albo nigdy ich nie stosujemy. Najważniejsze jest ujednolicone podejście 🙂

          I w tym przypadku – z racji, żę wszystkie linie maja średnik, a linia z await page.waitForLoadState("domcontentloaded") nie ma średnika – to jest drobny błąd do poprawy 😉

          Krzysiek Kijas Krzysiek Kijas
    1. Pomogło ostatecznie dodanie w configu

       launchOptions: {
            slowMo: 200,
          },
      

      Ale uważam, że to kiepskie rozwiązanie. Może ktoś ma jakieś inne sugestie? Dodawanie wszelkich dodatkowych waitów uważam za złą praktykę 🙁

      Avatar Kojot
      1. Hej,
        To prawda, że waity na sztywno to zła praktyka. W PW jest kilka sposobów na inteligentne czekanie (np. na zadany stan strony await page.waitForLoadState('domcontentloaded'); albo na odpowiedz z API).
        Jednak HTTP ERROR 405 nie powinien mieć miejsca🤔
        Również przed chwilą puszczałem testy i wszystko działało🤔

        Podeślij mi proszę cały spakowany projekt na mail kontaktowy (znajdziesz go po prawej stronie w sekcji Znajdź nas lub na stronie /kontakt).
        Dodaj też proszę dokładne logi jak wygląda błąd i co jest w konsoli.

        Rzucę na niego okiem i spróbuje odnaleźć przyczynę tej 405 🙂

        Krzysiek Kijas Krzysiek Kijas
  6. Czy jest możliwe ze Playwright próbje działać zbyt szybko? Próbowałem uruchamiać u siebie kod dokładnie taki jak na githubie i za każdym razem test failuje. Kiedy uruchamiam to w trybie headed to wygląda to tak, jakby wybór z dropdownu był za wolny i pozostałe dwa pola z kwotą i tutułem przelewu pozostają nie wypełnione.

    Kiedy dodałem sztywny timeout po wyborze dropdownu

     await page.locator('#widget_1_transfer_receiver').selectOption('2');
            await page.waitForTimeout(100);
            await page.locator('#widget_1_transfer_amount').fill('120');
    

    test przechodzi bez problemów

    Avatar Grzesiek
      1. Dostaję błąd dokłądnie taki jak dostał Przemek w 33 min nagrania.

        Error: Timed out 5000ms waiting for expect(received).toHaveText(expected)

        Expected string: “Przelew wykonany! Chuck Demobankowy – 120,00PLN – pizza”
        Received string: “Brak wiadomości”
        Call log:
        – expect.toHaveText with timeout 5000ms
        – waiting for locator(‘#show_messages’)
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”
        – locator resolved to Brak wiadomości
        – unexpected value “Brak wiadomości”

        Tylko, że u mnie ten błąd bez dodatkowego timeoutu występuje cały czas.

        Próbowałem uruchomić test w trybie UI i tutaj nawet bez dodatkowego timeoutu błąd się nie pojawia i test przechodzi za każdym razem. Natomiast w trybie headed i headless bez timeoutu błąd jest za każdym razem.

        Avatar Grzesiek
        1. Hm podejrzane 🤔
          Puszczałem przed chwilą testy z najnowszymi paczkami (Playwright 1.38.1) na 2 różnych konfiguracjach (Win10 i Win11) i w różnych lokalizacjach – we wszystkich przypadkach problem nie występował 🤔 Dodatkowo puszczałem testy w pętli, ale również problem nie wystąpił:

          npx playwright test  --repeat-each=20
          

          Zamiast sztywnego czekania, możesz spróbować jeszcze dodać:

          
              await page.waitForLoadState('domcontentloaded');
              
          

          Jest to inteligentne czekanie, aż dokument się załaduje 🙂

          Jeśli to nie pomoże – podeślij mi proszę cały spakowany projekt na maila (znajdziesz go po prawej stronie w sekcji Znajdź nas lub na stronie /kontakt) – to rzuce na niego okiem i spróbuje przeanalizować przyczynę tego błędu.

          Krzysiek Kijas Krzysiek Kijas
  7. Hej,

    * Maximum time expect() should wait for the condition to be met.
    * For example in `await expect(locator).toHaveText();`
    */
    timeout: 500
    

    można znaleźć w playwright.config.ts w sekcji expect – rzuć okiem do naszego repo: https://github.com/jaktestowac/playwright_automatyzacja_wprowadzenie/blob/main/S01_wprowadzenie/L02_testy_logowania/playwright.config.ts

    Później podczas testów ten timeout jest brany pod uwagę przy asercjach expect jako czas czekania na elementy 🙂

    Krzysiek Kijas Krzysiek Kijas
  8. Test timeout of 30000ms exceeded.
    locator.click: Target closed
    =========================== logs ===========================
    waiting for getByTestId('close-button')
    ============================================================
    
      14 |
      15 |       await page.getByRole('button', { name: 'wykonaj' }).click();
    > 16 |       await page.getByTestId('close-button').click();
         |                                              ^
      17 |       
      18 |
      19 |       await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Chuck Demobankowy - 100,25PLN - Test payment')
    
        at /Users/wojtek/Desktop/projects/playWrightTest/tests/pulpit.spec.ts:16:46
    Pending operations:
      - locator.click at tests/pulpit.spec.ts:16:46
    
    Avatar Wojciech Błądek
  9. expect.toHaveText with timeout 30000ms
          - waiting for locator('#show_messages')
          -   locator resolved to Przelew wykonany! Chuck Demobankowy - 150PLN - pi…
          -   unexpected value "Przelew wykonany! Chuck Demobankowy - 150PLN - pizza"
          - waiting for locator('#show_messages')
          -   locator resolved to Przelew wykonany! Chuck Demobankowy - 150PLN - pi…
          -   unexpected value "Przelew wykonany! Chuck Demobankowy - 150PLN - pizza"
          -   locator resolved to Brak wiadomości
          -   unexpected value "Brak wiadomości"
          -   locator resolved to Brak wiadomości
          -   unexpected value "Brak wiadomości"
          -   locator resolved to Brak wiadomości
    
    Avatar Wojciech Błądek
      1. Hej,
        Wygląda na to, że dany napis z jakiegoś powodu jest ucięty na testowanej stronie.

        Jak wygląda Twój kod testów?
        Czy masz poprawnie skonstruowaną asercję lub wyszukiwanie elementu?

        Daj mi proszę znać, jak to u Ciebie wygląda, a znajdziemy rozwiązanie 🙂

        Krzysiek Kijas Krzysiek Kijas
        1. test.describe('Pulpit tests', () => {
              
              test.only('quick payment with correct data', async ({ page }) => {
                await page.goto('https://demo-bank.vercel.app/');
                await page.getByTestId('login-input').fill('testerLO');
                await page.getByTestId('password-input').fill('password');
                await page.getByTestId('login-button').click();
                
                await page.locator('#widget_1_transfer_receiver').selectOption('2');
                await page.locator('#widget_1_transfer_amount').fill('100,25');
                await page.locator('#widget_1_transfer_title').fill('Test payment');
          
                await page.getByRole('button', { name: 'wykonaj' }).click();
                await page.getByTestId('close-button').click();
                
                await expect(page.locator('#show_messages')).toHaveText('Przelew wykonany! Chuck Demobankowy - 100,25PLN - Test payment')
              });
          });
          
          Avatar Wojciech Błądek
          1. Błąd wskazuje, że dane, które są używane w teście (selektory i wartości) się nie zgadzają z oczekiwanymi wynikami.
            Sam test wygląda poprawnie i gdy puściłem go u siebie, to zakończył się powodzeniem. Aczkolwiek nie był to kod, z którego wysłałeś poprzedni błąd, który dotyczył przelewu o tytule pizza – bez aktualnie używanego przez Ciebie kodu, ciężko będzie wskazać powód tego błędu 🙂

            Również jak wygląda cała informacja w konsoli?
            Powyżej wiadomości, którą wysłałeś w pierwszej wiadomości, powinny być też takie wpisy:

            Error: expect(received).toHaveText(expected)

            Expected string: “Błędny oczekiwany napis”
            Received string: “Przelew wykonany! Chuck Demobankowy – 100,25PLN – Test pizza”

            Jak wygląda testowana strona w trakcie testu? Tutaj możesz użyć polecenia
            npx playwright test --headed

            Krzysiek Kijas Krzysiek Kijas
            1. Zrobilem maly refaktor swojego kodu i test teraz przechodzi pozytywnie, czy moge cos w nim usprawnic?

              import { test, expect } from '@playwright/test';
              
              
              
              const randomRange = (myMin: number, myMax: number) => Math.floor(Math.random() * (myMax - myMin + 1)) + myMin
              const amount = '2150'
              const title = 'Test payment'
              
              
              test.describe('Pulpit tests', () => {
              
                  test.only('quick payment with correct data', async ({ page }) => {
                      await page.goto('/');
                      await page.getByTestId('login-input').fill('testerLO');
                      await page.getByTestId('password-input').fill('password');
                      await page.getByTestId('login-button').click();
              
                      await page.locator('#widget_1_transfer_receiver').selectOption(`${randomRange(1, 3)}`);
                      let name = await page.locator('#uniform-widget_1_transfer_receiver > span').textContent()
              
                      
                      await page.locator('#widget_1_transfer_amount').fill(amount);
                      await page.locator('#widget_1_transfer_title').fill(title);
              
                      await page.getByRole('button', { name: 'wykonaj' }).click();
                      await page.getByTestId('close-button').click();
              
              
                      await expect(page.locator('#show_messages')).toHaveText(`Przelew wykonany! ${name} - ${amount},00PLN - ${title}`)
                  });
              });
              
              
              Avatar Wojciech Błądek
              1. Doskonale, że już wszystko działa 😉 Czy udało Ci sie zlokalizować i okreslić wcześniejszy problem?

                Jesli chodzi o dalsze usprawnienia – to pokażemy je w kolejnych lekcjach, które udostępnimy w najbliższych dniach – tam pokażemy zmniejszenie duplikacji kodu, wydzielenie wartości do zmiennych (które tu wykonałeś) oraz praktykę AAA czyli Arrange Act Assert. W dalszych lekcjach pokażemy jak zastosować wzorzec POM w tym projekcie 🙂

                Krzysiek Kijas Krzysiek Kijas
                1. This page isn’t workingIf the problem continues, contact the site owner.
                  HTTP ERROR 405
                  
                  import { test, expect } from '@playwright/test';
                  import { logToApp } from '../helpers/logToApp';
                  
                  
                  const randomRange = (myMin: number, myMax: number) => Math.floor(Math.random() * (myMax - myMin + 1)) + myMin
                  const amount = '2150'
                  const title = 'Test payment'
                  
                  test.describe('Pulpit tests', () => {
                  
                      test.only('quick payment with correct data', async ({ page }) => {
                          await page.goto('/');
                          await logToApp(page);
                  
                          await page.locator('#widget_1_transfer_receiver').selectOption(`${randomRange(1, 3)}`);
                          // let name = await page.locator('#uniform-widget_1_transfer_receiver > span').textContent()
                  
                  
                          await page.locator('#widget_1_transfer_amount').fill(amount);
                          await page.locator('#widget_1_transfer_title').fill(title);
                  
                          await page.getByRole('button', { name: 'wykonaj' }).click();
                          await page.getByTestId('close-button').click();
                  
                          // await expect(page.locator('#show_messages')).toHaveText(`Przelew wykonany! ${name} - ${amount},00PLN - ${title}`)
                  
                          let informationText = await page.locator('#show_messages').textContent()
                  
                  
                          await expect(page.locator('#show_messages')).toHaveText(`${informationText}`)
                      });
                  });
                  

                  Spotykam kolejny problem, gdy chce wyswietlic kontent z selektora #show_messages, dostaje error z strony

                  This page isn’t workingIf the problem continues, contact the site owner.
                  HTTP ERROR 405 - The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed response status code indicates that the server knows the request method, but the target resource doesn't support this method.
                  

                  jezeli odkomentuje kod I usune te dwie linie

                          let informationText = await page.locator('#show_messages').textContent()
                          await expect(page.locator('#show_messages')).toHaveText(`${informationText}`) 
                  

                  to test przechodzi pozytywnie co tu moze byc nie take czy blad po waszej stronie?

                  Avatar Wojciech Błądek
                  1. Sprawdziłem po naszej stronie – Twój test działa poprawnie (musiałem tylko zakomentować logToApp):

                    import { test, expect } from '@playwright/test';
                    // import { logToApp } from '../helpers/logToApp';
                    
                    
                    const randomRange = (myMin: number, myMax: number) => Math.floor(Math.random() * (myMax - myMin + 1)) + myMin
                    const amount = '2150'
                    const title = 'Test payment'
                    
                    test.describe('Pulpit tests', () => {
                    
                        test.only('quick payment with correct data', async ({ page }) => {
                            const url = 'https://demo-bank.vercel.app/'
                            const userId = 'testerLO'
                            const userPassword= '10987654'
                      
                            // await logToApp(page);
                            await page.goto(url);
                            await page.getByTestId('login-input').fill(userId);
                            await page.getByTestId('password-input').fill(userPassword);
                            await page.getByTestId('login-button').click();
                    
                            await page.locator('#widget_1_transfer_receiver').selectOption(`${randomRange(1, 3)}`);
                            // let name = await page.locator('#uniform-widget_1_transfer_receiver > span').textContent()
                    
                    
                            await page.locator('#widget_1_transfer_amount').fill(amount);
                            await page.locator('#widget_1_transfer_title').fill(title);
                    
                            await page.getByRole('button', { name: 'wykonaj' }).click();
                            await page.getByTestId('close-button').click();
                    
                            // await expect(page.locator('#show_messages')).toHaveText(`Przelew wykonany! ${name} - ${amount},00PLN - ${title}`)
                    
                            let informationText = await page.locator('#show_messages').textContent()
                    
                    
                            await expect(page.locator('#show_messages')).toHaveText(`${informationText}`)
                        });
                    });
                    

                    Jaki wynik otrzymujesz, gdy wykonasz go manualnie?
                    Jak wygląda wynik gdy uruchamiasz testy z opcją –headed?

                    Krzysiek Kijas Krzysiek Kijas
                    1. Hej tak jak powyżej przesłałem jak użyje testy z headed ,

                      otrzymuje kod z przeglądarki

                      This page isn’t workingIf the problem continues, contact the site owner.
                      HTTP ERROR 405

                      Avatar Wojciech
                  2. Jeśli możesz spakuj pliki z kodem (bez folderu node_modules) i podeślij do nas na maila kontakt (małpa) jaktestowac.pl Widząc cały kod i ustawienia będziemy mogli pomóc. Pozdro

                    Przemek Barański Przemek Barański
                  3. Miałam te same błędy. Musiałam zwiększyć czas w playwright.config.ts . Dokładnie w tym miejscu:
                    * Maximum time expect() should wait for the condition to be met.
                    * For example in `await expect(locator).toHaveText();`
                    */
                    timeout: 6000
                    Wcześniej miałam na 5000 i testy nie przechodziły, być może internet nie był wystarczająco szybki.

                    Avatar Pixma
                    1. Może się tak zdarzyć, gdy aplikacja działa odrobinę wolniej w danym momencie.
                      Dodaliśmy stosowne informacje w pierwszych lekcjach 🙂

                      Krzysiek Kijas Krzysiek Kijas
                    2. gdzie w playwright.config.ts można znaleźć? trzeba wejść gdzieś do powiązanych plików?
                      * Maximum time expect() should wait for the condition to be met.
                      * For example in `await expect(locator).toHaveText();`
                      */
                      timeout: 500

                      Avatar Karolina Zakrzewska

Dodaj komentarz

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