Rozwiązanie – podstawowy POM w testach

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

Dodatkowe materiały

Rozwiązanie prezentowane w tej lekcji zajdziesz w naszym repozytorium: L06_pom_refactor

Modele stron

Zawartość payment.page.ts:

import { Page } from '@playwright/test';


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


  transferReceiverInput = this.page.getByTestId('transfer_receiver');
  transferToInput = this.page.getByTestId('form_account_to');
  transferAmountInput = this.page.getByTestId('form_amount');


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


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

Zawartość pulpit.page.ts:

import { Page } from '@playwright/test';


export class PulpitPage {
  constructor(private page: 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')
}

Testy wykorzystujące modele stron

Zawartość payment.spec.ts:

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';


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()


    await page.getByRole('link', { name: 'płatności' }).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);
  });
});


Zawartość pulpit.spec.ts:

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


test.describe('Pulpit 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()
  });


  test('quick payment with correct data', async ({ page }) => {
    // Arrange
    const receiverId = '2';
    const transferAmount = '150';
    const transferTitle = 'pizza';
    const expectedTransferReceiver = 'Chuck Demobankowy';


    // Act
    const pulpitPage = new PulpitPage(page)
    await pulpitPage.transferReceiverInput.selectOption(receiverId);
    await pulpitPage.transferAmountInput.fill(transferAmount);
    await pulpitPage.transferTitleInput.fill(transferTitle);


    await pulpitPage.transferButton.click();
    await pulpitPage.actionCloseButton.click();


    // Assert
    await expect(pulpitPage.messageText).toHaveText(
      `Przelew wykonany! ${expectedTransferReceiver} - ${transferAmount},00PLN - ${transferTitle}`
    );
  });


  test('successful mobile top-up', async ({ page }) => {
    // Arrange
    const topUpReceiver = '500 xxx xxx';
    const topUpAmount = '50';
    const expectedMessage = `Doładowanie wykonane! ${topUpAmount},00PLN na numer ${topUpReceiver}`;


    // Act
    const pulpitPage = new PulpitPage(page)
    await pulpitPage.topupReceiverInput.selectOption(topUpReceiver);
    await pulpitPage.topupAmountInput.fill(topUpAmount);
    await pulpitPage.topupAgreementCheckbox.click();


    await pulpitPage.topupExecuteButton.click();
    await pulpitPage.actionCloseButton.click();


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


  test('correct balance after successful mobile top-up', async ({ page }) => {
    // Arrange
    const pulpitPage = new PulpitPage(page)
    const topUpReceiver = '500 xxx xxx';
    const topUpAmount = '50';
    const initialBalance = await pulpitPage.moneyValueText.innerText();
    const expectedBalance = Number(initialBalance) - Number(topUpAmount);


    // Act
    await pulpitPage.topupReceiverInput.selectOption(topUpReceiver);
    await pulpitPage.topupAmountInput.fill(topUpAmount);
    await pulpitPage.topupAgreementCheckbox.click();


    await pulpitPage.topupExecuteButton.click();
    await pulpitPage.actionCloseButton.click();


    // Assert
    await expect(pulpitPage.moneyValueText).toHaveText(`${expectedBalance}`);
  });
});


Zawartość login.spec.ts:

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


test.describe('User login to Demobank', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });


  test('successful login with correct credentials', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const userPassword = loginData.userPassword;
    const expectedUserName = 'Jan Demobankowy';


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


    // Assert
    const pulpitPage = new PulpitPage(page);
    await expect(pulpitPage.userNameText).toHaveText(expectedUserName);
  });


  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    const loginPage = new LoginPage(page);
    await loginPage.loginInput.fill(incorrectUserId);
    await loginPage.passwordInput.click();


    // Assert
    await expect(loginPage.loginError).toHaveText(
      expectedErrorMessage
    );
  });


  test('unsuccessful login with too short password', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const incorrectPassword = '1234';
    const expectedErrorMessage = 'hasło ma min. 8 znaków';


    // Act
    const loginPage = new LoginPage(page);
    await loginPage.loginInput.fill(userId);
    await loginPage.passwordInput.fill(incorrectPassword);
    await loginPage.passwordInput.blur();


    // Assert
    await expect(loginPage.passwordError).toHaveText(
      expectedErrorMessage
    );
  });
});


