Skip to content

Commit b3a8679

Browse files
test: Added test for US3
1 parent ee7c2d3 commit b3a8679

File tree

2 files changed

+260
-2
lines changed

2 files changed

+260
-2
lines changed

.github/workflows/python-selenium-ci-workflow.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ jobs:
111111
mkdir -p tests/screenshots
112112
113113
cd tests
114-
# Explicitly call us1.py and us2.py, add HTML report generation
115-
python -m pytest -v --html=report.html us1.py us2.py
114+
# Explicitly call us1.py, us2.py and us3.py, add HTML report generation
115+
python -m pytest -v --html=report.html us1.py us2.py us3.py
116116
env:
117117
APP_URL: 'http://localhost:3000'
118118
PYTHONPATH: ${{ github.workspace }}

tests/us3.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import unittest
2+
import time
3+
import os
4+
import logging
5+
from selenium import webdriver
6+
from selenium.webdriver.common.by import By
7+
from selenium.webdriver.support.ui import WebDriverWait
8+
from selenium.webdriver.support import expected_conditions as EC
9+
from selenium.common.exceptions import TimeoutException, NoSuchElementException
10+
11+
logger = logging.getLogger(__name__)
12+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
13+
14+
class US03Tests(unittest.TestCase):
15+
BASE_URL = "http://localhost:3000"
16+
GRADE_INPUT_SELECTOR = "input.home__input[placeholder='0.0'][type='number']"
17+
PERCENTAGE_INPUT_SELECTOR = "input.home__input[placeholder='0'][type='number']"
18+
ADD_GRADE_BUTTON_SELECTOR = "button.home__add-button"
19+
GRADES_LIST_ITEM_SELECTOR = "div.home__grades-container > div.home__grade-row"
20+
REMAINING_PERCENTAGE_DISPLAY_SELECTOR = "div.home__results-card p.home__card-text span"
21+
22+
FIRST_TIME_ALERT_BUTTON_SELECTOR = ".alert__button.alert__button--single"
23+
ALERT_OVERLAY_SELECTOR = "div.alert__overlay"
24+
NAV_BACK_BUTTON_XPATH = "//button[contains(@class, 'nav-bar__button') and .//span[contains(@class, 'back-icon')]/svg[contains(@class, 'lucide-chevron-left')]]"
25+
HOME_CONTAINER_SELECTOR = "div.home__container"
26+
27+
def set_driver_fixture(self, driver):
28+
self.driver = driver
29+
self.wait_short = WebDriverWait(self.driver, 5)
30+
self.wait_long = WebDriverWait(self.driver, 15)
31+
if not os.path.exists("screenshots"):
32+
os.makedirs("screenshots")
33+
34+
def setUp(self):
35+
# This setup is primarily for direct unittest execution.
36+
# Pytest will use the fixture from conftest.py
37+
if not hasattr(self, 'driver') or not self.driver:
38+
logger.info("WebDriver not set by fixture, attempting fallback setup for direct unittest execution.")
39+
try:
40+
options = webdriver.ChromeOptions()
41+
# Add any desired options here, e.g., headless
42+
# options.add_argument('--headless')
43+
# options.add_argument('--disable-gpu')
44+
self.driver = webdriver.Chrome(options=options)
45+
self.set_driver_fixture(self.driver) # Call to setup waits and screenshot dir
46+
self.is_driver_managed_by_fallback = True
47+
logger.info("Fallback WebDriver initialized for direct unittest execution.")
48+
except Exception as e:
49+
logger.error(f"Failed to initialize fallback WebDriver: {e}")
50+
self.fail(f"Failed to initialize fallback WebDriver: {e}")
51+
else:
52+
logger.info("WebDriver already set, likely by a pytest fixture.")
53+
self.is_driver_managed_by_fallback = False
54+
self._initial_setup()
55+
56+
def tearDown(self):
57+
if hasattr(self, 'is_driver_managed_by_fallback') and self.is_driver_managed_by_fallback:
58+
if self.driver:
59+
self.driver.quit()
60+
logger.info("Fallback WebDriver quit.")
61+
else:
62+
logger.info("Driver teardown managed by pytest fixture (if applicable).")
63+
64+
def _take_screenshot(self, name_suffix):
65+
timestamp = int(time.time())
66+
test_method_name = getattr(self, '_testMethodName', 'unknown_test')
67+
screenshot_name = f"screenshots/{test_method_name}_{name_suffix}_{timestamp}.png"
68+
try:
69+
if hasattr(self, 'driver') and self.driver:
70+
self.driver.save_screenshot(screenshot_name)
71+
logger.info(f"Screenshot saved: {screenshot_name}")
72+
except Exception as e:
73+
logger.error(f"Error saving screenshot {screenshot_name}: {e}")
74+
75+
def _initial_setup(self):
76+
if not hasattr(self, 'driver') or not self.driver:
77+
logger.error("Driver not initialized in _initial_setup. Aborting setup.")
78+
self.fail("Driver not initialized for test setup.")
79+
return
80+
81+
self.driver.get(self.BASE_URL)
82+
logger.info(f"Navigated to base URL: {self.BASE_URL}")
83+
84+
try:
85+
logger.info(f"Attempting to handle first-time alert with button '{self.FIRST_TIME_ALERT_BUTTON_SELECTOR}'.")
86+
alert_button = WebDriverWait(self.driver, 10).until(
87+
EC.element_to_be_clickable((By.CSS_SELECTOR, self.FIRST_TIME_ALERT_BUTTON_SELECTOR))
88+
)
89+
alert_button.click()
90+
logger.info(f"Clicked first-time alert button: '{self.FIRST_TIME_ALERT_BUTTON_SELECTOR}'.")
91+
92+
WebDriverWait(self.driver, 5).until(
93+
EC.invisibility_of_element_located((By.CSS_SELECTOR, self.ALERT_OVERLAY_SELECTOR))
94+
)
95+
logger.info(f"Alert overlay '{self.ALERT_OVERLAY_SELECTOR}' is no longer visible. App should be on Settings page.")
96+
97+
# Navigate back to home page
98+
nav_back_button = WebDriverWait(self.driver, 10).until(
99+
EC.element_to_be_clickable((By.XPATH, self.NAV_BACK_BUTTON_XPATH))
100+
)
101+
nav_back_button.click()
102+
logger.info("Clicked nav back button to return to Home page.")
103+
104+
WebDriverWait(self.driver, 10).until(
105+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
106+
)
107+
logger.info("Successfully navigated back to the Home page.")
108+
109+
except TimeoutException:
110+
logger.info("First-time user alert or navigation elements not found or timed out. Assuming already on Home page or flow is different.")
111+
# Check if we are on the home page, if not, try to navigate.
112+
if self.HOME_CONTAINER_SELECTOR not in self.driver.page_source:
113+
self.driver.get(self.BASE_URL) # Re-navigate if something went wrong
114+
WebDriverWait(self.driver, 10).until(
115+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
116+
)
117+
logger.info("Re-navigated to Home page as a fallback.")
118+
except Exception as e:
119+
logger.error(f"An unexpected error occurred during initial setup: {e}")
120+
self._take_screenshot("initial_setup_error")
121+
# self.fail(f"Critical error during initial setup: {e}") # Decide if this should fail the test or just log
122+
123+
def _add_grade_and_percentage(self, grade, percentage):
124+
if not hasattr(self, 'driver') or not self.driver:
125+
logger.error("Driver not available in _add_grade_and_percentage.")
126+
self.fail("Driver not available.")
127+
return
128+
129+
grade_rows = self.driver.find_elements(By.CSS_SELECTOR, self.GRADES_LIST_ITEM_SELECTOR)
130+
if not grade_rows:
131+
logger.error("No grade rows found to add grade and percentage.")
132+
self._take_screenshot("no_grade_rows_found")
133+
self.fail("No grade rows found.")
134+
return
135+
136+
last_row = grade_rows[-1]
137+
logger.info(f"Targeting the last of {len(grade_rows)} grade rows for input.")
138+
139+
try:
140+
grade_input_element = last_row.find_element(By.CSS_SELECTOR, self.GRADE_INPUT_SELECTOR)
141+
percentage_input_element = last_row.find_element(By.CSS_SELECTOR, self.PERCENTAGE_INPUT_SELECTOR)
142+
except NoSuchElementException as e:
143+
logger.error(f"Could not find grade or percentage input in the last row: {e}")
144+
logger.info(f"HTML of last row: {last_row.get_attribute('outerHTML')}")
145+
self._take_screenshot("input_not_found_in_last_row")
146+
self.fail("Input elements not found in the last row.")
147+
return
148+
149+
add_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.ADD_GRADE_BUTTON_SELECTOR)))
150+
151+
grade_input_element.clear()
152+
grade_input_element.send_keys(str(grade))
153+
percentage_input_element.clear()
154+
percentage_input_element.send_keys(str(percentage))
155+
156+
add_button.click()
157+
logger.info(f"Clicked 'Agregar nota' after attempting to add grade: {grade}, percentage: {percentage} to the last row.")
158+
# Wait for UI to update, especially if new rows are added dynamically.
159+
# A more robust wait would be for the number of rows to change or for a specific element to appear.
160+
time.sleep(0.5) # Small delay for React state updates
161+
162+
def _get_remaining_percentage(self):
163+
cleaned_text = "" # Initialize cleaned_text
164+
try:
165+
# Wait for the element to be present and contain some text
166+
percentage_element = self.wait_long.until(
167+
EC.presence_of_element_located((By.CSS_SELECTOR, self.REMAINING_PERCENTAGE_DISPLAY_SELECTOR))
168+
)
169+
# Add a small delay or check for text to be non-empty if necessary
170+
time.sleep(0.2) # give a brief moment for text to update
171+
172+
# Attempt to get text, retry if it's empty initially due to rendering delays
173+
text_value = percentage_element.text.strip()
174+
attempts = 0
175+
while text_value == "" and attempts < 5: # Retry if empty
176+
time.sleep(0.3)
177+
text_value = percentage_element.text.strip()
178+
attempts += 1
179+
180+
logger.info(f"Raw remaining percentage text: '{text_value}'")
181+
if not text_value: # If still empty after retries
182+
logger.warning("Remaining percentage text is empty after retries.")
183+
self._take_screenshot("empty_remaining_percentage")
184+
return "Error: Empty Value" # Or raise an error
185+
186+
# Assuming the text is like "XX %" or "XX%", we remove "%" and convert to float
187+
# If it's just "XX", this will also work.
188+
cleaned_text = text_value.replace('%', '').strip()
189+
return float(cleaned_text)
190+
except TimeoutException:
191+
logger.error(f"Timeout waiting for remaining percentage display element: {self.REMAINING_PERCENTAGE_DISPLAY_SELECTOR}")
192+
self._take_screenshot("remaining_percentage_timeout")
193+
return "Error: Timeout" # Or raise an error
194+
except ValueError:
195+
logger.error(f"Could not convert remaining percentage text '{cleaned_text}' to float.") # cleaned_text is now defined
196+
self._take_screenshot("remaining_percentage_value_error")
197+
return "Error: Conversion" # Or raise an error
198+
except Exception as e:
199+
logger.error(f"Error getting remaining percentage: {e}")
200+
self._take_screenshot("get_remaining_percentage_error")
201+
return "Error: General" # Or raise an error
202+
203+
# US03: Calculo del porcentaje faltante
204+
def test_us03_verify_calculation_of_remaining_percentage(self):
205+
test_name = self._testMethodName
206+
logger.info(f"Running test: {test_name}")
207+
208+
try:
209+
# 1. Initial state: Verify remaining percentage is 100%
210+
logger.info("Verifying initial remaining percentage.")
211+
initial_remaining = self._get_remaining_percentage()
212+
self.assertEqual(initial_remaining, 100.0,
213+
f"Initial remaining percentage expected 100.0, but got {initial_remaining}")
214+
logger.info(f"Initial remaining percentage is correct: {initial_remaining}%")
215+
216+
# 2. Add one grade and verify
217+
logger.info("Adding first grade (20%).")
218+
self._add_grade_and_percentage("3.0", "20")
219+
# It might take a moment for the calculation to update
220+
time.sleep(0.5) # Wait for calculation update
221+
remaining_after_first_grade = self._get_remaining_percentage()
222+
self.assertEqual(remaining_after_first_grade, 80.0,
223+
f"Remaining percentage after 1st grade (20%) expected 80.0, but got {remaining_after_first_grade}")
224+
logger.info(f"Remaining percentage after 1st grade (20%) is correct: {remaining_after_first_grade}%")
225+
226+
# 3. Add a second grade and verify
227+
logger.info("Adding second grade (30%). Total 50%.")
228+
self._add_grade_and_percentage("4.0", "30")
229+
time.sleep(0.5) # Wait for calculation update
230+
remaining_after_second_grade = self._get_remaining_percentage()
231+
self.assertEqual(remaining_after_second_grade, 50.0,
232+
f"Remaining percentage after 2nd grade (total 50%) expected 50.0, but got {remaining_after_second_grade}")
233+
logger.info(f"Remaining percentage after 2nd grade (total 50%) is correct: {remaining_after_second_grade}%")
234+
235+
# 4. Add a third grade to make total 100% and verify
236+
logger.info("Adding third grade (50%). Total 100%.")
237+
self._add_grade_and_percentage("5.0", "50")
238+
time.sleep(0.5) # Wait for calculation update
239+
remaining_after_third_grade = self._get_remaining_percentage()
240+
self.assertEqual(remaining_after_third_grade, 0.0,
241+
f"Remaining percentage after 3rd grade (total 100%) expected 0.0, but got {remaining_after_third_grade}")
242+
logger.info(f"Remaining percentage after 3rd grade (total 100%) is correct: {remaining_after_third_grade}%")
243+
244+
except AssertionError as e:
245+
logger.error(f"AssertionError in {test_name}: {e}")
246+
self._take_screenshot(f"{test_name}_assertion_failure")
247+
raise # Re-raise the assertion error to fail the test
248+
except Exception as e:
249+
logger.error(f"An unexpected error occurred in {test_name}: {e}")
250+
self._take_screenshot(f"{test_name}_unexpected_error")
251+
raise # Re-raise to fail the test
252+
253+
logger.info(f"Test {test_name} completed successfully.")
254+
255+
if __name__ == '__main__':
256+
# This allows running the test file directly with `python us3.py`
257+
# For more comprehensive test runs, use pytest.
258+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)