Najprostsza implementacja wzorca POM

UWAGA: W tej lekcji uległ odrobinie zmianie sposób implementacji dla Page Object!

Więcej o nowym sposobie przeczytasz pod nagraniem, w sekcji Uwaga 😉

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

Prezentacja

Najprostsza implementacja wzorca POM w Playwright

Dodatkowe materiały

Bazujemy na kodzie lekcji L08_dane_i_wtyczka_pw

Kod wynikowy tej lekcji znajduje się tu: L03_pom_login

Pamiętaj, aby po danej porcji pracy: uruchamiać test, commitować poprawnie działający kod 😉

TIP: W tej lekcji używamy kontroli wersji Git. Również w tej sekcji mamy w projekcie skonfigurowane narzędzie Prettier, które automatycznie formatuje kod.

Jeśli w poprzednich lekcjach go nie skonfigurowałeś tych narzędzi, to możesz to zrobić bazując na lekcjach bonusowych: Wersjonowanie projektu z Git oraz Prettier, czyli formatter kodu.

Page Object Model

Page Object Model może zostać zaimplementowany na różne sposoby i z różnym poziomem złożoności. Wszystko zależy od języka programowania, zastosowanych dodatkowych wzorców, wymagań i potrzeb. W tej lekcji zaczniemy od jednej z najprostszych implementacji 🙂

Page Object Model w najprostszej postaci

Page Object Model w jednej z najprostszych postaci to klasa, która reprezentuje daną stronę. Natomiast w tej klasie znajdują się lokatory, które wskazują na elementy (przyciski, pola tekstowe, etc.), z którymi wchodzimy w interakcję podczas testów.

W dużym uproszczeniu jego implementacja polega na następujących krokach:

  1. Tworzymy nowy moduł (plik), który zawiera klasę, która reprezentuje testowaną stronę.
  2. Następnie w tej klasie deklarujemy lokatory, które odwołują się do elementów na stronie.
  3. W testach wykorzystujemy obiekt strony wraz z lokatorami, aby wchodzić w interakcje z elementami strony.

Zawartość pliku 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')
}

UWAGA: 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.

W naszym przypadku oznacza to, że inicjalizację lokatorów musimy przenieść do konstruktora. 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:

Test successful login with correct credentials z pliku login.spec.ts:

  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
    await expect(page.getByTestId('user-name')).toHaveText(expectedUserName);
  });

Zalety:

  • zmniejszenie duplikacji kodu w testach – selektory i lokatory są w jednym miejscu (w klasie strony)
  • dobra utrzymywalność – gdy nastąpi zmiana na stronie (np. zmiana ID elementu), to musimy zaktualizować selektory w klasie strony, a testy pozostają bez zmian,
  • agregacja danych strony – zebranie w jednym miejscu wszystkich elementów strony, z którymi wchodzimy w interakcję,
  • bardzo szybka implementacja,
  • dobry dla mniejszych i prostszych testów i projektów.

Wady:

  • odpowiedzialność za kolejność działań jest po stronie piszącego testu – osoba pisząca nowe testy musi pamiętać, jakie elementy w jakiej kolejności powinno się używać na danej stronie, np. najpierw wpisanie loginu, później hasła, a na końcu przyciśnięcie przycisku zaloguj,
  • słabsza utrzymywalność przy większych projektach lub bardzo dużej liczbie testów – jeśli zmieni się wymagana kolejność działań (np. najpierw hasło, a później login), to będzie wymagana aktualizacja testów.