6 komentarzy

  1. Hej, robiąc samemu zwróciłem uwagę na to, że w PulpitPage łatwo robi się bałagan w property. Nie pomyślałem, że można trochę to wszystko uporządkować pustymi liniami i stwierdziłem, że stworzę obiekty z lokatorami właściwymi dla każdego okienka.

    export class PulpitPage {
        constructor(private page: Page) {}
    
        balance = this.page.locator('#money_value');
        closeButton = this.page.getByTestId('close-button');
        messageWindow = this.page.locator('#show_messages');
        paymentsButton = this.page.getByRole('link', { name: 'płatności' });
        userName = this.page.getByTestId('user-name');
    
        quickPayment = {
            transferReceiver: this.page.locator('#widget_1_transfer_receiver'),
            transferAmount: this.page.locator('#widget_1_transfer_amount'),
            transferTitle: this.page.locator('#widget_1_transfer_title'),
            executeButton: this.page.locator('#execute_btn'),
        };
    
        mobileTopUp = {
            topUpReceiver: this.page.locator('#widget_1_topup_receiver'),
            topUpAmount: this.page.locator('#widget_1_topup_amount'),
            agreementCheckbox: this.page.locator(
                '#uniform-widget_1_topup_agreement span'
            ),
            topUpButton: this.page.locator('#execute_phone_btn'),
        };
    }
    

    W samych testach trochę się wydłużyła ścieżka do lokatora. Pytanie, czy to w ogóle ma sens, czy to już przerost formy nad treścią?

    wykorzystanie w teście:

    //Act
            const pulpitPage = new PulpitPage(page);
            await pulpitPage.quickPayment.transferReceiver.selectOption(receiverId);
            await pulpitPage.quickPayment.transferAmount.fill(transferAmount);
            await pulpitPage.quickPayment.transferTitle.fill(transferTitle);
    
            await pulpitPage.quickPayment.executeButton.click();
            await pulpitPage.closeButton.click();
    
    Avatar Adam Kupper
    1. Hej,
      Bardzo słuszna obserwacja i bardzo dobry krok, szczególnie w bardziej rozbudowanych stronach 🙂

      Takie podejście poprawia czytelność, a to jest spory plus. Również dłuższe ścieżki w testach await pulpitPage.quickPayment.transferTitle.fill(transferTitle) to tylko chwilowa niedogodność – w przyszłych lekcjach (o agregacji akcji) zobaczysz dlaczego 😉

      Zauważ też, że możemy iśc jeszcze dalej – z wydzielonych przez Ciebie sekcji (jak quickPayment ) przygotować nowe moduły (pliki) z klasami odpowiedzialnymi za daną sekcję (np. export class QuickPaymentSection). Takie klasy stają się wtedy komponentami, a strona składałaby się z takich komponentów.
      I tego typu podejście to kompozycja 😉

      Dzięki temu możemy jeszcze zmniejszyć wielkość implementacji takiej strony o całe takie sekcje:

          quickPayment = {
              transferReceiver: this.page.locator('#widget_1_transfer_receiver'),
              transferAmount: this.page.locator('#widget_1_transfer_amount'),
              transferTitle: this.page.locator('#widget_1_transfer_title'),
              executeButton: this.page.locator('#execute_btn'),
          };
      

      Reasumując – w przypadku rozbudowanych stron, gdzie można na stronie wydzielić takie logiczne całości/sekcje, to dobry krok w stronę zmniejszenia wielkości plików i klas 🙂
      Natomiast w przypadku niewielkich testów (tych prezentowanych w tej lekcji) mozna jeszcze trzymać wszystko w jednym pliku.

      Krzysiek Kijas Krzysiek Kijas
  2. Czy istnieje możliwość zadeklarowania obiektów typu page.ts w poszczególnych klasach testów, żeby były dostępne we wszystkich testach, które znajdują się w danej klasie, bez konieczności duplikacji linijki w formie np. const loginPage = new LoginPage(page); w każdym z testów w danej klasie? 🙂

    Avatar Marcin Bubolc
    1. Jak najbardziej, jest kilka sposobów na to.

      1. Na początek (najprościej) można to zrobić klasyczną kompozycją. Czyli wydzielić kod logowania do osobnej funkcji (i umieścić ją w osobnym pliku z np. funkcjami pomocniczymi lub po prostu login.helper.ts ):

      import { Page } from '@playwright/test';
      import { LoginPage } from '../pages/login.page';
      
      const loginToService = async (page: Page, userId: string, userPassword: string) => {
        const loginPage = new LoginPage(page);
        await loginPage.loginInput.fill(userId);
        await loginPage.passwordInput.fill(userPassword);
        await loginPage.loginButton.click();
      }
      

      i po prostu wywoływać ją przed wszystkimi akcjami w teście:

      await loginToService(page, 'user', 'password')
      

      2. Można to zrealizować za pomocą projektów zależnych (omawiamy je na tym webinarze: https://jaktestowac.pl/pw-strategie/ lub jeśli masz dostęp do pełnego Programu to jest to tutaj: https://jaktestowac.pl/lesson/pw9-web1/). W tym przypadku używamy pliku sesji w testach i nie musimy się logować w testach, które podłączymy pod projekt.

      3. Można również zrobić poprzez fixture, który umożliwi zdefiniowanie takich akcji i użycie ich w dowolnym teście (np. zalogowanie się). Jeśli masz dostęp do pełnego Programu tu znajdziesz pełną sekcję o fixtures: https://jaktestowac.pl/lesson/pw2s09l01/).

      Zacznij od najprostszych rozwiązań a potem śmiało rozwijaj i testuj różne podejścia 😎

      Przemek Przemek
  3. Hej,
    W pulpit.page.ts i payment.page.ts mamy dwa identyczne properties:: actionCloseButton i messageText.
    Czy złą praktyką jest umieszczenie ich tylko w jednym pliku (np. w pulpit.page,ts) i potem wykorzystywanie z tego miejsca również w testach payment?

    Avatar Jarosław Frączek
    1. Hej,
      Gdybyśmy umieścili te elementy na stronie pulpit.page,ts, a później z nich korzystali w payment.page.ts – to mamy tu zależność elementów między stronami. Strona payment zależy od pulpit – co nie jest uznawane za dobrą praktykę 😉

      Dlatego w kolejnych lekcjach opowiadam o komponentach (i kompozycji) – czyli wydzielaniu takich wspólnych elementów, a później ich reużywanie w rożnych stronach 🙂 Dzięki temu zmniejszamy duplikacje.

      W komponentach możemy wydzielać różne elementy, które są wspólne dla wielu stron – lewe menu, popupy etc 🙂

      Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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