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ść, 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.
Tutaj
this.page
jest dostępne dopiero po wywołaniu konstruktora. Tymczasem polaloginInput, passwordInput, loginButton
są inicjalizowane zanim konstruktor się wykona. W efekcie TypeScript zgłasza błąd: “property is used before its initialization”.A teraz przykład z dziedziczeniem 😉
a klasa dziedzicząca:
Kiedy klasa dziedzicząca (np.
LoginPage)
wywołujesuper(page)
w swoim konstruktorze, polepage
w klasie bazowej zostaje poprawnie zainicjalizowane. To jest kluczowe – dzięki dziedziczeniu, polepage
w 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.page
w 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ć)