POM i komponenty, czyli wspólne elementy

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

Prezentacja

POM i komponenty, czyli wspólne elementy

POM i komponenty, czyli wspólne elementy

Dodatkowe materiały

Bazujemy na kodzie lekcji L06_pom_refactor

Kod wynikowy tej lekcji znajduje się tu: L07_pom_component

Pamiętaj, aby po danej porcji pracy: uruchamiać test, commitować poprawnie działający kod 😉

TIP: Zauważ, że do wspólnego komponentu możemy też przenieść element wyświetlający nazwę użytkownika, która również jest częścią panelu bocznego 😉

Zawartość side-menu.component.ts:

export class SideMenuComponent {
    constructor(private page) { }


    paymentLink = this.page.getByRole('link', { name: 'płatności' })
}

Zawartość pulpit.page.ts oraz wykorzystanie SideMenuComponent:

import { Page } from '@playwright/test';
import { SideMenuComponent } from '../component/side-menu.component';


export class PulpitPage {
  constructor(private page: Page) {}


  sideMenuComponent = new SideMenuComponent(this.page)


  transferReceiverInput = this.page.locator('#widget_1_transfer_receiver');
  transferAmountInput = this.page.locator('#widget_1_transfer_amount');
  transferTitleInput = this.page.locator('#widget_1_transfer_title');


  transferButton = this.page.getByRole('button', { name: 'wykonaj' });
  actionCloseButton = this.page.getByTestId('close-button');


  messageText = this.page.locator('#show_messages');


  topUpReceiverInput = this.page.locator('#widget_1_topup_receiver');
  topUpAmountInput = this.page.locator('#widget_1_topup_amount');
  topUpAgreementCheckbox = this.page.locator(
    '#uniform-widget_1_topup_agreement span'
  );
  topUpExecuteButton = this.page.getByRole('button', {
    name: 'doładuj telefon',
  });


  moneyValueText = this.page.locator('#money_value');
  userNameText = this.page.getByTestId('user-name')
}

Zawartość payment.spec.ts wraz z wykorzystaniem PulpitPage, w którym znajduje się SideMenuComponent:

import { test, expect } from '@playwright/test';
import { loginData } from '../test-data/login.data';
import { LoginPage } from '../pages/login.page';
import { PaymentPage } from '../pages/payment.page';
import { PulpitPage } from '../pages/pulpit.page';


test.describe('Payment tests', () => {
  test.beforeEach(async ({ page }) => {
    const userId = loginData.userId;
    const userPassword = loginData.userPassword;


    await page.goto('/');
    const loginPage = new LoginPage(page)
    await loginPage.loginInput.fill(userId)
    await loginPage.passwordInput.fill(userPassword)
    await loginPage.loginButton.click()


    const pulpitPage = new PulpitPage(page)
    await pulpitPage.sideMenuComponent.paymentLink.click();
  });


  test('simple payment', async ({ page }) => {
    // Arrange
    const transferReceiver = 'Jan Nowak';
    const transferAccount = '12 3456 7890 1234 5678 9012 34568';
    const transferAmount = '222';
    const expectedMessage = `Przelew wykonany! ${transferAmount},00PLN dla Jan Nowak`;


    // Act
    const paymentPage = new PaymentPage(page)
    await paymentPage.transferReceiverInput.fill(transferReceiver)
    await paymentPage.transferToInput.fill(transferAccount)
    await paymentPage.transferAmountInput.fill(transferAmount)


    await paymentPage.transferButton.click()
    await paymentPage.actionCloseButton.click()


    // Assert
    await expect(paymentPage.messageText).toHaveText(expectedMessage);
  });
});

