Powrót do: Praktyczne wprowadzenie do testów automatycznych z Playwright
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
);
});
});

Cześć, jest jakiś przyjęty wzorzec nazywania pól w klasach dla obiektów page?
Hej, nie ma jednego oficjalnego standardu językowego, ale w praktyce wykształciły się dość spójne konwencje, szczególnie w Page Object Model (POM) w TypeScripcie.
Najczęściej przyjęta konwencje
– camelCase + nazwa biznesowa elementu
– nazwy oparte o rolę:
class LoginPage { usernameInput: Locator passwordInput: Locator loginButton: Locator }albo:
class OrderSummaryPage { confirmOrderButton totalPriceLabel }Natomiast niezalecane są takie nazwy:
– greenButton
– rightPanelText
– loginButtonDiv
– usernameInputFieldTextBox
bo są zbyt dokładne (np Div – a co jeśli zmieni się typ na stronie?), albo niewiele mówią (greenButton niewiele mówi o przeznaczeniu elementu).
Kluczowe w projekcie jest to aby spisać wszystkie przyjęte standardy jako coding standards i je konsekwentnie stosować przez cały zespół 🙂
PS. Jeśli masz Program Playwright, to więcej o tym piszemy w tej lekcji: https://jaktestowac.pl/lesson/api6sd02l02/ 😉
Cześć!
Krótkie pytanie. Dlaczego przy logowaniu z prawidłowymi credentialami (15:44) dodajesz w asercji const pulpitPage = new PulpitPage(page); ?
Bez tego test też działa.
hej,
JEśli dobrze zrozumiałem Twoje pytanie i chodzi o 15:44, to tam potrzebujemy znacjonalizować obiekt strony, przypisać go do zmiennej aby móc wykorzystać w asercji lokatory, które mamy zadeklarowane w obiekcie strony, czyli:
const pulpitPage = new PulpitPage(page); await expect(pulpitPage.userNameText).toHaveText(expectedUserName);Bez tego przy tej architekturze nie damy rady skonstruować tej asercji 🙂
A jak wygląda Twój kod?
Nie wiem czemu, kiedy robię te zadania według tego jak na rozwiązaniu na filmiku to zawsze słówko page w np.
transferReceiverInput = this.page.getByTestId(‘transfer_reciver’);
podkreśla mi na czerwono
Jak zrobiłem ze słówkiem Locator to już nie było tego problemu:
import { Locator, Page } from '@playwright/test'; // export class PaymentPage { // transferReceiverInput: Locator; // transferToInput: Locator; // transferAmountInput: Locator; // transferButton: Locator; // actionCloseButton: Locator; // messageText: Locator; // constructor(private page: Page) { // this.transferReceiverInput = this.page.getByTestId('transfer_receiver'); // this.transferToInput = this.page.getByTestId('form_account_to'); // this.transferAmountInput = this.page.getByTestId('form_amount'); // this.transferButton = this.page.getByRole('button', {name: 'wykonaj przelew' }); // this.actionCloseButton = this.page.getByTestId('close-button'); // this.messageText = this.page.locator('#show_messages') // }Hej,
Dzięki za kod – wygląda dobrze (rozumiem, ze to fragment? Bo tam na końcu brakuje zamknięcia z
}dla klasy 😉 ), a problem, który opisujesz, najprawdopodobniej wynika z tego, że TypeScript nie wie, czym jest this.page 🤔Widze, że masz tutaj zastosowane poprawki po https://jaktestowac.pl/lesson/pw1s03l03b/ – wiec wszystko powinno być ok 🤔
Daj prosze znać jeśli trafisz na ten błąd ponownie!
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();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.
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? 🙂
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:
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 😎
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?
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 🙂