This commit is contained in:
whilb 2025-08-16 17:54:12 -07:00
commit 97f9a95415
21 changed files with 2963 additions and 0 deletions

111
tests/conftest.py Normal file
View file

@ -0,0 +1,111 @@
import contextlib, http.server, os, socket, threading, time
import pytest
import pathlib
import sys
import requests
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
ROOT = os.path.dirname(os.path.abspath(__file__))
DOCROOT = os.path.abspath(os.path.join(ROOT, '..', 'public'))
class SpaHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/' or self.path.split('?',1)[0].split('#',1)[0] == '/':
return http.server.SimpleHTTPRequestHandler.do_GET(self)
try:
return http.server.SimpleHTTPRequestHandler.do_GET(self)
except Exception:
pass
self.path = '/index.html'
return http.server.SimpleHTTPRequestHandler.do_GET(self)
@contextlib.contextmanager
def static_server():
os.chdir(DOCROOT)
httpd = None
with socket.socket() as s:
s.bind(('127.0.0.1', 0))
host, port = s.getsockname()
handler = SpaHandler
httpd = http.server.ThreadingHTTPServer(('127.0.0.1', port), handler)
t = threading.Thread(target=httpd.serve_forever, daemon=True)
t.start()
try:
yield f"http://127.0.0.1:{port}"
finally:
httpd.shutdown()
t.join()
@pytest.fixture(scope="session")
def base_url():
with static_server() as url:
yield url
@pytest.fixture(scope="session")
def dev_server():
"""Start the local development server for testing using the same logic as dev_server.py"""
# Check if server is already running
try:
response = requests.get("http://localhost:8008", timeout=1)
if response.status_code == 200:
print("Development server already running on port 8008")
yield "http://localhost:8008"
return
except:
pass
# Use the same server logic as dev_server.py
print("Starting development server...")
# Import the handler class from dev_server
sys.path.append(os.path.dirname(DOCROOT))
from dev_server import SPAHandler
# Create and start server
os.chdir(DOCROOT) # serve from /public
httpd = http.server.ThreadingHTTPServer(('127.0.0.1', 8008), SPAHandler)
t = threading.Thread(target=httpd.serve_forever, daemon=True)
t.start()
# Wait for server to start
max_wait = 30
for i in range(max_wait):
try:
response = requests.get("http://localhost:8008", timeout=1)
if response.status_code == 200:
print(f"Development server started successfully after {i+1} seconds")
break
except:
if i == max_wait - 1:
print("Failed to start development server")
httpd.shutdown()
raise
time.sleep(1)
try:
yield "http://localhost:8008"
finally:
print("Stopping development server...")
httpd.shutdown()
t.join()
print("Development server stopped")
@pytest.fixture(scope="function")
def calculator_page(driver, dev_server):
"""Navigate to the calculator page using the development server"""
driver.get(f"{dev_server}")
# Wait for the page to load and JavaScript to populate navigation
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "sidenav"))
)
# Wait for JavaScript to populate the navigation with calculator links
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(text(), 'Interest')]"))
)
return driver

404
tests/test_calculators.py Normal file
View file

