Add page objects and test framework scaffolding for Selenium-based test automation. Includes initial test suites, element locators, and configuration setup.
This commit is contained in:
parent
63e1574d2f
commit
b473e34809
10 changed files with 661 additions and 0 deletions
101
test/conftest.py
Normal file
101
test/conftest.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# conftest.py
|
||||
import pytest
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from pathlib import Path
|
||||
import openpyxl
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_options():
|
||||
options = Options()
|
||||
options.add_argument("--start-maximized")
|
||||
# options.add_argument("--headless") # Optional für CI/CD
|
||||
return options
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def driver(browser_options):
|
||||
"""Erstellt einen neuen Browser pro Test"""
|
||||
driver = webdriver.Chrome(options=browser_options)
|
||||
driver.implicitly_wait(10)
|
||||
yield driver
|
||||
driver.quit()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def wait(driver):
|
||||
"""WebDriverWait Instanz"""
|
||||
return WebDriverWait(driver, 20)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_url():
|
||||
"""Basis-URL deiner Anwendung"""
|
||||
return "http://localhost:5173"
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
"""Parametrisiert Tests basierend auf Excel-Dateien im testdata Ordner"""
|
||||
if "testcase_file" in metafunc.fixturenames:
|
||||
testdata_dir = Path("testcases")
|
||||
excel_files = list(testdata_dir.glob("*.xlsx"))
|
||||
|
||||
# IDs für bessere Testausgabe
|
||||
ids = [f.stem for f in excel_files]
|
||||
|
||||
metafunc.parametrize("testcase_file", excel_files, ids=ids)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_data(testcase_file):
|
||||
"""Lädt Testdaten aus Excel-Datei"""
|
||||
wb = openpyxl.load_workbook(testcase_file)
|
||||
|
||||
# Eingabedaten aus "in" Sheet
|
||||
input_sheet = wb["in"]
|
||||
input_data = {}
|
||||
groups = {}
|
||||
for row in input_sheet.iter_rows(values_only=True):
|
||||
if row[0]: # Feldname in Spalte A
|
||||
if '_' in row[0]:
|
||||
[remainder, suffix] = row[0].rsplit('_',1)
|
||||
if suffix.isdigit():
|
||||
[prefix, name] = remainder.split('_',1)
|
||||
if prefix not in groups:
|
||||
groups[prefix] = {}
|
||||
if groups[prefix].get(suffix) is None:
|
||||
groups[prefix][suffix] = {}
|
||||
groups[prefix][suffix][name] = row[1]
|
||||
else:
|
||||
input_data[row[0]] = row[1]
|
||||
else:
|
||||
input_data[row[0]] = row[1] # Wert in Spalte B
|
||||
input_data.update(groups)
|
||||
|
||||
# Erwartete Ausgaben aus "out" Sheet
|
||||
output_sheet = wb["out"]
|
||||
expected_data = {}
|
||||
expected_groups = {}
|
||||
for row in input_sheet.iter_rows(values_only=True):
|
||||
if row[0]: # Feldname in Spalte A
|
||||
if '_' in row[0]:
|
||||
[remainder, suffix] = row[0].rsplit('_',1)
|
||||
if suffix.isdigit():
|
||||
[prefix, name] = remainder.split('_',1)
|
||||
if prefix not in expected_groups:
|
||||
expected_groups[prefix] = {}
|
||||
if expected_groups[prefix].get(suffix) is None:
|
||||
expected_groups[prefix][suffix] = {}
|
||||
expected_groups[prefix][suffix][name] = row[1]
|
||||
else:
|
||||
expected_data[row[0]] = row[1]
|
||||
else:
|
||||
expected_data[row[0]] = row[1] # Wert in Spalte B
|
||||
expected_data.update(expected_groups)
|
||||
return {
|
||||
"input": input_data,
|
||||
"expected": expected_data,
|
||||
"filename": testcase_file.name
|
||||
}
|
||||
46
test/pages/assistant.py
Normal file
46
test/pages/assistant.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import time
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from conftest import driver
|
||||
from pages.base_page import BasePage
|
||||
|
||||
|
||||
class Assistant(BasePage):
|
||||
"""Page Object für den Assistant"""
|
||||
|
||||
# Mapping von Excel-Feldnamen zu Locators
|
||||
FIELD_MAPPING = {
|
||||
"PART_NUMBER": (By.CSS_SELECTOR, "textarea[name='partNumbers']"),
|
||||
"ANALYZE_BUTTON": (By.CSS_SELECTOR, ".part-number-modal-action > .btn--primary"),
|
||||
"SUPPLIER_NAME": (By.CSS_SELECTOR, ".supplier-headers-searchbar-container .search-input"),
|
||||
"LOAD_FROM_PREVIOUS": (By.CSS_SELECTOR, ".start-calculation-footer-container .checkbox-item"),
|
||||
"CREATE_CALCULATION_BUTTON": (By.CSS_SELECTOR, ".start-calculation-footer-container .btn--secondary"),
|
||||
"DELETE_SUPPLIER_BUTTON": (By.CSS_SELECTOR, ".supplier-headers ~ .item-list .item-list-element .icon-btn-container .icon-btn"),
|
||||
}
|
||||
|
||||
def search_part_numbers(self, data_dict):
|
||||
"""Füllt das Formular mit Daten aus dem Excel"""
|
||||
self.fill_input(*self.FIELD_MAPPING["PART_NUMBER"], data_dict["PART_NUMBER"])
|
||||
self.click_button(*self.FIELD_MAPPING["ANALYZE_BUTTON"])
|
||||
|
||||
def delete_preselected_suppliers(self):
|
||||
while True:
|
||||
try:
|
||||
button = self.wait_for_clickable(
|
||||
*self.FIELD_MAPPING["DELETE_SUPPLIER_BUTTON"],
|
||||
timeout=1
|
||||
)
|
||||
button.click()
|
||||
time.sleep(0.2)
|
||||
except:
|
||||
# Keine Buttons mehr vorhanden
|
||||
break
|
||||
|
||||
def select_supplier(self, data_dict):
|
||||
self.search_and_select_autosuggest(*self.FIELD_MAPPING["SUPPLIER_NAME"], data_dict["SUPPLIER_NAME"])
|
||||
|
||||
|
||||
def create_calculation(self, data_dict):
|
||||
self.set_checkbox(*self.FIELD_MAPPING["LOAD_FROM_PREVIOUS"], data_dict["LOAD_FROM_PREVIOUS"])
|
||||
self.click_button(*self.FIELD_MAPPING["CREATE_CALCULATION_BUTTON"])
|
||||
147
test/pages/base_page.py
Normal file
147
test/pages/base_page.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# pages/base_page.py
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
import time
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BasePage:
|
||||
"""Basis-Klasse für alle Page Objects"""
|
||||
|
||||
def __init__(self, driver, wait):
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
|
||||
def wait_for_spa_navigation(self, expected_route_part, timeout=2):
|
||||
"""Wartet bis SPA zur erwarteten Route navigiert hat"""
|
||||
WebDriverWait(self.driver, timeout).until(
|
||||
lambda d: expected_route_part in d.current_url
|
||||
)
|
||||
# Zusätzlich auf Vue-Rendering warten
|
||||
time.sleep(0.5)
|
||||
|
||||
def wait_for_element(self, by, value, timeout=2):
|
||||
"""Wartet auf ein Element"""
|
||||
start_time = time.time()
|
||||
logger.info(f"Waiting for element: {by}={value}")
|
||||
|
||||
result = WebDriverWait(self.driver, timeout).until(
|
||||
EC.presence_of_element_located((by, value))
|
||||
)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"Found element after {elapsed:.2f}s")
|
||||
return result
|
||||
def wait_for_clickable(self, by, value, timeout=2):
|
||||
"""Wartet bis Element klickbar ist"""
|
||||
return WebDriverWait(self.driver, timeout).until(
|
||||
EC.element_to_be_clickable((by, value))
|
||||
)
|
||||
|
||||
def fill_input(self, by, value, text, check_existence=False, timeout=2):
|
||||
if check_existence:
|
||||
# Prüfe ob Element existiert
|
||||
try:
|
||||
element = WebDriverWait(self.driver, timeout).until(
|
||||
EC.presence_of_element_located((by, value))
|
||||
)
|
||||
logger.info(f"Element exists, filling...")
|
||||
except TimeoutException:
|
||||
logger.warning(f"Element does not exist, skipping (check_existence=True)")
|
||||
return False
|
||||
else:
|
||||
# Normaler Modus - erwarte dass Element existiert
|
||||
element = self.wait_for_element(by, value, timeout)
|
||||
|
||||
# Element existiert - jetzt füllen
|
||||
element.clear()
|
||||
element.send_keys(text)
|
||||
logger.info(f"Filled input with: {text}")
|
||||
return True
|
||||
|
||||
def click_button(self, by, value):
|
||||
"""Klickt einen Button"""
|
||||
start_time = time.time()
|
||||
logger.info(f"Clicking button: {by}={value}")
|
||||
|
||||
button = self.wait_for_clickable(by, value)
|
||||
button.click()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"Clicked after {elapsed:.2f}s")
|
||||
|
||||
def set_checkbox(self, by, value, checked, timeout=2):
|
||||
label = self.wait_for_clickable(by, value, timeout)
|
||||
|
||||
checkbox_input = label.find_element(By.CSS_SELECTOR, "input[type='checkbox']")
|
||||
|
||||
is_checked = checkbox_input.is_selected()
|
||||
|
||||
if is_checked != checked:
|
||||
label.click()
|
||||
time.sleep(0.3)
|
||||
|
||||
|
||||
def select_dropdown_option(self, by, value, option_text, timeout=10):
|
||||
dropdown_button = self.wait_for_element(by, value, timeout=timeout)
|
||||
|
||||
try:
|
||||
current_value = dropdown_button.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"span.dropdown-trigger-text"
|
||||
).text
|
||||
|
||||
if current_value == option_text:
|
||||
logger.info(f"Dropdown already has value: {option_text}")
|
||||
return
|
||||
except:
|
||||
pass # Falls kein Text gefunden wurde, öffne das Dropdown
|
||||
|
||||
dropdown_button.click()
|
||||
logger.info("Opened dropdown")
|
||||
|
||||
menu = WebDriverWait(self.driver, timeout).until(
|
||||
EC.visibility_of_element_located((By.CSS_SELECTOR, "ul.dropdown-menu"))
|
||||
)
|
||||
logger.info("Dropdown menu visible")
|
||||
|
||||
option_xpath = f"//li[contains(@class, 'dropdown-option')][normalize-space(text())='{option_text}']"
|
||||
option = WebDriverWait(self.driver, timeout).until(
|
||||
EC.element_to_be_clickable((By.XPATH, option_xpath))
|
||||
)
|
||||
|
||||
logger.info(f"Clicking option: {option_text}")
|
||||
option.click()
|
||||
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
def search_and_select_autosuggest(self, by_or_selector, value_or_search_text,
|
||||
search_text=None,
|
||||
suggestion_selector=".suggestion-item",
|
||||
timeout=2):
|
||||
|
||||
if search_text is not None:
|
||||
# Fall: (By.CSS_SELECTOR, ".selector", "search_text")
|
||||
search_input = self.wait_for_element(by_or_selector, value_or_search_text, timeout)
|
||||
text_to_search = search_text
|
||||
else:
|
||||
# Fall: (".selector", "search_text")
|
||||
search_input = self.wait_for_element(By.CSS_SELECTOR, by_or_selector, timeout)
|
||||
text_to_search = value_or_search_text
|
||||
|
||||
search_input.clear()
|
||||
search_input.send_keys(text_to_search)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
suggestion = WebDriverWait(self.driver, timeout).until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, suggestion_selector))
|
||||
)
|
||||
suggestion.click()
|
||||
|
||||
time.sleep(0.5)
|
||||
157
test/pages/calculation_page.py
Normal file
157
test/pages/calculation_page.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# pages/calculation_page.py
|
||||
from selenium.webdriver.common.by import By
|
||||
from pages.base_page import BasePage
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CalculationPage(BasePage):
|
||||
"""Page Object für die Berechnungsformulare"""
|
||||
|
||||
# WICHTIG: Verwende data-v-* Attribute NUR wenn sie WIRKLICH stabil sind
|
||||
# Besser: Positionsbasierte Selektoren mit aussagekräftigen Parent-Elementen
|
||||
|
||||
FIELD_MAPPING = {
|
||||
# Material-Sektion (erste Box)
|
||||
"HS_CODE": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][1]//div[contains(@class, 'caption-column')][text()='HS code']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"TARIFF_RATE": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][1]//div[contains(@class, 'caption-column')][contains(text(), 'Tariff rate')]"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
|
||||
# Price-Sektion (zweite Box)
|
||||
"PRICE": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][2]//div[contains(@class, 'caption-column')][text()='MEK_A [EUR]']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"OVERSEA_SHARE": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][2]//div[contains(@class, 'caption-column')][contains(text(), 'Oversea share')]"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
# Handling Unit-Sektion (dritte Box)
|
||||
"LENGTH": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='HU length']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"WIDTH": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='HU width']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"HEIGHT": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='HU height']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"WEIGHT": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='HU weight']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
"PIECES_UNIT": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='Pieces per HU']"
|
||||
"/following-sibling::div//input[@class='input-field']"
|
||||
),
|
||||
|
||||
# Dropdowns
|
||||
"DIMENSION_UNIT": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='Dimension unit']"
|
||||
"/following-sibling::div//button[contains(@class, 'dropdown-trigger')]"
|
||||
),
|
||||
"WEIGHT_UNIT": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][3]//div[contains(@class, 'caption-column')][text()='Weight unit']"
|
||||
"/following-sibling::div//button[contains(@class, 'dropdown-trigger')]"
|
||||
),
|
||||
|
||||
# Checkboxen
|
||||
"FBA_FEE": (
|
||||
By.XPATH,
|
||||
"//div[contains(@class, 'master-data-item')][2]"
|
||||
"//label[contains(@class, 'checkbox-item')]"
|
||||
"[.//span[contains(@class, 'checkbox-label')][normalize-space(text())='']]"
|
||||
),
|
||||
"MIXED": (
|
||||
By.XPATH,
|
||||
"//label[contains(@class, 'checkbox-item')]"
|
||||
"[.//span[contains(@class, 'checkbox-label')][text()='Mixable']]"
|
||||
),
|
||||
"STACKED": (
|
||||
By.XPATH,
|
||||
"//label[contains(@class, 'checkbox-item')]"
|
||||
"[.//span[contains(@class, 'checkbox-label')][text()='Stackable']]"
|
||||
),
|
||||
}
|
||||
|
||||
DEST_FIELD_MAPPING = {
|
||||
"NAME": (By.XPATH, "//input[@placeholder='Add new Destination ...']"),
|
||||
"QUANTITY": (By.XPATH,
|
||||
"//div[contains(@class, 'destination-edit-column-caption') and contains(text(), 'Annual quantity')]/following-sibling::div[1]//input[@class='input-field']")
|
||||
"ROUTING": (By.XPATH,
|
||||
"//input[@type='radio' and @name='model' and @value='routing']"),
|
||||
"D2D": (By.XPATH,
|
||||
"//input[@type='radio' and @name='model' and @value='d2d']"),
|
||||
"ROUTE": (By.XPATH,
|
||||
"//div[@class='destination-route-container']//div[contains(@class, 'destination-route-inner-container')][.//span[contains(text(), 'Ireland Su')] and .//span[contains(text(), 'WH ULHA')] and .//span[contains(text(), 'AB')]]")
|
||||
"HANDLING_TAB": (By.XPATH, "//button[@class='tab-header' and text()='Handling & Repackaging']"),
|
||||
"CUSTOM_HANDLING": (By.XPATH,
|
||||
"//div[@class='destination-edit-handling-cost']//label[@class='checkbox-item']/input[@type='checkbox']"),
|
||||
"REPACKING": (By.XPATH,
|
||||
"//div[@class='destination-edit-column-caption' and contains(text(), 'Repackaging cost')]/following-sibling::div[@class='destination-edit-column-data'][1]//input[@class='input-field']"),
|
||||
"HANDLING": (By.XPATH,
|
||||
"//div[@class='destination-edit-column-caption' and contains(text(), 'Handling cost')]/following-sibling::div[@class='destination-edit-column-data'][1]//input[@class='input-field']"),
|
||||
"DISPOSAL": (By.XPATH,
|
||||
"//div[@class='destination-edit-column-caption' and contains(text(), 'Disposal cost')]/following-sibling::div[@class='destination-edit-column-data'][1]//input[@class='input-field']"),
|
||||
}
|
||||
|
||||
# Buttons
|
||||
CALCULATE_CLOSE_BUTTON = (By.XPATH, "//button[contains(., 'Calculate & close')]")
|
||||
CLOSE_BUTTON = (By.XPATH, "//button[contains(., 'Close') and not(contains(., 'Calculate'))]")
|
||||
|
||||
def fill_form(self, data_dict):
|
||||
"""Füllt das Formular mit Daten aus dem Excel"""
|
||||
for field_name, locator in self.FIELD_MAPPING.items():
|
||||
|
||||
value = data_dict[field_name]
|
||||
logger.info(f"Filling field: {field_name} = {value}")
|
||||
|
||||
try:
|
||||
if field_name in ["FBA_FEE", "STACKED", "MIXED"]:
|
||||
self.set_checkbox(*locator, str(value) == 'True')
|
||||
|
||||
elif field_name in ["DIMENSION_UNIT", "WEIGHT_UNIT"]:
|
||||
self.select_dropdown_option(*locator, str(value))
|
||||
|
||||
else:
|
||||
self.fill_input(*locator, str(value), check_existence=field_name in ["HS_CODE", "TARIFF_RATE"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fill field {field_name}: {e}")
|
||||
self.driver.save_screenshot(f"failed_field_{field_name}.png")
|
||||
raise Exception(f"Could not fill field '{field_name}': {e}") from e
|
||||
|
||||
def add_destination(self, data_dict):
|
||||
self.search_and_select_autosuggest(*self.DEST_FIELD_MAPPING["NAME"], data_dict["NAME"])
|
||||
|
||||
def fill_destination(self, data_dict):
|
||||
self.wait_for_element(*self.DEST_FIELD_MAPPING["QUANTITY"])
|
||||
pass
|
||||
|
||||
def click_calculate_and_close(self):
|
||||
"""Klickt auf 'Calculate & close' Button"""
|
||||
self.click_button(*self.CALCULATE_CLOSE_BUTTON)
|
||||
|
||||
def click_close(self):
|
||||
"""Klickt auf 'Close' Button"""
|
||||
self.click_button(*self.CLOSE_BUTTON)
|
||||
53
test/pages/dev_page.py
Normal file
53
test/pages/dev_page.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# pages/results_page.py
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
import time
|
||||
|
||||
class DevPage:
|
||||
MODAL_YES_BUTTON = (By.CSS_SELECTOR, "div.modal-dialog-actions button.btn--primary")
|
||||
MODAL_NO_BUTTON = (By.CSS_SELECTOR, "div.modal-dialog-actions button.btn--secondary")
|
||||
|
||||
def __init__(self, driver, wait):
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
|
||||
def dev_login(self, base_url, user_name="John"):
|
||||
"""
|
||||
Simuliert Login über /dev Seite
|
||||
|
||||
Args:
|
||||
base_url: Basis-URL der Anwendung
|
||||
user_name: Vorname des Users (z.B. "John", "Sarah", "Mike")
|
||||
"""
|
||||
# Navigiere zur Dev-Seite
|
||||
self.driver.get(f"{base_url}/dev")
|
||||
|
||||
# Warte bis Tabelle geladen ist
|
||||
self.wait.until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "table.data-table"))
|
||||
)
|
||||
|
||||
# Finde die User-Row anhand des Vornamens
|
||||
# Die Tabelle hat Spalten: First name | Last name | E-Mail | ...
|
||||
rows = self.driver.find_elements(By.CSS_SELECTOR, "table.data-table tbody tr.table-row")
|
||||
|
||||
for row in rows:
|
||||
cells = row.find_elements(By.TAG_NAME, "td")
|
||||
if cells and user_name in cells[0].text: # cells[0] ist "First name"
|
||||
# Klicke auf die Row
|
||||
self.wait.until(EC.element_to_be_clickable(row))
|
||||
row.click()
|
||||
break
|
||||
else:
|
||||
raise Exception(f"User '{user_name}' nicht in der Dev-User-Tabelle gefunden")
|
||||
|
||||
# Warte auf Modal und klicke "Yes"
|
||||
yes_button = self.wait.until(
|
||||
EC.element_to_be_clickable(self.MODAL_YES_BUTTON)
|
||||
)
|
||||
yes_button.click()
|
||||
|
||||
# Warte bis Modal geschlossen ist
|
||||
self.wait.until(
|
||||
EC.invisibility_of_element_located((By.CSS_SELECTOR, "div.modal-container"))
|
||||
)
|
||||
29
test/pages/navigation.py
Normal file
29
test/pages/navigation.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# pages/navigation.py
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from pages.assistant import Assistant
|
||||
from pages.base_page import BasePage
|
||||
from pages.calculation_page import CalculationPage
|
||||
from pages.results_page import ResultsPage
|
||||
|
||||
|
||||
class Navigation(BasePage):
|
||||
"""Handhabt die SPA-Navigation"""
|
||||
|
||||
# Locators für Navigationselemente
|
||||
MENU_BUTTON = (By.CSS_SELECTOR, "button.menu-toggle")
|
||||
NEW_CALCULATION_LINK = (By.CSS_SELECTOR, "a[href*='/assistant']")
|
||||
RESULTS_LINK = (By.CSS_SELECTOR, "a[href*='/results']")
|
||||
|
||||
def start_calculation(self, base_url):
|
||||
"""Navigiert zur Berechnungsseite"""
|
||||
self.driver.get(base_url+"/assistant")
|
||||
self.wait_for_spa_navigation("/assistant")
|
||||
return Assistant(self.driver, self.wait)
|
||||
|
||||
|
||||
def navigate_to_results(self):
|
||||
"""Navigiert zur Ergebnisseite"""
|
||||
self.click_button(*self.RESULTS_LINK)
|
||||
self.wait_for_spa_navigation("/results")
|
||||
return ResultsPage(self.driver, self.wait)
|
||||
35
test/pages/results_page.py
Normal file
35
test/pages/results_page.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# pages/results_page.py
|
||||
from selenium.webdriver.common.by import By
|
||||
from pages.base_page import BasePage
|
||||
|
||||
|
||||
class ResultsPage(BasePage):
|
||||
"""Page Object für die Ergebnisseite"""
|
||||
|
||||
# Mapping von Excel-Feldnamen zu Locators
|
||||
RESULT_MAPPING = {
|
||||
"gesamtkosten": (By.ID, "result-total-cost"),
|
||||
"zollgebuehr": (By.ID, "result-customs-fee"),
|
||||
"transportkosten": (By.ID, "result-transport-cost"),
|
||||
"mehrwertsteuer": (By.ID, "result-vat"),
|
||||
# Weitere Ergebnisfelder...
|
||||
}
|
||||
|
||||
def wait_for_results(self):
|
||||
"""Wartet bis Ergebnisse geladen sind"""
|
||||
self.wait_for_element(By.CSS_SELECTOR, ".results-container.loaded")
|
||||
|
||||
def read_results(self):
|
||||
"""Liest alle Ergebniswerte aus"""
|
||||
self.wait_for_results()
|
||||
|
||||
results = {}
|
||||
for field_name, locator in self.RESULT_MAPPING.items():
|
||||
try:
|
||||
element = self.wait_for_element(*locator)
|
||||
results[field_name] = element.text
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Lesen von '{field_name}': {e}")
|
||||
results[field_name] = None
|
||||
|
||||
return results
|
||||
18
test/pytest.ini
Normal file
18
test/pytest.ini
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[pytest]
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)s] %(message)s
|
||||
log_cli_date_format = %H:%M:%S
|
||||
testpaths = .
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--tb=short
|
||||
--capture=no
|
||||
--html=reports/report.html
|
||||
--self-contained-html
|
||||
markers =
|
||||
smoke: Smoke tests
|
||||
regression: Regression tests
|
||||
5
test/requirements.txt
Normal file
5
test/requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pytest==9.0.2
|
||||
selenium==4.39.0
|
||||
openpyxl==3.1.5
|
||||
pytest-html==4.1.1
|
||||
webdriver-manager==4.0.2
|
||||
70
test/test_calculation.py
Normal file
70
test/test_calculation.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# tests/test_calculation.py
|
||||
import pytest
|
||||
|
||||
from pages.dev_page import DevPage
|
||||
from pages.navigation import Navigation
|
||||
from pages.calculation_page import CalculationPage
|
||||
from pages.results_page import ResultsPage
|
||||
|
||||
|
||||
class TestCalculation:
|
||||
"""Testklasse für Berechnungslogik"""
|
||||
|
||||
def test_calculation_workflow(self, driver, wait, base_url, test_data):
|
||||
"""
|
||||
Haupttest: Führt einen kompletten Berechnungsdurchlauf durch
|
||||
Wird automatisch für jede Excel-Datei parametrisiert
|
||||
"""
|
||||
|
||||
login_page = DevPage(driver, wait)
|
||||
login_page.dev_login(base_url, user_name="John")
|
||||
|
||||
|
||||
# Setup assistant
|
||||
nav = Navigation(driver, wait)
|
||||
assistant = nav.start_calculation(base_url)
|
||||
assistant.search_part_numbers(test_data["input"])
|
||||
assistant.delete_preselected_suppliers()
|
||||
assistant.select_supplier(test_data["input"])
|
||||
assistant.create_calculation(test_data["input"])
|
||||
|
||||
# Fill calculation page
|
||||
calc_page = CalculationPage(driver, wait)
|
||||
calc_page.fill_form(test_data["input"])
|
||||
|
||||
for destination in test_data["input"]["DESTINATION"].values():
|
||||
calc_page.add_destination(destination)
|
||||
calc_page.fill_destination(destination)
|
||||
|
||||
# Ergebnisse lesen
|
||||
results_page = ResultsPage(driver, wait)
|
||||
actual_results = results_page.read_results()
|
||||
|
||||
# Assertions: Vergleich mit erwarteten Werten aus "out" Sheet
|
||||
expected = test_data["expected"]
|
||||
|
||||
for field_name, expected_value in expected.items():
|
||||
actual_value = actual_results.get(field_name)
|
||||
|
||||
# Numerische Werte mit Toleranz vergleichen
|
||||
if self._is_numeric(expected_value):
|
||||
expected_num = float(expected_value)
|
||||
actual_num = float(actual_value.replace("€", "").replace(",", ".").strip())
|
||||
|
||||
assert pytest.approx(expected_num, rel=0.01) == actual_num, \
|
||||
f"Feld '{field_name}': Erwartet {expected_num}, erhalten {actual_num}"
|
||||
else:
|
||||
# String-Vergleich
|
||||
assert str(actual_value).strip() == str(expected_value).strip(), \
|
||||
f"Feld '{field_name}': Erwartet '{expected_value}', erhalten '{actual_value}'"
|
||||
|
||||
print(f"✓ Test erfolgreich für {test_data['filename']}")
|
||||
|
||||
@staticmethod
|
||||
def _is_numeric(value):
|
||||
"""Prüft ob Wert numerisch ist"""
|
||||
try:
|
||||
float(str(value).replace(",", "."))
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
Loading…
Add table
Reference in a new issue