Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/test state transitions #2

Merged
merged 16 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ allure-report/
pytest.log

/.venv
/.vscode
/traces
1 change: 1 addition & 0 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
MAIN_URL = "index.htm"
OVERVIEW_URL = "overview.htm"
REGISTER_URL = "register.htm"
OPEN_NEW_ACCOUNT_URL = "openaccount.htm"
8 changes: 8 additions & 0 deletions config/data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from enum import Enum

from dotenv import load_dotenv

Expand All @@ -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"
51 changes: 51 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import shutil
from collections.abc import Generator
from pathlib import Path
from typing import Any

import allure
Expand All @@ -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]:
Expand All @@ -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",
Expand All @@ -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,
Expand All @@ -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
4 changes: 4 additions & 0 deletions data/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
10 changes: 10 additions & 0 deletions pages/base_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 44 additions & 0 deletions pages/open_new_account_page.py
Original file line number Diff line number Diff line change
@@ -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!")
4 changes: 4 additions & 0 deletions pages/overview_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 41 additions & 3 deletions pages/register_page.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""
Expand All @@ -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)
Expand All @@ -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."""
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
3 changes: 3 additions & 0 deletions tests/base/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
67 changes: 67 additions & 0 deletions tests/test_open_new_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import allure
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


@allure.epic("Banking Application")
@allure.feature("Account Management")
@allure.description_html("""
<h2>Testing New Account Creation Functionality</h2>
<p>Test verifies the process of opening new accounts with different types:</p>
<ul>
<li>Savings Account creation</li>
<li>Checking Account creation</li>
</ul>
<p>The test performs the following operations:</p>
<ul>
<li>Navigation to the Open New Account page</li>
<li>Selection of account type (Savings/Checking)</li>
<li>Selection of existing account for linking</li>
<li>Account creation confirmation</li>
</ul>
<p>Expected Results:</p>
<ul>
<li>Successfully navigate to the account creation page</li>
<li>Create new account of specified type</li>
<li>Receive confirmation of account creation</li>
</ul>
""")
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"
4 changes: 2 additions & 2 deletions tests/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"