Bonus: Dostosowanie wzorca POM do standardów TypeScript

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

Prezentacja

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Bonus: Dostosowanie wzorca POM do standardów TypeScript

Zmiany

UWAGA: W tej lekcji będziemy operować na pewnych uproszczeniach, aby w przystępny sposób opisać wymagane zmiany i ich powody 🙂

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

TIP: Z czasem wersja ES2022 zostanie zastąpiona nowszą – dlatego Twoja wersja będzie zależała od tego, kiedy oglądasz to nagranie 🙂

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) {}
TIP: Dla przypomnienia, zapis konstruktora:

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:

8 komentarzy

  1. 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?

    Avatar Paulina Gruca
    1. 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.page jest dostępne dopiero po wywołaniu konstruktora. Tymczasem pola loginInput, 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 😉

      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łuje super(page) w swoim konstruktorze, pole page w klasie bazowej zostaje poprawnie zainicjalizowane. To jest kluczowe – dzięki dziedziczeniu, pole page w klasie bazowej zostaje ustawione, zanim cokolwiek innego w klasie dziedziczącej (np. LoginPage) zacznie korzystać z this.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ę do this.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 😉

      Krzysiek Kijas Krzysiek Kijas
      1. 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?

        Avatar Paulina Gruca
        1. 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 🙂

          Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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