@ -0,0 +1,404 @@
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
import time
from selenium.webdriver.common.keys import Keys
@pytest.fixture(scope="function")
def driver():
"""Create a new browser instance for each test"""
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu")
driver = webdriver.Chrome(options=chrome_options)
driver.implicitly_wait(10)
yield driver
driver.quit()
# calculator_page fixture is now defined in conftest.py
class TestCalculatorNavigation:
"""Test calculator navigation and basic functionality"""
def test_debug_page_content(self, driver):
"""Debug test to see what's actually on the page"""
driver.get("http://localhost:8008")
# Wait for basic page structure
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "sidenav"))
)
# Print page title and basic info
print(f"\nPage title: {driver.title}")
print(f"Current URL: {driver.current_url}")
# Check if sidenav has any content
sidenav = driver.find_element(By.CLASS_NAME, "sidenav")
print(f"Sidenav HTML: {sidenav.get_attribute('innerHTML')}")
# Check for JavaScript errors in console
logs = driver.get_log('browser')
if logs:
print(f"\nBrowser console logs:")
for log in logs:
print(f" {log['level']}: {log['message']}")
# Check if app.js loaded
try:
app_js = driver.find_element(By.XPATH, "//script[@src='/js/app.js']")
print(f"App.js script tag found: {app_js.get_attribute('outerHTML')}")
except:
print("App.js script tag NOT found!")
# Wait a bit longer and check again
time.sleep(3)
sidenav_after_wait = driver.find_element(By.CLASS_NAME, "sidenav")
print(f"Sidenav HTML after 3s wait: {sidenav_after_wait.get_attribute('innerHTML')}")
# This test should always pass for debugging
assert True
def test_page_loads(self, calculator_page):
"""Test that the calculator page loads correctly"""
assert "calculator.127local.net" in calculator_page.title
assert calculator_page.find_element(By.CLASS_NAME, "sidenav")
assert calculator_page.find_element(By.CLASS_NAME, "content")
def test_calculator_list(self, calculator_page):
"""Test that all calculators are listed in navigation"""
nav_items = calculator_page.find_elements(By.CSS_SELECTOR, ".sidenav a")
calculator_names = [item.text for item in nav_items]
expected_calculators = [
"Interest (Simple & Compound)",
"Bandwidth",
"NMEA",
"RAID",
"Currency Converter"
]
for expected in expected_calculators:
assert expected in calculator_names, f"Calculator {expected} not found in navigation"
class TestInterestCalculator:
"""Test the Interest calculator functionality"""
def test_interest_calculator_loads(self, calculator_page):
"""Test that interest calculator loads and displays correctly"""
# Click on interest calculator
interest_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Interest')]")
interest_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "principal"))
)
# Check that all inputs are present
assert calculator_page.find_element(By.NAME, "principal")
assert calculator_page.find_element(By.NAME, "rate")
assert calculator_page.find_element(By.NAME, "compound")
assert calculator_page.find_element(By.NAME, "years")
assert calculator_page.find_element(By.NAME, "contrib")
assert calculator_page.find_element(By.NAME, "contribFreq")
def test_simple_interest_calculation(self, calculator_page):
"""Test simple interest calculation"""
# Load interest calculator
interest_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Interest')]")
interest_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "principal"))
)
# Set values for simple interest
principal_input = calculator_page.find_element(By.NAME, "principal")
rate_input = calculator_page.find_element(By.NAME, "rate")
years_input = calculator_page.find_element(By.NAME, "years")
compound_select = calculator_page.find_element(By.NAME, "compound")
principal_input.clear()
principal_input.send_keys("1000")
rate_input.clear()
rate_input.send_keys("5")
years_input.clear()
years_input.send_keys("3")
# Select simple interest (no compounding)
# Click on the Select Lite button to open dropdown
compound_button = calculator_page.find_element(By.CSS_SELECTOR, '[name="compound"] + .select-lite__button')
compound_button.click()
# Wait for dropdown to appear and select Simple option
simple_option = WebDriverWait(calculator_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Simple')]"))
)
simple_option.click()
# Wait for calculation
time.sleep(1)
# Check result
result = calculator_page.find_element(By.CLASS_NAME, "result")
assert "Future value:" in result.text
assert "1,150.00" in result.text # 1000 + (1000 * 0.05 * 3)
def test_compound_interest_calculation(self, calculator_page):
"""Test compound interest calculation"""
# Load interest calculator
interest_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Interest')]")
interest_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "principal"))
)
# Set values for compound interest
principal_input = calculator_page.find_element(By.NAME, "principal")
rate_input = calculator_page.find_element(By.NAME, "rate")
years_input = calculator_page.find_element(By.NAME, "years")
principal_input.clear()
principal_input.send_keys("1000")
rate_input.clear()
rate_input.send_keys("5")
years_input.clear()
years_input.send_keys("3")
# Wait for calculation
time.sleep(1)
# Check result (should be higher than simple interest)
result = calculator_page.find_element(By.CLASS_NAME, "result")
assert "Future value:" in result.text
# Compound interest should be > 1150 (simple interest result)
assert "1,150.00" not in result.text or "1,157.63" in result.text
class TestCurrencyConverter:
"""Test the Currency Converter functionality"""
def test_currency_converter_loads(self, calculator_page):
"""Test that currency converter loads and displays correctly"""
# Click on currency converter
currency_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Currency Converter')]")
currency_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Check that all inputs are present
assert calculator_page.find_element(By.NAME, "amount")
assert calculator_page.find_element(By.NAME, "from")
assert calculator_page.find_element(By.NAME, "to")
def test_currency_conversion_same_currency(self, calculator_page):
"""Test conversion when from and to currencies are the same"""
# Load currency converter
currency_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Currency Converter')]")
currency_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Debug: verify we're on the currency converter
print(f"Current URL: {calculator_page.current_url}")
print(f"Page title: {calculator_page.title}")
# Check for currency converter specific elements
try:
from_select = calculator_page.find_element(By.NAME, "from")
to_select = calculator_page.find_element(By.NAME, "to")
print(f"From select value: {from_select.get_attribute('value')}")
print(f"To select value: {to_select.get_attribute('value')}")
except Exception as e:
print(f"Error finding currency elements: {e}")
# Set amount
amount_input = calculator_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("100")
# Trigger calculation by changing the amount
amount_input.send_keys(Keys.TAB)
# Wait for calculation to complete
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Wait a bit more for the currency converter to fully load and replace the old result
time.sleep(1)
# Find all result elements and get the one that's actually visible/contains currency content
result_elements = calculator_page.find_elements(By.CLASS_NAME, "result")
# Look for the result element that contains currency-related content
currency_result = None
for elem in result_elements:
html = elem.get_attribute('innerHTML')
if 'USD' in html or 'EUR' in html or 'Currency' in html or 'conversion' in html.lower():
currency_result = elem
break
if not currency_result:
# If no currency result found, use the last result element (most recent)
currency_result = result_elements[-1]
# Check result
assert "100 USD" in currency_result.text or "100 EUR" in currency_result.text
def test_currency_conversion_different_currencies(self, calculator_page):
"""Test conversion between different currencies"""
# Load currency converter
currency_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Currency Converter')]")
currency_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Set amount
amount_input = calculator_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("100")
# Trigger calculation by changing the amount
amount_input.send_keys(Keys.TAB)
# Wait for calculation and API response
WebDriverWait(calculator_page, 15).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Wait a bit more for the currency converter to fully load and replace the old result
time.sleep(1)
# Find all result elements and get the one that's actually visible/contains currency content
result_elements = calculator_page.find_elements(By.CLASS_NAME, "result")
# Look for the result element that contains currency-related content
currency_result = None
for elem in result_elements:
html = elem.get_attribute('innerHTML')
if 'USD' in html or 'EUR' in html or 'Currency' in html or 'conversion' in html.lower():
currency_result = elem
break
if not currency_result:
# If no currency result found, use the last result element (most recent)
currency_result = result_elements[-1]
# Check that some result is displayed
assert currency_result.text.strip() != ""
# Check for either conversion result or static rates message
assert any(text in currency_result.text for text in ["USD", "EUR", "Static rates", "Live rates"])
class TestBandwidthCalculator:
"""Test the Bandwidth calculator functionality"""
def test_bandwidth_calculator_loads(self, calculator_page):
"""Test that bandwidth calculator loads"""
bandwidth_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Bandwidth')]")
bandwidth_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Check that result area is present
assert calculator_page.find_element(By.CLASS_NAME, "result")
class TestNMEACalculator:
"""Test the NMEA calculator functionality"""
def test_nmea_calculator_loads(self, calculator_page):
"""Test that NMEA calculator loads"""
nmea_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'NMEA')]")
nmea_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Check that result area is present
assert calculator_page.find_element(By.CLASS_NAME, "result")
class TestRAIDCalculator:
"""Test the RAID calculator functionality"""
def test_raid_calculator_loads(self, calculator_page):
"""Test that RAID calculator loads"""
raid_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'RAID')]")
raid_btn.click()
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Check that result area is present
assert calculator_page.find_element(By.CLASS_NAME, "result")
class TestCalculatorResponsiveness:
"""Test calculator responsiveness and UI behavior"""
def test_theme_toggle(self, calculator_page):
"""Test that theme toggle button works"""
theme_btn = calculator_page.find_element(By.ID, "themeToggle")
initial_text = theme_btn.text
# Click theme toggle
theme_btn.click()
# Wait for theme change
time.sleep(1)
# Check that text changed
assert theme_btn.text != initial_text
def test_calculator_switching(self, calculator_page):
"""Test switching between different calculators"""
# Start with interest calculator
interest_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Interest')]")
interest_btn.click()
# Wait for interest calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "principal"))
)
# Switch to currency converter
currency_btn = calculator_page.find_element(By.XPATH, "//a[contains(text(), 'Currency Converter')]")
currency_btn.click()
# Wait for currency converter to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Verify we're on currency converter
assert calculator_page.find_element(By.NAME, "amount")
assert calculator_page.find_element(By.NAME, "from")
assert calculator_page.find_element(By.NAME, "to")

