Skip to content

Commit 9eb7968

Browse files
test: Add US04 tests for current weighted average calculation
1 parent 22b89d7 commit 9eb7968

File tree

2 files changed

+287
-2
lines changed

2 files changed

+287
-2
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ jobs:
111111
mkdir -p tests/screenshots
112112
113113
cd tests
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
114+
python -m pytest -v --html=report.html us1.py us2.py us3.py us4.py
116115
env:
117116
APP_URL: 'http://localhost:3000'
118117
PYTHONPATH: ${{ github.workspace }}

tests/us4.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
\
2+
import unittest
3+
import time
4+
import os
5+
import logging
6+
from selenium import webdriver
7+
from selenium.webdriver.common.by import By
8+
from selenium.webdriver.support.ui import WebDriverWait
9+
from selenium.webdriver.support import expected_conditions as EC
10+
from selenium.common.exceptions import TimeoutException, NoSuchElementException
11+
12+
logger = logging.getLogger(__name__)
13+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14+
15+
class US04Tests(unittest.TestCase):
16+
BASE_URL = "http://localhost:3000"
17+
GRADE_INPUT_SELECTOR = "input.home__input[placeholder='0.0'][type='number']"
18+
PERCENTAGE_INPUT_SELECTOR = "input.home__input[placeholder='0'][type='number']"
19+
ADD_GRADE_BUTTON_SELECTOR = "button.home__add-button"
20+
GRADES_LIST_ITEM_SELECTOR = "div.home__grades-container > div.home__grade-row"
21+
CALCULATE_BUTTON_SELECTOR = "button.home__calculate-button"
22+
# Hypothetical selector based on selenium-test-dev.md. May need verification.
23+
CURRENT_AVERAGE_DISPLAY_SELECTOR = "#current-average-display"
24+
25+
FIRST_TIME_ALERT_BUTTON_SELECTOR = ".alert__button.alert__button--single"
26+
ALERT_OVERLAY_SELECTOR = "div.alert__overlay"
27+
NAV_BACK_BUTTON_XPATH = "//button[contains(@class, 'nav-bar__button') and .//span[contains(@class, 'back-icon')]/svg[contains(@class, 'lucide-chevron-left')]]"
28+
HOME_CONTAINER_SELECTOR = "div.home__container"
29+
30+
def set_driver_fixture(self, driver):
31+
self.driver = driver
32+
self.wait_short = WebDriverWait(self.driver, 5)
33+
self.wait_long = WebDriverWait(self.driver, 15)
34+
if not os.path.exists("screenshots"):
35+
os.makedirs("screenshots")
36+
37+
def setUp(self):
38+
if not hasattr(self, 'driver') or not self.driver:
39+
logger.info("WebDriver not set by fixture, attempting fallback setup for direct unittest execution.")
40+
try:
41+
options = webdriver.ChromeOptions()
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)
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+
nav_back_button = WebDriverWait(self.driver, 10).until(
98+
EC.element_to_be_clickable((By.XPATH, self.NAV_BACK_BUTTON_XPATH))
99+
)
100+
nav_back_button.click()
101+
logger.info("Clicked nav back button to return to Home page.")
102+
103+
WebDriverWait(self.driver, 10).until(
104+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
105+
)
106+
logger.info("Successfully navigated back to the Home page.")
107+
108+
except TimeoutException:
109+
logger.info("First-time user alert or navigation elements not found or timed out. Assuming already on Home page or flow is different.")
110+
if self.HOME_CONTAINER_SELECTOR not in self.driver.page_source:
111+
self.driver.get(self.BASE_URL)
112+
WebDriverWait(self.driver, 10).until(
113+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
114+
)
115+
logger.info("Re-navigated to Home page as a fallback.")
116+
except Exception as e:
117+
logger.error(f"An unexpected error occurred during initial setup: {e}")
118+
self._take_screenshot("initial_setup_error")
119+
120+
def _add_grade_and_percentage(self, grade, percentage):
121+
if not hasattr(self, 'driver') or not self.driver:
122+
logger.error("Driver not available in _add_grade_and_percentage.")
123+
self.fail("Driver not available.")
124+
return
125+
126+
grade_rows = self.driver.find_elements(By.CSS_SELECTOR, self.GRADES_LIST_ITEM_SELECTOR)
127+
if not grade_rows: # Should not happen if app starts with one row
128+
logger.error("No grade rows found to add grade and percentage.")
129+
self._take_screenshot("no_grade_rows_found")
130+
# Attempt to click "Agregar nota" to see if it generates the first row
131+
try:
132+
add_button_global = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.ADD_GRADE_BUTTON_SELECTOR)))
133+
add_button_global.click()
134+
time.sleep(0.5) # wait for row to be added
135+
grade_rows = self.driver.find_elements(By.CSS_SELECTOR, self.GRADES_LIST_ITEM_SELECTOR)
136+
if not grade_rows:
137+
self.fail("Still no grade rows found after attempting to add one.")
138+
return
139+
except Exception as ex:
140+
logger.error(f"Failed to create an initial grade row: {ex}")
141+
self.fail("Failed to create an initial grade row.")
142+
return
143+
144+
145+
last_row = grade_rows[-1]
146+
logger.info(f"Targeting the last of {len(grade_rows)} grade rows for input.")
147+
148+
try:
149+
grade_input_element = last_row.find_element(By.CSS_SELECTOR, self.GRADE_INPUT_SELECTOR)
150+
percentage_input_element = last_row.find_element(By.CSS_SELECTOR, self.PERCENTAGE_INPUT_SELECTOR)
151+
except NoSuchElementException as e:
152+
logger.error(f"Could not find grade or percentage input in the last row: {e}")
153+
logger.info(f"HTML of last row: {last_row.get_attribute('outerHTML')}")
154+
self._take_screenshot("input_not_found_in_last_row")
155+
self.fail("Input elements not found in the last row.")
156+
return
157+
158+
add_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.ADD_GRADE_BUTTON_SELECTOR)))
159+
160+
grade_input_element.clear()
161+
grade_input_element.send_keys(str(grade))
162+
percentage_input_element.clear()
163+
percentage_input_element.send_keys(str(percentage))
164+
165+
add_button.click()
166+
logger.info(f"Clicked 'Agregar nota' after filling grade: {grade}, percentage: {percentage} into the last available row.")
167+
time.sleep(0.5)
168+
169+
def _get_current_weighted_average(self):
170+
raw_text = ""
171+
try:
172+
average_element = self.wait_long.until(
173+
EC.visibility_of_element_located((By.CSS_SELECTOR, self.CURRENT_AVERAGE_DISPLAY_SELECTOR))
174+
)
175+
# Add a small delay or check for text to be non-empty if necessary
176+
time.sleep(0.2) # give a brief moment for text to update
177+
178+
raw_text = average_element.text.strip()
179+
attempts = 0
180+
while raw_text == "" and attempts < 5: # Retry if empty
181+
time.sleep(0.3)
182+
raw_text = average_element.text.strip()
183+
attempts += 1
184+
185+
logger.info(f"Raw current weighted average text: '{raw_text}'")
186+
if not raw_text:
187+
logger.warning("Current weighted average text is empty after retries.")
188+
self._take_screenshot("empty_current_average")
189+
return "Error: Empty Value"
190+
191+
# Assuming the text is just a number, possibly with a suffix like "/ 5.0" or similar.
192+
# For now, let's assume it's a direct float value.
193+
# If it has " / X.X", we might need to parse it.
194+
# Example: "3.4 / 5.0" -> we need 3.4
195+
# For now, try direct conversion. If it fails, the app might display it differently.
196+
# The Subject.ts finalGrade is Number((weightedSum / 100).toFixed(1))
197+
# So it should be a direct number like "1.7".
198+
199+
# If the text might contain other characters like "Promedio: 1.7", parse accordingly.
200+
# For now, assuming it's just the number.
201+
return float(raw_text)
202+
except TimeoutException:
203+
logger.error(f"Timeout waiting for current weighted average display element: {self.CURRENT_AVERAGE_DISPLAY_SELECTOR}")
204+
logger.info(f"Page source at timeout:\\n{self.driver.page_source[:2000]}") # Log part of page source
205+
self._take_screenshot("current_average_timeout")
206+
return "Error: Timeout"
207+
except ValueError:
208+
logger.error(f"Could not convert current weighted average text '{raw_text}' to float.")
209+
self._take_screenshot("current_average_value_error")
210+
return "Error: Conversion"
211+
except Exception as e:
212+
logger.error(f"Error getting current weighted average: {e}")
213+
self._take_screenshot("get_current_average_error")
214+
return "Error: General"
215+
216+
# US04: Cálculo del Promedio Ponderado Actual
217+
def test_us04_verify_calculation_of_current_weighted_average(self):
218+
test_name = self._testMethodName
219+
logger.info(f"Running test: {test_name}")
220+
221+
try:
222+
# Click "Calcular" to see initial state if any (likely 0 or not shown)
223+
# For this test, we add grades first, then calculate and check.
224+
225+
# 1. Add first grade and verify average
226+
logger.info("Adding first grade (4.5, 20%).")
227+
self._add_grade_and_percentage("4.5", "20") # Adds and clicks "Agregar nota"
228+
229+
# Click "Calcular"
230+
calculate_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.CALCULATE_BUTTON_SELECTOR)))
231+
calculate_button.click()
232+
logger.info("Clicked 'Calcular' button.")
233+
time.sleep(0.5) # Wait for calculation and display update
234+
235+
current_avg_1 = self._get_current_weighted_average()
236+
expected_avg_1 = round((4.5 * 20) / 100, 1) # 0.9
237+
self.assertEqual(current_avg_1, expected_avg_1,
238+
f"Average after 1st grade expected {expected_avg_1}, but got {current_avg_1}")
239+
logger.info(f"Average after 1st grade is correct: {current_avg_1}")
240+
241+
# 2. Add second grade and verify average
242+
# _add_grade_and_percentage already clicked "Agregar nota", so a new row should be ready.
243+
logger.info("Adding second grade (3.0, 30%).")
244+
self._add_grade_and_percentage("3.0", "30")
245+
246+
calculate_button.click() # Re-click calculate
247+
logger.info("Clicked 'Calcular' button again.")
248+
time.sleep(0.5)
249+
250+
current_avg_2 = self._get_current_weighted_average()
251+
expected_avg_2 = round((4.5 * 20 + 3.0 * 30) / 100, 1) # (90 + 90)/100 = 1.8
252+
self.assertEqual(current_avg_2, expected_avg_2,
253+
f"Average after 2nd grade expected {expected_avg_2}, but got {current_avg_2}")
254+
logger.info(f"Average after 2nd grade is correct: {current_avg_2}")
255+
256+
# 3. Add third grade and verify average
257+
logger.info("Adding third grade (5.0, 50%).")
258+
self._add_grade_and_percentage("5.0", "50")
259+
260+
calculate_button.click() # Re-click calculate
261+
logger.info("Clicked 'Calcular' button again.")
262+
time.sleep(0.5)
263+
264+
current_avg_3 = self._get_current_weighted_average()
265+
expected_avg_3 = round((4.5 * 20 + 3.0 * 30 + 5.0 * 50) / 100, 1) # (90 + 90 + 250)/100 = 430/100 = 4.3
266+
self.assertEqual(current_avg_3, expected_avg_3,
267+
f"Average after 3rd grade expected {expected_avg_3}, but got {current_avg_3}")
268+
logger.info(f"Average after 3rd grade is correct: {current_avg_3}")
269+
270+
except AssertionError as e:
271+
logger.error(f"AssertionError in {test_name}: {e}")
272+
self._take_screenshot(f"{test_name}_assertion_failure")
273+
# Log page source if average display is not found or value is unexpected
274+
if "current_average_timeout" in str(e).lower() or "Error: Conversion" in str(e) or "Error: Empty Value" in str(e):
275+
logger.info(f"Page source at error:\\n{self.driver.page_source[:3000]}")
276+
raise
277+
except Exception as e:
278+
logger.error(f"An unexpected error occurred in {test_name}: {e}")
279+
self._take_screenshot(f"{test_name}_unexpected_error")
280+
logger.info(f"Page source at error:\\n{self.driver.page_source[:3000]}")
281+
raise
282+
283+
logger.info(f"Test {test_name} completed successfully.")
284+
285+
if __name__ == '__main__':
286+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)