From 13d5bd81c05e84b1d953e7d409ef69e8e8e78ff1 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Sun, 3 Nov 2024 20:58:29 +0300 Subject: [PATCH 01/15] chore: added .vscode --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f4c3c0f..e5ed32a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ allure-report/ pytest.log /.venv +/.vscode \ No newline at end of file From 03ac7af18edd7bc3a6546b08e6449700baa5fbb6 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Sun, 3 Nov 2024 20:59:47 +0300 Subject: [PATCH 02/15] feat: added select option method --- pages/base_page.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pages/base_page.py b/pages/base_page.py index a423b26..0691224 100644 --- a/pages/base_page.py +++ b/pages/base_page.py @@ -78,3 +78,13 @@ def get_by_role_to_be_visible(self, role: str, name: str) -> bool: return False else: return True + + @allure.step("Select option {value} in dropdown {selector}") + def select_option(self, selector: str, value: str) -> None: + """Select option from dropdown by value. + + Args: + selector (str): Dropdown selector + value (str): Option value to select + """ + self.find_element(selector).select_option(value) From c7b7634b06b281914d696747ce6ad74e7313d360 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Sun, 3 Nov 2024 21:00:21 +0300 Subject: [PATCH 03/15] chore: added open new account url --- config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.py b/config/config.py index 718d4c5..c683a55 100644 --- a/config/config.py +++ b/config/config.py @@ -2,3 +2,4 @@ MAIN_URL = "index.htm" OVERVIEW_URL = "overview.htm" REGISTER_URL = "register.htm" +OPEN_NEW_ACCOUNT_URL = "openaccount.htm" From d334712098563fe78aa7393b47713ef72b64a94c Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Sun, 3 Nov 2024 22:19:52 +0300 Subject: [PATCH 04/15] chore: added dir traces --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e5ed32a..4738a65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ allure-report/ pytest.log /.venv -/.vscode \ No newline at end of file +/.vscode +/traces From ab462d59e6fdc649f95dba3fcfa47e965bbe0be7 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Wed, 6 Nov 2024 15:34:02 +0300 Subject: [PATCH 05/15] feat: added new account page --- tests/base/base_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/base/base_test.py b/tests/base/base_test.py index 754f2e4..acc152e 100644 --- a/tests/base/base_test.py +++ b/tests/base/base_test.py @@ -3,6 +3,7 @@ from config.data import Data from pages.main_page import MainPage +from pages.open_new_account_page import OpenNewAccountPage from pages.overview_page import OverviewPage from pages.register_page import RegisterPage @@ -15,6 +16,7 @@ class BaseTest: main_page: MainPage overview_page: OverviewPage register_page: RegisterPage + open_new_account_page: OpenNewAccountPage page: Page @pytest.fixture(autouse=True) @@ -25,3 +27,4 @@ def setup(cls, page: Page) -> None: cls.main_page = MainPage(page) cls.overview_page = OverviewPage(page) cls.register_page = RegisterPage(page) + cls.open_new_account_page = OpenNewAccountPage(page) From f4548462f4b91d1b7b06c6d56116fc7cb662fecd Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 08:58:09 +0300 Subject: [PATCH 06/15] feat: added login fixture and trace viewer --- conftest.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/conftest.py b/conftest.py index 8e78d65..adb24dd 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,6 @@ +import shutil from collections.abc import Generator +from pathlib import Path from typing import Any import allure @@ -7,6 +9,10 @@ from playwright.sync_api import Page, sync_playwright from playwright.sync_api._generated import Browser, BrowserContext +from config.data import Data +from pages.main_page import MainPage +from pages.overview_page import OverviewPage + @pytest.fixture(autouse=True) def browser() -> Generator[Browser, None, None]: @@ -21,10 +27,19 @@ def browser() -> Generator[Browser, None, None]: def page(browser: Browser, request: SubRequest) -> Generator[Page, None, None]: """Page fixture.""" context: BrowserContext = browser.new_context() + + # Start tracing + context.tracing.start( + screenshots=True, # Capture screenshots + snapshots=True, # Capture snapshots of the DOM + sources=True, # Capture source code + ) + page: Page = context.new_page() yield page if request.node.rep_call.failed: # type: ignore + context.tracing.stop(path=f"traces/trace-{request.node.name}.zip") allure.attach( body=page.screenshot(), name="screenshot", @@ -34,6 +49,15 @@ def page(browser: Browser, request: SubRequest) -> Generator[Page, None, None]: page.close() +@pytest.fixture(autouse=True, scope="session") +def clear_traces() -> None: + """Clear traces directory before test run.""" + traces_dir = Path("traces") + if traces_dir.exists(): + shutil.rmtree(traces_dir) + traces_dir.mkdir(exist_ok=True) + + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport( item: pytest.FixtureRequest, @@ -42,3 +66,30 @@ def pytest_runtest_makereport( outcome = yield rep = outcome.get_result() setattr(item, "rep_" + rep.when, rep) + + +@pytest.fixture +def login(page: Page) -> tuple[MainPage, OverviewPage]: + """ + Fixture that performs user login. + + Args: + page: Playwright page fixture + + Returns: + Tuple[MainPage, OverviewPage]: Tuple containing initialized MainPage and + OverviewPage + """ + main_page = MainPage(page) + overview_page = OverviewPage(page) + + main_page.navigate() + assert main_page.is_page_loaded(), "Main page is not loaded properly" # type: ignore + + username = Data().USERNAME + password = Data().PASSWORD + + main_page.login(username, password) # type: ignore + assert overview_page.is_logged_in(), "Login failed" # type: ignore + + return main_page, overview_page From fcc1b27e23180295f85273dd7c00b6cc32736d93 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 08:59:00 +0300 Subject: [PATCH 07/15] chore: updated --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index adcb996..8c52f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,8 @@ ignore = [ "N812", # Lowercase `expected_conditions` imported as non-lowercase "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "PGH003", # use specific rule codes when ignoring type issues - "S105" # hard-coded password + "S105", # hard-coded password + ] fixable = ["ALL"] unfixable = [] From fee6b4efa2f3267c46eb340ceaf70a2ba7ce68ac Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 08:59:24 +0300 Subject: [PATCH 08/15] feat: added enum class --- config/data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/data.py b/config/data.py index 4b1e464..96f5abf 100644 --- a/config/data.py +++ b/config/data.py @@ -1,4 +1,5 @@ import os +from enum import Enum from dotenv import load_dotenv @@ -12,3 +13,10 @@ def __init__(self) -> None: """Initialize data class with credentials from environment variables.""" self.USERNAME: str | None = os.getenv("PARABANK_USERNAME") self.PASSWORD: str | None = os.getenv("PARABANK_PASSWORD") + + +class AccountType(Enum): + """Available account types.""" + + SAVINGS = "SAVINGS" + CHECKING = "CHECKING" From 58c81dd6657856314dfafe2d8082055c6c1ac6fb Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 08:59:57 +0300 Subject: [PATCH 09/15] feat: added generate new username --- data/user_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/user_data.py b/data/user_data.py index 4c084b3..7eb23d5 100644 --- a/data/user_data.py +++ b/data/user_data.py @@ -40,3 +40,7 @@ def generate_random_user() -> "UserData": username=fake.user_name(), password=fake.password(length=12), ) + + def generate_new_username(self) -> None: + """Generate new username.""" + self.username = fake.user_name() From 5351df46bcbd695c60ce97dd9ffdc49f1ee7dc41 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 09:03:25 +0300 Subject: [PATCH 10/15] feat: added click_open_new_account method --- pages/overview_page.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/overview_page.py b/pages/overview_page.py index bb37e8e..9674066 100644 --- a/pages/overview_page.py +++ b/pages/overview_page.py @@ -22,3 +22,7 @@ def is_logged_in(self) -> bool: return self.contains_text( self.ACCOUNT_OVERVIEW_HEADER, self.ACCOUNT_OVERVIEW_HEADER_TEXT ) + + def click_open_new_account(self) -> None: + """Click on Open new account button.""" + self.click_by_role("link", "Open New Account") # type: ignore From 3934ebbcb550be8e016cd431f3912c8975a6fa32 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 09:05:09 +0300 Subject: [PATCH 11/15] feat: added the functionality of updating username if one already exists --- pages/register_page.py | 44 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/pages/register_page.py b/pages/register_page.py index 9e70914..8784364 100644 --- a/pages/register_page.py +++ b/pages/register_page.py @@ -1,5 +1,6 @@ import allure from playwright.sync_api import Page +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from config.config import REGISTER_URL from data.user_data import UserData @@ -22,6 +23,8 @@ class RegisterPage(BasePage): PASSWORD_INPUT = '[id="customer.password"]' CONFIRM_PASSWORD_INPUT = "#repeatedPassword" REGISTER_BUTTON = "Register" + USERNAME_EXISTS_ERROR = "cell" + USERNAME_EXISTS_MESSAGE = "This username already exists." def __init__(self, page: Page) -> None: """The register page.""" @@ -38,6 +41,11 @@ def fill_registration_form(self, user_data: UserData) -> None: self.fill_text(self.ZIP_CODE_INPUT, user_data.zip_code) self.fill_text(self.PHONE_INPUT, user_data.phone) self.fill_text(self.SSN_INPUT, user_data.ssn) + self.fill_credentials(user_data) # type: ignore + + @allure.step("Fill user credentials") + def fill_credentials(self, user_data: UserData) -> None: + """Fill username and password fields.""" self.fill_text(self.USERNAME_INPUT, user_data.username) self.fill_text(self.PASSWORD_INPUT, user_data.password) self.fill_text(self.CONFIRM_PASSWORD_INPUT, user_data.password) @@ -47,11 +55,41 @@ def click_register_button(self) -> None: """Click register button.""" self.click_by_role("button", self.REGISTER_BUTTON) # type: ignore + def check_username_exists(self) -> bool: + """Check if username already exists error is displayed.""" + try: + self.page.get_by_role( + self.USERNAME_EXISTS_ERROR, name=self.USERNAME_EXISTS_MESSAGE + ).wait_for(timeout=3000) + except PlaywrightTimeoutError: + return False + else: + return True + @allure.step("Register new user") - def register_new_user(self, user_data: UserData) -> None: - """Complete registration process.""" + def register_new_user(self, user_data: UserData, max_attempts: int = 5) -> bool: + """Complete registration process with username collision handling. + + Args: + user_data: User data for registration + max_attempts: Maximum number of attempts to try with different usernames + + Returns: + bool: True if registration was successful, False otherwise + """ self.fill_registration_form(user_data) # type: ignore - self.click_register_button() # type: ignore + + for _ in range(max_attempts): + self.click_register_button() # type: ignore + + if self.check_username_exists(): + user_data.generate_new_username() + self.fill_credentials(user_data) # type: ignore + continue + + return True + + return False def is_registration_successful(self) -> bool: """Check if registration was successful.""" From 6ae84d24fc88aac65a5464071ff7c117ee7a2a5a Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 09:05:54 +0300 Subject: [PATCH 12/15] test: added test open new account --- tests/test_open_new_account.py | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_open_new_account.py diff --git a/tests/test_open_new_account.py b/tests/test_open_new_account.py new file mode 100644 index 0000000..4c9d3f2 --- /dev/null +++ b/tests/test_open_new_account.py @@ -0,0 +1,43 @@ +import pytest + +from config.config import BASE_URL, OPEN_NEW_ACCOUNT_URL +from config.data import AccountType +from pages.main_page import MainPage +from pages.overview_page import OverviewPage +from tests.base.base_test import BaseTest + + +class TestOpenNewAccount(BaseTest): + """The test class for the open new account page.""" + + @pytest.mark.parametrize( + "account_type", + [ + pytest.param(AccountType.SAVINGS, id="savings_account"), + pytest.param(AccountType.CHECKING, id="checking_account"), + ], + ) + def test_open_new_account( + self, + login: tuple[MainPage, OverviewPage], # noqa: ARG002 + account_type: AccountType, + ) -> None: + """ + Test opening new account of different types. + + Args: + login: Login fixture + account_type: Type of account to open (SAVINGS or CHECKING) + """ + self.overview_page.click_open_new_account() + assert self.open_new_account_page.expect_url( + f"{BASE_URL}{OPEN_NEW_ACCOUNT_URL}" + ) + + self.open_new_account_page.select_account_type(account_type) + self.open_new_account_page.choose_an_existing_account() # type: ignore + self.open_new_account_page.click_button_open_new_account() + + assert ( + self.open_new_account_page.is_account_created() + ), f"Failed to create {account_type.name.lower()} account" From 322f808413fe9445b0c1952919d667179a529b56 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 09:30:04 +0300 Subject: [PATCH 13/15] test(refactor): added registration success --- tests/test_register.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_register.py b/tests/test_register.py index b239edf..af53c1a 100644 --- a/tests/test_register.py +++ b/tests/test_register.py @@ -31,8 +31,8 @@ def test_successful_registration(self) -> None: self.register_page.navigate() user_data: UserData = UserData.generate_random_user() - self.register_page.register_new_user(user_data) # type: ignore - + registration_success = self.register_page.register_new_user(user_data) # type: ignore + assert registration_success, "Registration was not successful" assert ( self.register_page.is_registration_successful() ), "Registration was not successful" From 7b2ec14cf849b7d4a1a7ea3136258dfe9c543158 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Fri, 8 Nov 2024 09:30:51 +0300 Subject: [PATCH 14/15] feat: added open new account page --- pages/open_new_account_page.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pages/open_new_account_page.py diff --git a/pages/open_new_account_page.py b/pages/open_new_account_page.py new file mode 100644 index 0000000..cf69dd1 --- /dev/null +++ b/pages/open_new_account_page.py @@ -0,0 +1,44 @@ +import allure +from playwright.sync_api import Page + +from config.config import OPEN_NEW_ACCOUNT_URL +from config.data import AccountType +from pages.base_page import BasePage + + +class OpenNewAccountPage(BasePage): # noqa: D101 + # locators + ACCOUNT_TYPE_SELECT = "#type" + ACCOUNT_EXISTING_SELECT = "#fromAccountId" + + def __init__(self, page: Page) -> None: + """The open new account page.""" + super().__init__(page, url=OPEN_NEW_ACCOUNT_URL) + + def select_account_type(self, account_type: AccountType) -> None: + """Select SAVINGS account type from dropdown.""" + self.select_option(self.ACCOUNT_TYPE_SELECT, account_type.value) # type: ignore + + @allure.step("Choose existing account") + def choose_an_existing_account(self) -> None: + """Select first available existing account from dropdown.""" + available_accounts = self.find_element( + self.ACCOUNT_EXISTING_SELECT + ).element_handles() + if available_accounts: + with allure.step("Select first available existing account {value}"): # type: ignore + self.select_option( + self.ACCOUNT_EXISTING_SELECT, + available_accounts[0].get_attribute("value"), # type: ignore + ) # type: ignore + else: + error_message = "No existing accounts available to select from" + raise ValueError(error_message) + + def click_button_open_new_account(self) -> None: + """Click on Open new account button.""" + self.click_by_role("button", "Open New Account") # type: ignore + + def is_account_created(self) -> bool: + """Check if account has been created.""" + return self.contains_text("#openAccountResult", "Account Opened!") From 7d27a1c1edda1b9ec8251ead04fd351498a39874 Mon Sep 17 00:00:00 2001 From: AlexeyZh Date: Sat, 9 Nov 2024 19:22:48 +0300 Subject: [PATCH 15/15] feat: added allure --- tests/test_open_new_account.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_open_new_account.py b/tests/test_open_new_account.py index 4c9d3f2..70f6543 100644 --- a/tests/test_open_new_account.py +++ b/tests/test_open_new_account.py @@ -1,3 +1,4 @@ +import allure import pytest from config.config import BASE_URL, OPEN_NEW_ACCOUNT_URL @@ -7,6 +8,29 @@ from tests.base.base_test import BaseTest +@allure.epic("Banking Application") +@allure.feature("Account Management") +@allure.description_html(""" +

Testing New Account Creation Functionality

+

Test verifies the process of opening new accounts with different types:

+
    +
  • Savings Account creation
  • +
  • Checking Account creation
  • +
+

The test performs the following operations:

+
    +
  • Navigation to the Open New Account page
  • +
  • Selection of account type (Savings/Checking)
  • +
  • Selection of existing account for linking
  • +
  • Account creation confirmation
  • +
+

Expected Results:

+
    +
  • Successfully navigate to the account creation page
  • +
  • Create new account of specified type
  • +
  • Receive confirmation of account creation
  • +
+""") class TestOpenNewAccount(BaseTest): """The test class for the open new account page."""