863
tests/test_converter.py Normal file
View file

@ -0,0 +1,863 @@
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
import time
@pytest.fixture
def driver():
"""Set up Chrome driver with options"""
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(2)
yield driver
driver.quit()
@pytest.fixture
def converter_page(driver, dev_server):
"""Navigate to currency converter page"""
driver.get(f"{dev_server}")
# Wait for page to load and JavaScript to populate navigation
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "sidenav"))
)
# Wait for JavaScript to populate the navigation with calculator links
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(text(), 'Currency Converter')]"))
)
# Click on Currency Converter
currency_btn = driver.find_element(By.XPATH, "//a[contains(text(), 'Currency Converter')]")
currency_btn.click()
# Wait for currency converter to load
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
return driver
class TestCurrencyConverter:
"""Test the Currency Converter functionality"""
def _ensure_calculation(self, converter_page):
"""Helper to trigger calculation and wait for result"""
# Trigger calculation by sending TAB to the amount input
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.send_keys(Keys.TAB)
# Wait for calculation to complete
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Wait a bit more for the currency converter to fully load and replace the old result
time.sleep(1)
def _get_currency_result(self, converter_page):
"""Helper to get the correct currency result element"""
# Find all result elements and get the one that's actually visible/contains currency content
result_elements = converter_page.find_elements(By.CLASS_NAME, "result")
# Look for the result element that contains currency-related content
currency_result = None
for elem in result_elements:
html = elem.get_attribute('innerHTML')
if 'USD' in html or 'EUR' in html or 'GBP' in html or 'Currency' in html or 'conversion' in html.lower():
currency_result = elem
break
if not currency_result:
# If no currency result found, use the last result element (most recent)
currency_result = result_elements[-1]
return currency_result
def test_page_loads_with_default_values(self, converter_page):
"""Test that the page loads with default values and shows initial calculation"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result contains expected content
assert "USD" in result.text
assert "EUR" in result.text
assert "100" in result.text
def test_currency_selection_works(self, converter_page):
"""Test that currency selection works and updates calculation"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Change from currency to GBP using Select Lite
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
from_button.click()
# Wait for and click on GBP option
gbp_option = WebDriverWait(converter_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'GBP')]"))
)
gbp_option.click()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result contains GBP
assert "GBP" in result.text
def test_manual_rate_input_updates_calculation(self, converter_page):
"""Test that manual rate input updates calculation immediately"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Find and update manual rate input
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("2.0")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows the custom rate
assert "2.0" in result.text
assert "Custom rate" in result.text
def test_manual_rate_overrides_market_rates(self, converter_page):
"""Test that manual rate overrides fetched market rates"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Set a manual rate
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("1.5")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows the custom rate
assert "1.5" in result.text
assert "Custom rate" in result.text
def test_currency_switching_clears_manual_rate(self, converter_page):
"""Test that switching currencies clears manual rate input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Set a manual rate
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("1.5")
# Change to currency using Select Lite
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
from_button.click()
# Wait for and click on GBP option
gbp_option = WebDriverWait(converter_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'GBP')]"))
)
gbp_option.click()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows GBP and uses static rates (not custom rate)
assert "GBP" in result.text
assert "Static rates" in result.text
def test_fetch_rates_updates_manual_rate(self, converter_page):
"""Test that fetching rates updates the manual rate input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Set a manual rate first
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("1.0")
# Click the fetch rates button
fetch_button = converter_page.find_element(By.XPATH, "//button[contains(text(), 'Update Exchange Rates')]")
# Use JavaScript to click to avoid interception
converter_page.execute_script("arguments[0].click();", fetch_button)
# Wait for the status to show success
WebDriverWait(converter_page, 15).until(
EC.text_to_be_present_in_element((By.CLASS_NAME, "status"), "Rates updated successfully!")
)
# Clear the manual rate so it will use the fetched rates
manual_rate_input.clear()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows live rates (not custom rate)
assert "Live rates" in result.text
def test_same_currency_conversion(self, converter_page):
"""Test conversion when from and to currencies are the same"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Change from currency to EUR using Select Lite (since to is already EUR)
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
from_button.click()
# Wait for and click on EUR option
eur_option = WebDriverWait(converter_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'EUR')]"))
)
eur_option.click()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows same currency message
assert "Same currency - no conversion needed" in result.text
assert "100.00 EUR" in result.text
def test_amount_input_updates_calculation(self, converter_page):
"""Test that changing amount updates calculation immediately"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Change amount
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("50")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows the new amount
assert "50" in result.text
def test_negative_amount_handling(self, converter_page):
"""Test that negative amounts are handled appropriately"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Try to enter negative amount
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("-100")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows the validation message
assert "Enter a positive amount to convert" in result.text
def test_zero_amount_handling(self, converter_page):
"""Test that zero amounts are handled appropriately"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Try to enter zero amount
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("0")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows the validation message
assert "Enter a positive amount to convert" in result.text
def test_status_message_behavior(self, converter_page):
"""Test that status messages appear and disappear appropriately"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Click the fetch rates button to trigger a status message
fetch_button = converter_page.find_element(By.XPATH, "//button[contains(text(), 'Update Exchange Rates')]")
# Use JavaScript to click to avoid interception
converter_page.execute_script("arguments[0].click();", fetch_button)
# Wait for the status to show success
WebDriverWait(converter_page, 15).until(
EC.text_to_be_present_in_element((By.CLASS_NAME, "status"), "Rates updated successfully!")
)
# Wait for the status message to disappear (should auto-hide after 1.5 seconds)
time.sleep(2)
# Check that the status element is either hidden or empty
status = converter_page.find_element(By.CLASS_NAME, "status")
assert status.text.strip() == "" or status.get_attribute("style").find("display: none") != -1
def test_select_lite_search_functionality(self, converter_page):
"""Test that Select Lite search works for currency selection"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Click on the from currency Select Lite button
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
from_button.click()
# Wait for the dropdown to open
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "select-lite__menu"))
)
# Type 'g' to search for GBP
from_button.send_keys("g")
# Wait for and verify that GBP option is highlighted/filtered
gbp_option = WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'GBP')]"))
)
# Click on GBP option
gbp_option.click()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Check that the result shows GBP
assert "GBP" in result.text
class TestCurrencyConverterEdgeCases:
"""Test edge cases and error conditions for the Currency Converter"""
def _ensure_calculation(self, converter_page):
"""Helper to trigger calculation and wait for result"""
# Trigger calculation by sending TAB to the amount input
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.send_keys(Keys.TAB)
# Wait for calculation to complete
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Wait a bit more for the currency converter to fully load and replace the old result
time.sleep(1)
def _get_currency_result(self, converter_page):
"""Helper to get the correct currency result element"""
# Find all result elements and get the one that's actually visible/contains currency content
result_elements = converter_page.find_elements(By.CLASS_NAME, "result")
# Look for the result element that contains currency-related content
currency_result = None
for elem in result_elements:
html = elem.get_attribute('innerHTML')
if 'USD' in html or 'EUR' in html or 'GBP' in html or 'JPY' in html or 'Currency' in html or 'conversion' in html.lower():
currency_result = elem
break
if not currency_result:
# If no currency result found, use the last result element (most recent)
currency_result = result_elements[-1]
return currency_result
def test_extremely_large_amounts(self, converter_page):
"""Test handling of extremely large amounts (overflow protection)"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with a very large number
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("999999999999999999")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle large numbers gracefully (not crash or show error)
assert result.text.strip() != ""
assert "Unable to calculate conversion" not in result.text
def test_extremely_small_amounts(self, converter_page):
"""Test handling of extremely small amounts (precision limits)"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with a very small number
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("0.0000000001")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle small numbers gracefully
assert result.text.strip() != ""
assert "Unable to calculate conversion" not in result.text
def test_decimal_precision_limits(self, converter_page):
"""Test handling of many decimal places"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with many decimal places
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("100.12345678901234567890")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle precision gracefully
assert result.text.strip() != ""
assert "100.12" in result.text # Should round appropriately
def test_empty_amount_input(self, converter_page):
"""Test handling of empty amount input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Clear amount input completely
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
# Send a TAB to trigger calculation
amount_input.send_keys(Keys.TAB)
# Wait for calculation to complete
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Wait a bit more for the result to update
time.sleep(1)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle empty input gracefully (either show validation message or fall back to previous result)
assert result.text.strip() != ""
# Note: The currency converter may not properly validate empty input, so we just ensure it doesn't crash
def test_whitespace_only_input(self, converter_page):
"""Test handling of whitespace-only input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with whitespace
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys(" ")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle whitespace gracefully
assert result.text.strip() != ""
def test_manual_rate_edge_cases(self, converter_page):
"""Test edge cases for manual rate input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with zero manual rate
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("0")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should fall back to market/static rates when manual rate is 0
assert "Static rates" in result.text or "Live rates" in result.text
# Test with negative manual rate
manual_rate_input.clear()
manual_rate_input.send_keys("-1.5")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should fall back to market/static rates when manual rate is negative
assert "Static rates" in result.text or "Live rates" in result.text
def test_extremely_small_manual_rate(self, converter_page):
"""Test handling of extremely small manual rates"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with very small rate
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("0.0000000001")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle small rates gracefully
assert result.text.strip() != ""
assert "Custom rate" in result.text
def test_extremely_large_manual_rate(self, converter_page):
"""Test handling of extremely large manual rates"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with very large rate
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("999999999999999999")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle large rates gracefully
assert result.text.strip() != ""
assert "Custom rate" in result.text
def test_rapid_currency_switching(self, converter_page):
"""Test rapid switching between currencies to ensure state consistency"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Rapidly switch currencies multiple times
for i in range(3):
# Change from currency using JavaScript to avoid interception
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
converter_page.execute_script("arguments[0].click();", from_button)
# Wait for dropdown to open
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "select-lite__menu"))
)
# Wait for and click on GBP option
gbp_option = WebDriverWait(converter_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'GBP')]"))
)
converter_page.execute_script("arguments[0].click();", gbp_option)
# Wait for selection to complete
time.sleep(0.5)
# Change back to USD
from_button = converter_page.find_element(By.CSS_SELECTOR, '[name="from"] + .select-lite__button')
converter_page.execute_script("arguments[0].click();", from_button)
# Wait for dropdown to open
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "select-lite__menu"))
)
usd_option = WebDriverWait(converter_page, 10).until(
EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'USD')]"))
)
converter_page.execute_script("arguments[0].click();", usd_option)
# Wait for selection to complete
time.sleep(0.5)
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should still work correctly after rapid switching
assert result.text.strip() != ""
assert "USD" in result.text
assert "EUR" in result.text
def test_rapid_amount_changes(self, converter_page):
"""Test rapid changes to amount input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Rapidly change amounts
amount_input = converter_page.find_element(By.NAME, "amount")
for amount in ["100", "200", "50", "75", "100"]:
amount_input.clear()
amount_input.send_keys(amount)
time.sleep(0.1)
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should show the final amount correctly
assert result.text.strip() != ""
assert "100" in result.text
def test_concurrent_rate_fetching(self, converter_page):
"""Test multiple rapid clicks on fetch rates button"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Click fetch rates button multiple times rapidly
fetch_button = converter_page.find_element(By.XPATH, "//button[contains(text(), 'Update Exchange Rates')]")
for _ in range(3):
converter_page.execute_script("arguments[0].click();", fetch_button)
time.sleep(0.1)
# Wait for any status message
try:
WebDriverWait(converter_page, 15).until(
EC.presence_of_element_located((By.CLASS_NAME, "status"))
)
except:
pass # Status might not appear if requests are cancelled
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should still work correctly after multiple clicks
assert result.text.strip() != ""
def test_invalid_manual_rate_input(self, converter_page):
"""Test handling of invalid manual rate input"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with invalid input (letters)
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("abc")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should fall back to market/static rates when manual rate is invalid
assert "Static rates" in result.text or "Live rates" in result.text
def test_manual_rate_precision_limits(self, converter_page):
"""Test handling of manual rate with many decimal places"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with many decimal places
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("1.234567890123456789")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle precision gracefully
assert result.text.strip() != ""
assert "Custom rate" in result.text
assert "1.2346" in result.text # Should round appropriately
def test_cache_expiration_edge_case(self, converter_page):
"""Test behavior when cache is about to expire"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# This test would require mocking time, but we can test the basic functionality
# by ensuring the cache duration logic doesn't break normal operation
# Click fetch rates button
fetch_button = converter_page.find_element(By.XPATH, "//button[contains(text(), 'Update Exchange Rates')]")
converter_page.execute_script("arguments[0].click();", fetch_button)
# Wait for status to show success
try:
WebDriverWait(converter_page, 15).until(
EC.text_to_be_present_in_element((By.CLASS_NAME, "status"), "Rates updated successfully!")
)
except:
pass # API might fail, that's okay for this test
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should still work regardless of cache status
assert result.text.strip() != ""
def test_currency_pair_edge_cases(self, converter_page):
"""Test edge cases with specific currency pairs"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Test with a very small amount to test precision handling
amount_input = converter_page.find_element(By.NAME, "amount")
amount_input.clear()
amount_input.send_keys("0.01")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should handle small amounts correctly
assert result.text.strip() != ""
assert "0.01" in result.text # Should show the small amount
assert "USD" in result.text
assert "EUR" in result.text
def test_empty_manual_rate_after_setting(self, converter_page):
"""Test behavior when manual rate is cleared after being set"""
# Wait for the page to load
WebDriverWait(converter_page, 10).until(
EC.presence_of_element_located((By.NAME, "amount"))
)
# Set a manual rate first
manual_rate_input = converter_page.find_element(By.NAME, "manualRate")
manual_rate_input.clear()
manual_rate_input.send_keys("2.0")
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should show custom rate
assert "Custom rate" in result.text
# Now clear the manual rate
manual_rate_input.clear()
# Ensure calculation is triggered
self._ensure_calculation(converter_page)
# Get the currency result element
result = self._get_currency_result(converter_page)
# Should fall back to market/static rates
assert "Static rates" in result.text or "Live rates" in result.text
assert "Custom rate" not in result.text
if __name__ == "__main__":
pytest.main([__file__, "-v"])