14 komentarzy

  1. Hej,
    czy bardzo złym pomysłem jest extendowanie klas page’ów o klasę komponentu sideMenu zamiast przypisywać instancję klasy komponentu do pola klasy?
    Czyli mniej więcej coś takiego:

    export class SideMenuComponent {
        paymentsNavBtn: Locator;
    
        constructor(protected readonly page: Page) {
            this.paymentsNavBtn = this.page.getByRole('link', { name: 'płatności' });
        }
    }
    
    export class DasboardPage extends SideMenuComponent {
        ....
        constructor(page: Page) {
            super(page);
            ....
        }
    }
    

    i standardowe użycie w teście:

    const dashboardPage = new DasboardPage(page);
    await dashboardPage.paymentsNavBtn.click();
    
    Avatar Mateusz Rodowski
    1. Hej,
      Świetne pytanie!
      Technicznie jest to możliwe, ale nie jest zalecane (i ja również bardzo bym to odradzał), ponieważ prowadzi do nieintuicyjnej hierarchii dziedziczenia i utrudnia utrzymanie kodu.

      W takim przypadku dziedziczenie nie odzwierciedla rzeczywistej relacji.
      DashboardPage nie jest SideMenuComponent, ale zawiera go jako element interfejsu.
      W dziedziczeniu powinniśmy myśleć o elementach dziedziczących jako elementach rozszerzających element bazowy.

      Mam dobry przykład z życia, który lepiej obrazuje relacje Page a Component 😉

      Załóżmy że mamy Radio oraz Car.
      Radio jest naturalnym elementem samochodu (czyli jest komponentem, z którego zbudowany jest samochód).

      I gdybyśmy teraz zrobili dziedziczenie – czyli Car dziedziczy po Radio:

      class Radio {
        playMusic() {
          console.log('Playing music...');
        }
      }
      
      class Car extends Radio { // ❌
        drive() {
          console.log('Driving...');
        }
      }
      

      to mamy sprzeczność bo samochód nie jest radiem!
      Dodatkowo co w przypadku gdy chcemy dodać więcej elementów wyposażenia do naszego samochodu? I tu pojawiają się kolejne problemy.

      W takich przypadkach lepszym podejściem jest kompozycja zamiast dziedziczenia – czyli tworzenie instancji komponentów wewnątrz klas, zamiast ich rozszerzania 😉

      Przykład poprawnego użycia dziedziczenia mógłby wyglądać w następujący sposób.
      Załóżmy, że mamy klasę bazową Page z pewnymi metodami.
      I chcemy, aby DashboardPage oraz SettingsPage dziedziczyły te metody.
      Dodatkowo mozemy do klasy bazowej dodać różne elementy (przez kompozycję) i wtedy taka klasa pochodna automatycznie je odziedziczy:

      class BasePage {
        sideMenu: SideMenuComponent;
        constructor(protected page: Page) {
          this.sideMenu = new SideMenuComponent(page);
        }
      
        async takeScreenshot() {
          await this.page.screenshot({ path: 'screenshot.png' });
        }
      }
      
      class DashboardPage extends BasePage {
        async open() {
          await this.page.goto('/dashboard');
        }
      }
      
      class SettingsPage extends BasePage {
        async open() {
          await this.page.goto('/settings');
        }
      }
      
      Krzysiek Kijas Krzysiek Kijas
  2. Cześć! Nie jestem do końca pewien jaki typ powinienem dać dla sideMenuComponent w payment.page.ts kiedy muszę dopasować się do standardów ES2022 – może warto byłoby to dodać do tej lekcji?

    export class PulpitPage {
      sideMenuComponent: // <---------------------- Jaki to typ powinien być?
    
      constructor(private page: Page) {
        this.sideMenuComponent = new SideMenuComponent(this.page);
      }
    }
    
    Avatar Jakub Wincenciak
      1. Hej,
        W takim przypadku, należałoby użyć nazwy klasy jako określenie typu, czyli:

        export class PulpitPage {
          sideMenuComponent: SideMenuComponent;
          constructor(private page: Page) {
            this.sideMenuComponent = new SideMenuComponent(this.page);
          }
        }
        
        

        i w tej postaci powinno to zadziałać😉

        Krzysiek Kijas Krzysiek Kijas
  3. Dodaliśmy w pliku payment.page.ts i pulpit.page.ts element

    sideMenu = new SideMenuComponent(this.page);
    

    , a w pliku payment.spec.ts z testami stosujemy :

    const pulpitPage = new PulpitPage(page);
    await pulpitPage.sideMenu.paymentButton.click();
    

    z racji ze zdecydowaliśmy sie utworzyć w obu plikach zmienną sideMenu poprawnie nie powinno być

    const paymentPage = new PaymentPage(page);
    await paymentPage.sideMenu.paymentButton.click();
    

    ?

    Avatar Grzegorz Gajownik
    1. Słuszna uwaga!
      Z jednej strony – moglibyśmy w beforeEach od razu stworzyć paymentPage, bo każda z tych stron zawiera w sobie SideMenuComponent.

      Z drugiej strony – zaraz po zalogowaniu jesteśmy na stronie pulpitu użytkownika (który reprezentuj PulpitPage). Zatem jeśli wykorzystamy opcję:

      const pulpitPage = new PulpitPage(page);
      await pulpitPage.sideMenu.paymentButton.click();
      

      obrazujemy w testach jak wygląda droga użytkownika (czyli jakie kolejno otwierane są strony).

      Oba podejścia są poprawne i każde z nich zawiera plusy i minusy. W pierwszym podejściu zmniejszamy odrobinę liczbę linii kodu, a w drugim – mamy więcej informacji o kolejnych krokach/odwiedzonych stronach 🙂

      Krzysiek Kijas Krzysiek Kijas
  4. u siebie w testach takie elementy (widoczne w każdym panelu, tj. np. pasek nawigacji, górny pasek, jakieś boczne ikony) wydzieliłem do osobnych page objectów czyli, przykładowo, taki pasek w naszym banku to czysta nawigacja:

    – navigation.page.ts

    i potem w testach
    const goTo = new navigationPage(page)

    await goTo.platnosci();

    macie jakieś przeciwwskazania dla takiego podejścia? Osobiście wydaje mi się to bardziej przejrzyste niż “ukrywanie” takiego menu w jakimś innym page’u 🙂

    Avatar Tomek Graul
    1. To troche zalezy od Twojego przypadku 🙂

      Ogólnie jeśli chodzi o komponenty występujące na stronach (menu, paski nawigacji, wyszukiwarka etc) zazwyczaj lepiej wydzielić jako osoby moduł, a następnie umiescic je w danej stronie (tak jak w przykładzie, stosując kompozycję).
      Przemawia za tym, że dane komponenty nie występują same w naturze, a są powiązane z daną stroną/stronami 🙂 Dzięki temu zabezpieczamy się przed wykorzystaniem elementów w momencie, gdy nie jesteśmy na stronie, na której te elementy występują.

      Jednak możemy zastosować też inne wzorce i praktyki, które modyfikują to podejście.

      Jesli dobrze rozumiem, to w Twoim przypadku taki komponent nawigacyjny pełni rolę nawigatora/transportera, czyli jest w stanie przekierować nas w dane miejsce (jak rozumiem – za pomoca menu).
      Rozważył bym też zmianę nazwy navigation.page.ts, bo page sugeruje że to strona 🙂 Może np. navigation.component.ts (jednak to moze sugerować komponent strony).

      Więc tu do rozważenia jak zaprojektować strony/komponenty/nawigacje w Twoim przypadku 🙂

      Krzysiek Kijas Krzysiek Kijas
  5. hmmm

    jeśli chodzi o:
    pulpitPage.sideMenuComponent.paymentLink.click();

    to czy nie lepiej by było zainicjować sideMenuComponent w teście bezpośrednio i po prostu się odnosić do niego:
    sideMenuComponent.paymentLink.click()?

    Avatar Michał Dudziak
    1. Pozwolę sobie porozmyślać, bo jest filozoficzne pytanie.

      `sideMenu` samo w sobie bez strony nie istnieje, dlatego w teście pulpitPage.sideMenu jest mniej dwuznaczne niż samo sideMenu bo komponent ten też będzie widoczny na innych stronach naszego serwisu, a są strony, na których on nie będzie widoczny jak loginPage. Nie używałbym raczej suffixu `Component` bo to już dodawanie za dużej ilości szczegółów do nazwy zmiennej, ale to następny temat rzeka 😀

      Avatar Adam Pajda
      1. Zgodzę się – sideMenu jako takie nie istnieje bez rodzica w postaci danej strony 😉 Przez to, że łączymy ten komponent ze stroną, to wiemy też na jakich stronach możemy go wywołać, a na jakich go nie ma (np. LoginPage) 🙂 Jest to mega cenna informacja i potrafi znacznie ułatwić pisanie testów.

        Nie jest to jedyne możliwe podejście – i jesli w projekcie z jakiegoś powodu podjęłaby została decyzja, że możemy używać elementów niezależenie – to też jest możliwe 🙂
        Minus – zaciemniamy sobie w testach co i gdzie jest możliwe do wykonania, jakie akcje gdzie mozna podejmować.

        Nazwy zmiennych to inna historia 😀
        Przy nauce na początku dobrze obrazują co jest czym, później można optymalizować i wpisywać ustalenia do Coding Standards 😉

        Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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