23 komentarze

  1. Hej, w klasie LoginPage podkreśla mi się page z komentarzem: Property ‘page’ is used before its initialization. Co jest dziwne to to, że nawet z tym podkreśleniem test wykonuje mi się poprawnie. Tak wygląda mój plik 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')
    }
    
    Avatar Katarzyna Jarosz
    1. Hej,
      Dobra obserwacja!
      Jest to znany przez nas “błąd” i mówimy o nim dokładniej w jednej z kolejnych lekcji 🙂
      https://jaktestowac.pl/lesson/pw2s05l04/

      W dużym uproszczeniu mamy tu rozjazd kilku standardów:
      – nasz kod,
      – kod modułów importowanych,
      – analiza przez ESLint.
      (masz w projekcie skonfigurowany ESLint? Lub czy masz paczkę tsconfig?)

      Błąd ten Property ‘page’ is used before its initialization jest związany ze sprawdzaniem naszego kodu pod względem danej wersji standardów.

      Tutaj dostajemy błąd, ze próbujemy korzystać z page, który jeszcze nie został zainicjalizowany, bo to następuje w konstruktorze. I TypeScript, próbuje nas bronić przed takim działaniem.

      Jednak z racji, że jesteśmy w klasie, to również przy inicjalizacji obiektu (wywołaniu konstruktora), inicjalizowane są wszystkie pola. Więc taki zapis jest poprawny w kontekście klas! 😀

      Dlatego w tym momencie mozesz ten błąd zignorować, a my później pokażemy Ci jak skonfigurować projekt, aby pozbyć się tego błędu 🙂

      Krzysiek Kijas Krzysiek Kijas
    1. Hej,
      Oficjalnych bibliotek nie widzieliśmy, natomiast widziałem na GitHuba różne mało popularne próby opakowania lokatorów.
      Osobiście z nich nie korzystamy a sami twórcy aktualnie tez oficjalnie nie rekomendują tego 🙂

      Krzysiek Kijas Krzysiek Kijas
  2. Cześć, mam problem z loginButton dostaje: ‘await’ has no effect on the type of this expression.ts(80007)

    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');
    }
    
    import { test, expect } from '@playwright/test';
    import { loginData } from '../test-data/login.data';
    import { LoginPage } from '../pages/login.page';
    
    test.describe('User login to Demobank', () => {
      test.beforeEach(async ({ page }) => {
        await page.goto('/'); //adres jest w playwright config
      });
    
      test.only('successful login with correct credentials', async ({ page }) => {
        const userId = loginData.userId;
        const password = loginData.userPassword;
        const expectedUserName = 'Jan Demobankowy';
    
        const loginPage = new LoginPage(page);
        await loginPage.loginInput.fill(userId)
        await loginPage.passwordInput.fill(password)
        await loginPage.loginButton.click
        // await page.waitForLoadState('domcontentloaded');
    
        await expect(page.getByTestId('user-name')).toHaveText(expectedUserName);
      });
    
    
    Avatar Rafał Leśnicki
    1. Hej,
      Rozumiem, że błąd występuje w linii:

      await loginPage.loginButton.click
      

      Błąd jest spowodowany brakiem () na końcu linii 🙂

      Zmień proszę linię na:

      await loginPage.loginButton.click()
      

      i daj znać czy to pomogło 😉

      Krzysiek Kijas Krzysiek Kijas
    2. Cześć, mam problem z loginButton dostaje: ‘await’ has no effect on the type of this expression.ts(80007)

      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');
      }
      
      import { test, expect } from '@playwright/test';
      import { loginData } from '../test-data/login.data';
      import { LoginPage } from '../pages/login.page';
      
      test.describe('User login to Demobank', () => {
        test.beforeEach(async ({ page }) => {
          await page.goto('/'); //adres jest w playwright config
        });
      
        test.only('successful login with correct credentials', async ({ page }) => {
          const userId = loginData.userId;
          const password = loginData.userPassword;
          const expectedUserName = 'Jan Demobankowy';
      
          const loginPage = new LoginPage(page);
          await loginPage.loginInput.fill(userId)
          await loginPage.passwordInput.fill(password)
          await loginPage.loginButton.click
          // await page.waitForLoadState('domcontentloaded');
      
          await expect(page.getByTestId('user-name')).toHaveText(expectedUserName);
        });
      
      

      Nie było pytania, można usunąć komentarz, znalazłem przyczynę

      Avatar Rafał Leśnicki
  3. Hej hej mam 2 pytanka,
    Klasę pod loginPage skonstruowałem trochę inaczej

    // Firstly import propernames from playwright test
    import {
      Locator,
      Page,
    } from '@playwright/test';
    
    export class LoginPage {
      // The variables with types
      private page: Page;
      private userId: string;
      private userPasswword: string;
      public loginInnput: Locator;
      public passwordInput: Locator;
      public loginSubmitButton: Locator;
      // For assertion only
      public accountOwner: Locator;
    
      // Constructor
      constructor(page: Page) {
        this.page = page;
        this.loginInnput = this.page.getByTestId('login-input');
        this.passwordInput = this.page.getByTestId('password-input');
        this.loginSubmitButton = this.page.getByTestId('login-button');
        // For assertion only
        this.accountOwner = this.page.getByTestId('user-name');
      }
    
      // Methods
      // User Login type in
      enterUserId = async (userId: string) => {
        await this.loginInnput.fill(userId);
      };
      // User Password type in
      enterUserPassword = async (userPasswword: string) => {
        await this.passwordInput.fill(userPasswword);
      }
      // Login user with credentials 
      submitLoggedUser = async () => {
        // this.enterUserId(this.userId);
        // this.enterUserPassword(this.userPasswword);
        await this.loginSubmitButton.click();
      }
    }
    

    Pytanie pierwsze brzmi czy używając w klasie metod z wykorzystaniem odpowiednich akcji lepiej jest użyć jednej metody która wywołuje kroki po kolei – czy lepiej rozbić to wszystko na osobne metody:
    – minusy – większa ilość kodu
    – plusy – po każdym kroku można używać asercje w kodzie co pozwoli na dokładniejsze przetestowanie danego działania??

    A pytanie drugie to
    Użyłem i w testach loginu i w testach desktopa i paymentów takiej konstrukcji w before Each’u

      test.beforeEach(async ({ page }) => {
        const userId = loginData.userId;
        const userPassword = loginData.userPassword;
    
        // Declaring our page instance
        const loginPage = new LoginPage(page);
    
        await page.goto('/');
        // new attitude of login operation
        await loginPage.enterUserId(userId);
        loginPage.enterUserPassword(userPassword);
        loginPage.submitLoggedUser();
      });
    

    Jak zauważyliście mimo deklaracji w klasie awaita przy metodzie wpisywania id usera musiałem dodać i w beforeEachu awaita dla tej metody ponieważ playwright nie wychwytywał tej metody – wpisywania tekstu w pole na czas i pole loginu zostawało puste – moje pytanie brzmi:
    Czy to jest błąd mój czy raczej Playwrighta że nie wychwytuje wpisania z przywołanej z klasy metody asychronicznej – i to tylko na pole loginu bo submit button i wpisanie hasła nie poczebuje już awaita ??
    Dzięki z góry za odpowiedź,

    Pozdrawiam,
    Jakub K

    Avatar Jakub Kruszyński
    1. znawczy się w testach loginu nie robiłem before eacha dla logowania tylko do wejścia na stronę i w żadnej z metod nie musiałem potem robić awaita

          // Act LOGIN:
          // Here we need to  creaqte loginPage obhect
          const loginPage = new LoginPage(page);
          // Assertion of proper page display
          await expect.soft(page).toHaveURL(/.*.app/);
          // Act entering the user Login
          loginPage.enterUserId(userId);
          // Soft Assertion - to verify element which is less important:
          await expect.soft(loginPage.loginInnput).toHaveValue(userId);
          // Act enterting user password
          loginPage.enterUserPassword(userPassword);
          // Soft Assertion - to verify element which is less important:
          await expect.soft(loginPage.passwordInput).toHaveValue(userPassword);
          // Submit login button
          // await page.getByTestId('login-button').click();
          loginPage.submitLoggedUser();
      

      Więc może to z hookiem Playwright ma problemy wiecie coś o tym ?? dzięki z góry za odpowiedź
      Pozdrawiam i spokojności życzę,
      Jakub K

      Avatar Jakub Kruszyński
    2. Hej,
      Sugeruje jednak agregacje akcji (czyli jedną metodę odpowiedzialną za akcje użytkownika, jaką jest logowanie), niż rozbijanie interakcji z elementami na pojedyncze metody.
      Więcej o tym opowiadam w kolejnych lekcjach: https://jaktestowac.pl/lesson/pw1s03l08/ 😉 Tam też opowiadam o plusach i minusach tego podejścia 😉

      W beforeEach brakuje Ci await
      przed metodami asynchronicznymi:

          loginPage.enterUserPassword(userPassword);
          loginPage.submitLoggedUser();
      

      Bez await wywoływanie tych metod może powodować wyścig – pole może nie byc wypełnione przed próbą naciśnięcia przycisku. Bez await akcje i testy będą niestabilne – raz zadziała, a innym razem (i przeważnie) – nie 🙂

      Krzysiek Kijas Krzysiek Kijas
  4. A ja mam pewną zagwozdkę 🙂 co zrobić z wait-ami i asercjami w stosunku do Page Object?

    Załóżmy, że w teście chcę mieć dwie metody ‘login’ i ‘logout’.

    Metoda ‘login’ zawiera:
    – wprowadzenie loginu,
    – hasła,
    – kliknięcie ‘Zaloguj’,
    – poczekanie (wait) na załadowanie się strony
    – sprawdzenie (expect), czy selektor ‘.name’ ma tekst ‘Witaj, ABC’.

    Metoda ‘logout’ zawiera:
    – kliknięcie przycisku ‘Wyloguj’,
    – poczekanie (wait) na wylogowanie
    – sprawdzenie (expect), czy ponownie widoczne jest okno logowania.

    Czy w Playwright wait-y i asercje nie mogą znajdować się w POM? Zostają w teście?

    Czyli wygladało by to tak:

    await login.fillInLoginAndPasswordAdmin();
    await page.waitForTimeout(3000);
    await expect(page.locator('.name')).toHaveText('Witaj, ABC');
    await logout.logoutMP();
    await page.waitForTimeout(3000);
    await expect(page.locator('h2')).toHaveText('Logowanie');
    
    Avatar Monika
    1. Bardzo dobre pytanie! 🙂

      Tak jak piszesz – są dwa podejścia.
      Jedno pozwala na umieszczanie asercji zarówno w obiektach stron oraz w testach.
      Natomiast drugie – tylko na poziomie testów.

      Oba mają swoje plusy i minusy.

      Asercje w obiektach stron:
      ❌Minusem z praktycznego punktu widzenia – jeśli dodamy asercję do metody logowania, to nie będziemy mogli użyć tej metody w teście niepoprawnego logowania.
      ❌Mniejsza czytelność testów – na poziomie plików .spec nie widzimy asercji -> nie wiemy co dokładnie sprawdzamy i co jest kluczowe w danym teście.
      ❌Niezgodne z Single Responsibility Principle
      ✅Możemy wykorzystywać mechanizm auto-wait – czekanie na wartość elementu jeśli wykorzystamy lokator w expect – await expect(page.locator('h2')).toHaveText('Logowanie');
      ✅Implementacja metod jest bardziej zgodna z praktyką Tell Dont Ask

      Asercje w testach:
      ❌Minusem z praktycznego punktu widzenia – jeśli dodamy asercję do metody logowania, to nie będziemy mogli użyć tej metody w teście niepoprawnego logowania.
      ❌Nie zawsze będzie możliwe czyste i proste wykorzystanie wcześniej wspomnianego mechanizmu auto-wait (chociaż w kolejnych lekcjach pokazuje jak podejść do testów negatywnych).
      ❌Implementacja metod jest mniej zgodna z praktyką Tell Dont Ask
      ✅Większa czytelność testów – asercje na poziomie plików .spec
      ✅Zgodne z Single Responsibility Principle

      Osobiście skłaniałbym się do umieszczania głównych asercji na poziomie testów 🙂 Czyli jesli mam test na logowanie – to asercje umieszczam na poziomie testu na jego końcu. W innych przypadkach, jeśli wykorzystuje logowanie, to mogę w inny sposób zweryfikować czy logowanie się powiodło (np. jesli aplikacja udostępnia API – to za pomocą szybkiej weryfikacji odpowiedzi – za pomocą metody page.waitForResponse() :))
      Jednocześnie nie wykluczałbym czasem potrzeby użycia sprawdzenia czegoś w metodach w obiektach stron. Duzo tu zależy od poziomu skomplikowania testów i samej aplikacji 🙂

      PS. Bardziej szczegółowo ten temat będę omawiał w zaawansowanym kursie Przegląd Architektury Testów z pełnego Programu https://jaktestowac.pl/playwright/

      Krzysiek Kijas Krzysiek Kijas
  5. Hejka,

    mam pytanie związane z przekazywaniem zmiennych.

    test('successful login with correct username', async ({ page }) => {
        const expectedUsername = 'Jan Demobankowy';
    
        await page.goto('/');
    
        const loginPage = new LoginPage(page)
        await loginPage.loginInput.fill(loginData.username);
    

    Czy powinniśmy przypisać wartości z pól obiektu “loginData.username” do zmiennej np. “loginID” czy możemy to sobie odpuścić?

    Chodzi mi o to czy wszystkie wartości powinny być wyciągane do zmiennych i wrzucane do sekcji Arrange? Czy jednak można to zostawić w taki sposób jak wkleiłem wyżej?

    Skoro zdefiniowałem te pola obiektu loginData, to czy jest sens dodatkowo je przypisywać do kolejnych zmiennych, szczególnie, że format przekazanego argumentu to obiekt.pole i jest on dość jednoznaczny

    Avatar Tomasz Guzik
    1. Hej,
      Bardzo dobre pytanie 😉

      Oba podejścia mają swoje plusy i minusy:

      Wydzielenie do zmiennych:
      ✅ lepsza widoczność zmiennych i danych które używamy w testach
      ✅ łatwiejsza zmiana danych, jeśli w teście wykorzystujemy kilka razy dane/obiekt loginData.username
      ❌ odrobina duplikacji – dodatkowa zmienna

      Używanie danych z loginData bezpośrednio:
      ✅ mniejsza duplikacja, mniej dodatkowych zmiennych
      ❌ możemy mieć trudność od razu stwierdzić jakie dane są wykorzystywane w testach – musimy przeglądać cały kod testów

      Jeśli mamy lekkie i krótkie testy – powiedziałbym, że można rozważyć podejście nr. 2.
      Natomiast gdy mamy bardziej rozbudowane scenariusze – wtedy sugerowałbym podejście 1. 🙂

      Dodatkowo pozostaje kwestia spójności kodu w różnych testach – tutaj też sugerowałbym trzymanie sie jednego podejścia. Dzięki temu my się nigdy nie zgubimy w naszym kodzie a wdrażanie nowych osób będzie łatwiejsze 🙂

      Krzysiek Kijas Krzysiek Kijas
  6. hej, mam pytanie o samą koncepcję POM i jej konkretne zastosowanie tutaj.

    przejrzałem stronę PW odnośnie POM:
    https://playwright.dev/docs/pom

    i z tego co widzę jest kilka różnic w stosunku do tego, jak Wy tutaj podchodzicie do zapisu rożnych rzeczy.

    Przykładowo, oni pokazują trochę inny styl deklarowania elementów w klasie:
    readonly getStartedLink: Locator;

    oraz później przypisanie im wartości:
    this.getStartedLink = page.locator(‘a’, { hasText: ‘Get started’ });

    Wy macie w klasie od razu:
    loginInput = this.page.getByTestId(‘login-input’)

    Możecie powiedzieć coś więcej na ten temat? Dlaczego nie robicie tak jak twórcy PW? 😀
    Czy warto wybrać Wasz sposób zapisu z jakichś konkretnych powodów?

    Avatar Michał Dudziak
    1. Różnice:
      – readonly – dzięki temu mamy pewność, że nie nadpiszemy danej wartości
      – deklaracja lokatorów – tutaj nie ma większych różnic (pomijając szczegóły jak to TS/JS później pod spodem ogarniają) – w naszym sposobie sam konstruktor jest wizualnie lżejszy

      Najważniejsze, zeby po wyborze danego podejścia konsekwentnie się go trzymać w całym frameworku 🙂

      Krzysiek Kijas Krzysiek Kijas
      1. Też miałem o to zapytać ponieważ zaciekawiło mnie, że deklarowane zmienne w POM są bez readonly, gdzie u siebie w pracy komercyjnej każdy POM jest tak skonstruowany. Tak jak Krzysiek napisał, najważniejsze to konsekwentne podejście, więc ja zostanę przy readonly 🙂

        Avatar Dominik Calak
  7. Część “Przemka” skomentowałem już wcześniej.
    Kurs jest świetny więc chciałem się jeszcze odnieść do części Krzyska.
    W skrócie – kontynuacja świetnej roboty 🙂

    Z POM już pracowałem dlatego w zasadzie starczyły mi materiały udostępnione miesiąc temu czyli do tej lekcji (+ był jeszcze git hub bodajże z 7 lekcji).

    Bardzo prosto przekazane są podstawy – mogłem szybko wystartować z POM w typescript. W zasadzie samego Playwrighta potem od razu porzuciłem..
    Ale dosyć szybko połapałem się dzięki przerobionemu kursowi w Cypress, który pojawił się z nowym projektem.

    Szczerze mówiąc jakbym miał wybierać to osobiście wolałbym Playwright – ale nie zawsze jest dana możliwość wyboru technologii. Zazwyczaj trzeba się dostosować do tego co jest 🙂

    Pozdrawiam. Dziękuję za kurs.
    Zapewne co jakiś czas będą do Was zaglądać bo sporo fajnych rzeczy tutaj macie.

    Avatar Łukasz
    1. Dzięki wielkie za pracę i zaangażowanie!
      Bardzo się ciesze, że z tego czego się nauczyłeś, to mogłeś wykorzystać w swojej pracy 🙂
      Wspominasz też o Cypressie. Osobiście gdybym miał do wyboru narzędzia w nowym proj – to zdecydowanie wybrałbym Playwrighta. Nawet w obecnym projekcie jestem odpowiedzialny za zmianę frameworka – z Cypressa na Playwrighta 🙂
      Sam Cypress też jest dobry w konkretnych przypadkach. Z każdego narzędzia jesteśmy w stanie się czegoś nauczyć, a później wykorzystać to doświadczenie w przyszłości 🙂

      Do zobaczenia w kolejnych materiałach!

      Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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