Powrót do: Praktyczne wprowadzenie do testów automatycznych z Playwright
Bonus: Dostosowanie wzorca POM do standardów TypeScript
Prezentacja
Zmiany
W repozytorium z kodem playwright_automatyzacja_wprowadzenie bazujemy już na nowym, dostosowanym do standardów podejściu 🙂
W najnowszej wersji TypeScript i ES doszły nowe reguły sprawdzające poprawność kodu.
Co ważne – VS Code domyślnie korzysta z nowszych wersji reguł (dla zainteresowanych – ustawienie “JS/TS › Implicit Project Config: Target” – aktualnie ES2022).
O samym ES i konfiguracjach na razie nie musisz więcej wiedzieć, jednak…
Co to dla nas zmienia? 🤔
Aktualnie wskazane jest inicjalizowanie zmiennych w konstruktorze.
Dotychczasowa implementacja – jej wady, zalety i gdzie jest ta niezgodność?
W naszym przypadku oznacza to, że inicjalizację lokatorów musimy przenieść do konstruktora.
W poprzedniej lekcji przyjeliśmy taką implementację w login.page.ts:
import { Page } from '@playwright/test'
export class LoginPage {
constructor(private page: Page) {}
loginInput = this.page.getByTestId('login-input')
passwordInput = this.page.getByTestId('password-input')
loginButton = this.page.getByTestId('login-button')
}
W tym przypadku inicjalizacja lokatorów odbywa się w ciele klasy, zaraz przy definiowaniu właściwości (pól) klasy.
Jaka jest zasada działania?
Gdy tworzymy obiekt strony:
const loginPage = new LoginPage(page)
to wywoływany jest constructor:
constructor(private page: Page) {}
import { Page } from '@playwright/test'
export class LoginPage {
constructor(private page: Page) {}
}
jest równoważny z takim zapisem:
import { Page } from '@playwright/test'
export class LoginPage {
private page: Page
constructor(page: Page) {
this.page = page
}
}
Zauważ, że w tym przypadku musimy mieć w klasie zadeklarowane pole page!
Następnie po wywołaniu konstruktora inicjalizowane są pola (property/właściwości) w klasie, czyli wszystkie lokatory:
import { Page } from '@playwright/test'
export class LoginPage {
constructor(private page: Page) {}
loginInput = this.page.getByTestId('login-input')
passwordInput = this.page.getByTestId('password-input')
loginButton = this.page.getByTestId('login-button')
}
Inicjalizacja tych zmiennych zadziała, bo wartość page mamy już przypisaną w konstruktorze 😉
Dlaczego jest to niezgodne z ES2022? 🔍
W wersji ECMAScript 2022 (ES2022) wprowadzono pewne zasady dotyczące inicjalizacji właściwości w klasach. Według tych zasad, jeśli właściwość klasy zależy od innych właściwości lub parametrów konstruktora (takich jak nasz page), to jej inicjalizacja powinna nastąpić po pełnym zainicjalizowaniu wszystkich tych zależności.
W naszym przypadku właściwości loginInput, passwordInput i loginButton są inicjalizowane bezpośrednio w ciele klasy.
ES2022 obecnie wymaga, aby taka inicjalizacja odbywała się w konstruktorze, gdzie mamy pewność, że wszystkie potrzebne zależności są już dostępne.
Przenosząc inicjalizację lokatorów do konstruktora, zapewniamy, że właściwość page została już przypisana i możemy bezpiecznie z niej korzystać.
Mimo że nasza poprzednia implementacja działa poprawnie, zmiana w ES2022 ma na celu ujednolicenie podejścia do inicjalizacji właściwości klas, aby zapewnić większą spójność i zgodność ze standardem.
Wymóg inicjalizacji pól w konstruktorze pomaga uniknąć sytuacji, w których właściwości mogłyby być używane przed ich pełnym zainicjalizowaniem, co zmniejsza ryzyko błędów.
Podejście to zyskuje na znaczeniu nie tylko w aplikacjach webowych i testach automatycznych, ale również w bardziej rozbudowanych aplikacjach, gdzie wymagana jest większa stabilność i przewidywalność działania kodu. Ujednolicenie zasad inicjalizacji zmniejsza ryzyko nieoczekiwanych błędów 😉
Zmiany w implementacji
W najnowszej wersji TypeScript musimy wykonać w konstruktorze inicjalizację pól klasy.
Zawartość pliku login.page.ts przyjmie postać:
import { Locator, Page } from '@playwright/test';
export class LoginPage {
loginInput: Locator;
passwordInput: Locator;
loginButton: Locator;
constructor(private page: Page) {
this.loginInput = this.page.getByTestId('login-input');
this.passwordInput = this.page.getByTestId('password-input');
this.loginButton = this.page.getByTestId('login-button');
}
}
Zmiana ta dotyczy w tym momencie tylko plików stron 🙂
Kod w repozytorium zaktualizowaliśmy do najnowszych wymagań.
Poprzedni kod (gdzie konstruktor mamy pusty) nadal będzie działał, ale w VS Code otrzymasz podkreślone na czerwowo this.page oraz informacje o błędzie:
Property 'page' is used before its initialization.ts(2729) login.page.ts(4, 15): 'page' is declared here. (property) LoginPage.page: Page
Jeśli jesteś zainteresowany czym jest ECMAScript, to skorzystaj z poniższych materiałów:











Cześć, takie pytanie:
Dlaczego dodajesz private w konstruktorze?
Hej!
Dodanie private w konstruktorze:
constructor(private page: Page) { ... }to skrócony zapis TypeScript, który automatycznie:
– tworzy pole page w klasie – nie trzeba pisać page: Page; na początku klasy.
– trzypisuje wartość page przekazaną do konstruktora – nie trzeba dodawać osobnej linii
this.page = pageTo samo można zapisać bardziej rozbudowanie tak:
class LoginPage { private page: Page; constructor(page: Page) { this.page = page; } }Ale wersja z private w parametrze konstruktora jest krótsza i czytelna – dlatego często się jej używa 😉
I co ważne – tak samo działa z public i protected:
– public – pole będzie dostępne na zewnątrz klasy (np. loginPage.page)
– protected – pole dostępne tylko w klasie i klasach dziedziczących
– private – pole dostępne tylko w tej klasie
Czyli korzystając z innych modyfikatorów dostępu całość moze tez tak wyglądać:
class LoginPage { constructor(public page: Page) { } }Cześć, trochę dalej jednak nie rozumiem.
U mnie klasa LoginPage wygląda następująco:
export class LoginPage { loginInput: Locator; passwordInput: Locator; loginButton: Locator; userName: Locator; usernameLoginError: Locator; passwordLoginError: Locator; constructor(page: Page) { this.loginInput = page.getByTestId('login-input'); this.passwordInput = page.getByTestId('password-input'); this.loginButton = page.getByTestId('login-button'); this.userName = page.getByTestId('user-name'); this.usernameLoginError = page.getByTestId('error-login-id'); this.passwordLoginError = page.getByTestId('error-login-password'); } }Ani page: Page na początku klasy ani this.page = page w konstruktorze potrzebne dla mojej klasy nie jest, działa i bez tego.
Też trochę nie rozumiem użycia private z punktu widzenia działania tego parametru w przypadku tej klasy. No chyba że założenie jest takie, że w przyszłości będzie dodana kolejna biblioteka, w której występuje page.
To w Twoim aktualnym przypadku nie potrzebujesz jeszcze mieć pola page w klasie 🙂
Ale jesli będziesz chciał w przyszłości zaimplementować w tej klasie metody, które korzystają z obiektu page, np:
export class LoginPage { loginInput: Locator; passwordInput: Locator; loginButton: Locator; userName: Locator; usernameLoginError: Locator; passwordLoginError: Locator; constructor(page: Page) { this.loginInput = page.getByTestId('login-input'); this.passwordInput = page.getByTestId('password-input'); this.loginButton = page.getByTestId('login-button'); this.userName = page.getByTestId('user-name'); this.usernameLoginError = page.getByTestId('error-login-id'); this.passwordLoginError = page.getByTestId('error-login-password'); } async getTitle(): Promise { // return await page.title(); // bedziemy potrzebowac pola page w tej klasie } }to będziesz potrzebował posiadać zapisany obiekt page w konstruktorze.
Również modyfikator private/protected może być ważny jesli zdecydujemy się na dziedziczenie klas i przygotowanie klasy bazowej, po której kolejne klasy stron będą dziedziczyły.
Natomiast jeśli chodzi o podejście – warto zachować spójność.
Dodałem private page: Page, bo:
– chciałem pokazać pełną strukturę klasy, która może być rozbudowana,
– często piszemy metody w klasach, które korzystają z this.page,
– to częsty wzorzec w Page Objectach w Playwright,
– skrót private page: Page ułatwia życie w większych klasach.
W Twoim przykładzie – nie jest to potrzebne, i masz rację, że działa bez tego 🙂
Cześć, zauważyłam, że ten błąd przy tworzeniu lokatorów i ostrzeżenie przed użyciem page przed inicjalizacja nie pojawi się jak mamy zastosowane dziedziczenie po innej klasie w związku z czym zmienia się trochę konstruktor, bo musimy użyć tej konstrukcji ze słowem kluczowym “super”. Możesz wyjaśni dlaczego ten błąd pojawia się tylko w klasach bez dziedziczenia?
Bardzo trafna obserwacja! 😀
Różnica wynika z zasad inicjalizacji pól w JavaScript/TypeScript, szczególnie w kontekście klas z i bez dziedziczenia.
W klasach, które nie dziedziczą, inicjalizacja pól w TypeScript/JavaScript odbywa się zaraz po stworzeniu instancji klasy, ale przed wywołaniem konstruktora.
export class LoginPage { loginInput = this.page.getByTestId('login-input'); // Błąd: `this.page` nie jest jeszcze zainicjalizowane. passwordInput = this.page.getByTestId('password-input'); loginButton = this.page.getByTestId('login-button'); constructor(private page: Page) {} }Tutaj
this.pagejest dostępne dopiero po wywołaniu konstruktora. Tymczasem polaloginInput, passwordInput, loginButtonsą inicjalizowane zanim konstruktor się wykona. W efekcie TypeScript zgłasza błąd: “property is used before its initialization”.A teraz przykład z dziedziczeniem 😉
import { Page } from '@playwright/test'; export class BasePage { constructor(protected page: Page) {} }a klasa dziedzicząca:
import { Page } from '@playwright/test'; import { BasePage } from './base.page'; export class LoginPage extends BasePage { constructor(page: Page) { super(page); } loginInput = this.page.getByTestId('login-input'); passwordInput = this.page.getByTestId('password-input'); loginButton = this.page.getByTestId('login-button'); }Kiedy klasa dziedzicząca (np.
LoginPage)wywołujesuper(page)w swoim konstruktorze, polepagew klasie bazowej zostaje poprawnie zainicjalizowane. To jest kluczowe – dzięki dziedziczeniu, polepagew klasie bazowej zostaje ustawione, zanim cokolwiek innego w klasie dziedziczącej (np.LoginPage) zacznie korzystać zthis.page.Ponieważ
super(page)musi zostać wywołane jako pierwsza instrukcja w konstruktorze klasy dziedziczącej, pole page w klasie bazowej jest już zainicjalizowane przed jakąkolwiek próbą odwołania się dothis.pagew klasie dziedziczącej.Reasumując chronologie inicjalizacji:
1. Wywołujemy konstruktor klasy (LoginPage)
2 .W LoginPage wywołany jest super(page)
3. super(page) wywołuje konstruktor klasy bazowej (BasePage)
4. Pole page jest ustawiane w klasie bazowej (BasePage) – this.page jest już dostępne w klasach dziedziczących.
5. Wracamy do konstruktora klasy dziedziczącej (LoginPage) – pola takie jak loginInput, passwordInput i loginButton mogą korzystać z this.page bez błędów.
PS. Ważny jest tutaj modyfikator dostępu protected w BasePage, który umożliwia skorzystanie z tego pola w klasach pochodnych 😉
super, dziękuję za to dodatkowe wyjaśnienie – teraz wszystko jest bardziej zrozumiałe 🙂 Teraz pytanie techniczne: czy pomimo tego, że żaden błąd nie jest zgłoszony jest sens w klasach z użyciem dziedziczenia inicjalizować pola tak jak w klasach bez dziedziczenia choćby dla zachowania spójności? Czy w tym przypadku taki refaktor jest nadmiarowy?
Dobre pytanie 😉
Jesli kazda strona dziedziczy (i będzie dziedziczyć) po BasePage – to powiedziałbym, żę refactor nie jest wymagany. Wtedy mamy zarówno spójność, jak i brak błędów związanych z ES2022.
Natomiast gdy nie przewidujemy dziedziczenia, lub dziedziczenie będzie tylko w części stron – to warto przemyśleć refactor 🙂
Po niedawnej aktualizacji pojawił mi się opisany tu błąd. Zacząłem szukać przyczyny, a tu widzę, że już wszystko wyjaśnione w bonusowej lekcji. Dzięki!
Cała przyjemność po naszej stronie!
Działamy szybko i sprawnie 😁
I aktualizujemy materiały do najnowszych standardów 😉
Ostatni slajd: literówka w drugim podpunkcie, jest ‘musimy rekatoryzować kod’ a powinno być ‘musimy refaktoryzować kod’
Hej!
Dzięki za zgłoszenie – poprawione na screenie 🙂
(wideo postaram się przy okazji przerenderować)