Skip to content

Commit 92bca75

Browse files
test: Add US06 tests for final status verification scenarios
1 parent 2861443 commit 92bca75

File tree

2 files changed

+353
-1
lines changed

2 files changed

+353
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
- name: Run Selenium tests
109109
run: |
110110
cd tests
111-
pytest us1.py us2.py us3.py us4.py us5.py # Added us5.py
111+
pytest us1.py us2.py us3.py us4.py us5.py us6.py
112112
113113
- name: Upload screenshots artifact (on failure)
114114
if: failure()

tests/us6.py

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
\
2+
import unittest
3+
import os
4+
import time
5+
import re
6+
import logging
7+
8+
from selenium import webdriver
9+
from selenium.webdriver.common.by import By
10+
from selenium.webdriver.support.ui import WebDriverWait
11+
from selenium.webdriver.support import expected_conditions as EC
12+
from selenium.common.exceptions import TimeoutException, NoSuchElementException
13+
14+
# Configure logging
15+
logger = logging.getLogger(__name__)
16+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17+
18+
class US06Tests(unittest.TestCase):
19+
BASE_URL = "http://localhost:3000"
20+
GRADE_INPUT_SELECTOR = "input.home__input[placeholder='0.0'][type='number']"
21+
PERCENTAGE_INPUT_SELECTOR = "input.home__input[placeholder='0'][type='number']"
22+
ADD_GRADE_BUTTON_SELECTOR = "button.home__add-button"
23+
GRADES_LIST_ITEM_SELECTOR = "div.home__grades-container > div.home__grade-row"
24+
CALCULATE_BUTTON_SELECTOR = "button.home__calculate-button"
25+
26+
RESULT_PAGE_CONTAINER_SELECTOR = "div.result"
27+
FIRST_TIME_ALERT_BUTTON_SELECTOR = ".alert__button.alert__button--single"
28+
ALERT_OVERLAY_SELECTOR = "div.alert__overlay"
29+
NAV_BACK_BUTTON_XPATH = "//button[contains(@class, 'nav-bar__button') and .//span[contains(@class, 'back-icon')]/svg[contains(@class, 'lucide-chevron-left')]]"
30+
NAV_BACK_BUTTON_SELECTOR = "nav.nav-bar > button.nav-bar__button:first-child"
31+
HOME_CONTAINER_SELECTOR = "div.home__container"
32+
33+
# Selector for US06 - Final Estimated Status
34+
FINAL_STATUS_DISPLAY_SELECTOR = "#final-status-display" # Based on selenium-test-dev.md (id="final-status-display")
35+
36+
# Selectors needed for setting up scenarios (from US05, though not directly tested here)
37+
REQUIRED_GRADE_DISPLAY_SELECTOR = "p.result__card-needed"
38+
SETTINGS_NAV_BUTTON_XPATH = "//button[contains(@class, 'nav-bar__button') and .//span[contains(@class, 'settings-icon')]/svg[contains(@class, 'lucide-settings')]]"
39+
APPROVAL_GRADE_INPUT_SELECTOR = "input.settings__input[type='number']"
40+
41+
def set_driver_fixture(self, driver):
42+
self.driver = driver
43+
self.wait_short = WebDriverWait(self.driver, 5)
44+
self.wait_long = WebDriverWait(self.driver, 15)
45+
if not os.path.exists("screenshots"):
46+
os.makedirs("screenshots")
47+
48+
def setUp(self):
49+
if not hasattr(self, 'driver') or not self.driver:
50+
logger.info("WebDriver not set by fixture, attempting fallback setup for direct unittest execution.")
51+
try:
52+
options = webdriver.ChromeOptions()
53+
# options.add_argument('--headless')
54+
# options.add_argument('--disable-gpu')
55+
self.driver = webdriver.Chrome(options=options)
56+
self.set_driver_fixture(self.driver)
57+
self.is_driver_managed_by_fallback = True
58+
logger.info("Fallback WebDriver initialized for direct unittest execution.")
59+
except Exception as e:
60+
logger.error(f"Failed to initialize fallback WebDriver: {e}")
61+
self.fail(f"Failed to initialize fallback WebDriver: {e}")
62+
else:
63+
logger.info("WebDriver already set, likely by a pytest fixture.")
64+
self.is_driver_managed_by_fallback = False
65+
self._initial_setup()
66+
67+
def tearDown(self):
68+
if hasattr(self, 'is_driver_managed_by_fallback') and self.is_driver_managed_by_fallback:
69+
if self.driver:
70+
self.driver.quit()
71+
logger.info("Fallback WebDriver quit.")
72+
else:
73+
logger.info("Driver teardown managed by pytest fixture (if applicable).")
74+
75+
def _take_screenshot(self, name_suffix):
76+
timestamp = int(time.time())
77+
test_method_name = getattr(self, '_testMethodName', 'unknown_test')
78+
screenshot_name = f"screenshots/{test_method_name}_{name_suffix}_{timestamp}.png"
79+
try:
80+
if hasattr(self, 'driver') and self.driver:
81+
self.driver.save_screenshot(screenshot_name)
82+
logger.info(f"Screenshot saved: {screenshot_name}")
83+
except Exception as e:
84+
logger.error(f"Error saving screenshot {screenshot_name}: {e}")
85+
86+
def _set_approval_grade(self, approval_grade_value):
87+
logger.info(f"Setting approval grade to: {approval_grade_value} using JavaScript injection")
88+
script = f"""
89+
localStorage.setItem('settings', JSON.stringify({{
90+
minAcceptValue: parseFloat({approval_grade_value}),
91+
minValue: 0,
92+
maxValue: 5
93+
}}));
94+
localStorage.setItem("isFirstTime", "false");
95+
"""
96+
self.driver.execute_script(script)
97+
logger.info(f"Settings updated via JavaScript. Approval grade set to: {approval_grade_value}")
98+
self.driver.refresh()
99+
self.wait_long.until(EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR)))
100+
logger.info("Page refreshed after setting approval grade")
101+
102+
def _initial_setup(self):
103+
if not hasattr(self, 'driver') or not self.driver:
104+
logger.error("Driver not initialized in _initial_setup. Aborting setup.")
105+
self.fail("Driver not initialized for test setup.")
106+
return
107+
108+
self.driver.get(self.BASE_URL)
109+
logger.info(f"Navigated to base URL: {self.BASE_URL}")
110+
111+
try:
112+
logger.info(f"Attempting to handle first-time alert with button '{self.FIRST_TIME_ALERT_BUTTON_SELECTOR}'.")
113+
alert_button = WebDriverWait(self.driver, 10).until(
114+
EC.element_to_be_clickable((By.CSS_SELECTOR, self.FIRST_TIME_ALERT_BUTTON_SELECTOR))
115+
)
116+
alert_button.click()
117+
logger.info(f"Clicked first-time alert button: '{self.FIRST_TIME_ALERT_BUTTON_SELECTOR}'.")
118+
119+
WebDriverWait(self.driver, 5).until(
120+
EC.invisibility_of_element_located((By.CSS_SELECTOR, self.ALERT_OVERLAY_SELECTOR))
121+
)
122+
logger.info(f"Alert overlay '{self.ALERT_OVERLAY_SELECTOR}' is no longer visible. App should be on Settings page.")
123+
124+
try:
125+
nav_back_button = WebDriverWait(self.driver, 5).until(
126+
EC.element_to_be_clickable((By.CSS_SELECTOR, self.NAV_BACK_BUTTON_SELECTOR))
127+
)
128+
logger.info("Found back button using CSS selector.")
129+
except TimeoutException:
130+
logger.info("CSS selector failed for back button, trying XPath...")
131+
nav_back_button = WebDriverWait(self.driver, 5).until(
132+
EC.element_to_be_clickable((By.XPATH, self.NAV_BACK_BUTTON_XPATH))
133+
)
134+
logger.info("Found back button using XPath selector.")
135+
136+
nav_back_button.click()
137+
logger.info("Clicked nav back button to return to Home page.")
138+
139+
WebDriverWait(self.driver, 10).until(
140+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
141+
)
142+
logger.info("Successfully navigated back to the Home page after initial alert.")
143+
144+
except TimeoutException:
145+
logger.info("First-time user alert or subsequent navigation elements not found or timed out. Checking if already on Home page.")
146+
try:
147+
self.driver.find_element(By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR)
148+
logger.info("Already on the Home page or initial alert was not present.")
149+
except NoSuchElementException:
150+
logger.warning(f"Home container '{self.HOME_CONTAINER_SELECTOR}' not found. Attempting to re-navigate to BASE_URL.")
151+
self.driver.get(self.BASE_URL)
152+
try:
153+
WebDriverWait(self.driver, 10).until(
154+
EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR))
155+
)
156+
logger.info("Successfully navigated to Home page as a fallback.")
157+
except TimeoutException:
158+
logger.error("Failed to ensure presence on Home page even after fallback. Current URL: %s", self.driver.current_url)
159+
self._take_screenshot("initial_setup_home_fallback_failed")
160+
self.fail("Could not ensure presence on the Home page during initial setup.")
161+
except Exception as e:
162+
logger.error(f"An unexpected error occurred during initial setup: {e}", exc_info=True)
163+
self._take_screenshot("initial_setup_error")
164+
self.fail(f"Unexpected error during initial setup: {e}")
165+
166+
self._set_approval_grade("3.0") # Default approval grade for tests
167+
168+
def _add_grade_and_percentage(self, grade, percentage):
169+
if not hasattr(self, 'driver') or not self.driver:
170+
logger.error("Driver not available in _add_grade_and_percentage.")
171+
self.fail("Driver not available.")
172+
return
173+
174+
on_result_page = False
175+
try:
176+
if self.driver.find_element(By.CSS_SELECTOR, self.RESULT_PAGE_CONTAINER_SELECTOR).is_displayed():
177+
on_result_page = True
178+
except NoSuchElementException:
179+
on_result_page = False
180+
181+
if on_result_page:
182+
logger.info("Currently on result page, navigating back to home to add grades.")
183+
try:
184+
nav_back_button = self.wait_short.until(
185+
EC.element_to_be_clickable((By.CSS_SELECTOR, self.NAV_BACK_BUTTON_SELECTOR))
186+
)
187+
except TimeoutException:
188+
try:
189+
nav_back_button = self.wait_long.until(
190+
EC.element_to_be_clickable((By.XPATH, self.NAV_BACK_BUTTON_XPATH))
191+
)
192+
except TimeoutException:
193+
nav_bar = self.wait_long.until(
194+
EC.presence_of_element_located((By.CSS_SELECTOR, "nav.nav-bar"))
195+
)
196+
nav_buttons = nav_bar.find_elements(By.TAG_NAME, "button")
197+
if nav_buttons:
198+
nav_back_button = nav_buttons[0]
199+
else:
200+
self._take_screenshot("no_buttons_in_nav_bar_add_grade")
201+
self.fail("No buttons found in nav bar when trying to navigate back")
202+
nav_back_button.click()
203+
self.wait_long.until(EC.presence_of_element_located((By.CSS_SELECTOR, self.HOME_CONTAINER_SELECTOR)))
204+
time.sleep(0.5)
205+
206+
grade_rows = self.driver.find_elements(By.CSS_SELECTOR, self.GRADES_LIST_ITEM_SELECTOR)
207+
need_new_row = True
208+
if grade_rows:
209+
last_row = grade_rows[-1]
210+
try:
211+
grade_input = last_row.find_element(By.CSS_SELECTOR, self.GRADE_INPUT_SELECTOR)
212+
percentage_input = last_row.find_element(By.CSS_SELECTOR, self.PERCENTAGE_INPUT_SELECTOR)
213+
if not grade_input.get_attribute("value") and not percentage_input.get_attribute("value"):
214+
need_new_row = False
215+
except NoSuchElementException:
216+
need_new_row = True
217+
218+
if need_new_row or not grade_rows:
219+
add_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.ADD_GRADE_BUTTON_SELECTOR)))
220+
add_button.click()
221+
time.sleep(0.5)
222+
grade_rows = self.driver.find_elements(By.CSS_SELECTOR, self.GRADES_LIST_ITEM_SELECTOR)
223+
if not grade_rows:
224+
self._take_screenshot("failed_to_add_grade_row")
225+
self.fail("Failed to add grade row")
226+
227+
last_row = grade_rows[-1]
228+
try:
229+
grade_input_element = last_row.find_element(By.CSS_SELECTOR, self.GRADE_INPUT_SELECTOR)
230+
percentage_input_element = last_row.find_element(By.CSS_SELECTOR, self.PERCENTAGE_INPUT_SELECTOR)
231+
232+
grade_input_element.clear()
233+
grade_input_element.send_keys(str(grade))
234+
percentage_input_element.clear()
235+
percentage_input_element.send_keys(str(percentage))
236+
237+
add_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.ADD_GRADE_BUTTON_SELECTOR)))
238+
add_button.click()
239+
time.sleep(0.5)
240+
except NoSuchElementException as e:
241+
self._take_screenshot("input_elements_not_found")
242+
self.fail(f"Input elements not found: {e}")
243+
244+
def _click_calculate_and_wait_for_result_page(self):
245+
calculate_button = self.wait_long.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.CALCULATE_BUTTON_SELECTOR)))
246+
calculate_button.click()
247+
self.wait_long.until(EC.presence_of_element_located((By.CSS_SELECTOR, self.RESULT_PAGE_CONTAINER_SELECTOR)))
248+
logger.info("Clicked 'Calcular' and result page container is present.")
249+
250+
def _get_final_status_message(self):
251+
"""Extracts and returns the final estimated status message from the result page."""
252+
raw_text = ""
253+
try:
254+
self.wait_long.until(EC.presence_of_element_located((By.CSS_SELECTOR, self.RESULT_PAGE_CONTAINER_SELECTOR)))
255+
256+
status_element = self.wait_long.until(
257+
EC.visibility_of_element_located((By.CSS_SELECTOR, self.FINAL_STATUS_DISPLAY_SELECTOR))
258+
)
259+
time.sleep(0.2) # Allow text to stabilize
260+
raw_text = status_element.text.strip()
261+
logger.info(f"Raw final status message text: '{raw_text}'")
262+
263+
if not raw_text:
264+
logger.warning("Final status message text is empty.")
265+
self._take_screenshot("empty_final_status_message")
266+
return "Error: Empty Value"
267+
268+
# Expected values: "Aprobado", "En riesgo", "No aprueba"
269+
return raw_text
270+
271+
except TimeoutException:
272+
logger.error(f"Timeout waiting for final status display: {self.FINAL_STATUS_DISPLAY_SELECTOR}")
273+
self._take_screenshot("final_status_timeout")
274+
return "Error: Timeout"
275+
except Exception as e:
276+
logger.error(f"Error getting final status message: {e}", exc_info=True)
277+
self._take_screenshot("get_final_status_error")
278+
return "Error: General"
279+
280+
# --- Test Cases for US06 Task 6.3 ---
281+
282+
def test_us06_verify_final_status_aprobado(self):
283+
# Scenario "Aprobado": Input grades leading to an "already approved" state (Task 5.3).
284+
# Verify the status displays "Aprobado".
285+
# Grades: Grade: "4.0", Percentage: "80%". Approval grade: 3.0
286+
test_name = self._testMethodName
287+
logger.info(f"Running test: {test_name}")
288+
try:
289+
self._add_grade_and_percentage("4.0", "80")
290+
self._click_calculate_and_wait_for_result_page()
291+
292+
final_status = self._get_final_status_message()
293+
self.assertEqual(final_status, "Aprobado", f"Test {test_name}: Expected 'Aprobado', got '{final_status}'")
294+
logger.info(f"Test {test_name} passed. Final status: {final_status}")
295+
296+
except AssertionError as e:
297+
logger.error(f"AssertionError in {test_name}: {e}", exc_info=True)
298+
self._take_screenshot(f"{test_name}_assertion_error")
299+
self.fail(f"AssertionError in {test_name}: {e}")
300+
except Exception as e:
301+
logger.error(f"Exception in {test_name}: {e}", exc_info=True)
302+
self._take_screenshot(f"{test_name}_exception")
303+
self.fail(f"Exception in {test_name}: {e}")
304+
305+
def test_us06_verify_final_status_en_riesgo(self):
306+
# Scenario "En riesgo": Input grades leading to a "required grade" that is achievable (Task 5.1).
307+
# Verify the status displays "En riesgo".
308+
# Grades: Grade: "2.0", Percentage: "50%". Approval grade: 3.0. (Requires 4.0)
309+
test_name = self._testMethodName
310+
logger.info(f"Running test: {test_name}")
311+
try:
312+
self._add_grade_and_percentage("2.0", "50")
313+
self._click_calculate_and_wait_for_result_page()
314+
315+
final_status = self._get_final_status_message()
316+
self.assertEqual(final_status, "En riesgo", f"Test {test_name}: Expected 'En riesgo', got '{final_status}'")
317+
logger.info(f"Test {test_name} passed. Final status: {final_status}")
318+
319+
except AssertionError as e:
320+
logger.error(f"AssertionError in {test_name}: {e}", exc_info=True)
321+
self._take_screenshot(f"{test_name}_assertion_error")
322+
self.fail(f"AssertionError in {test_name}: {e}")
323+
except Exception as e:
324+
logger.error(f"Exception in {test_name}: {e}", exc_info=True)
325+
self._take_screenshot(f"{test_name}_exception")
326+
self.fail(f"Exception in {test_name}: {e}")
327+
328+
def test_us06_verify_final_status_no_aprueba(self):
329+
# Scenario "No aprueba": Input grades leading to an "impossible to approve" state (Task 5.2).
330+
# Verify the status displays "No aprueba".
331+
# Grades: Grade: "1.0", Percentage: "80%". Approval grade: 3.0. (Requires 11.0)
332+
test_name = self._testMethodName
333+
logger.info(f"Running test: {test_name}")
334+
try:
335+
self._add_grade_and_percentage("1.0", "80")
336+
self._click_calculate_and_wait_for_result_page()
337+
338+
final_status = self._get_final_status_message()
339+
self.assertEqual(final_status, "No aprueba", f"Test {test_name}: Expected 'No aprueba', got '{final_status}'")
340+
logger.info(f"Test {test_name} passed. Final status: {final_status}")
341+
342+
except AssertionError as e:
343+
logger.error(f"AssertionError in {test_name}: {e}", exc_info=True)
344+
self._take_screenshot(f"{test_name}_assertion_error")
345+
self.fail(f"AssertionError in {test_name}: {e}")
346+
except Exception as e:
347+
logger.error(f"Exception in {test_name}: {e}", exc_info=True)
348+
self._take_screenshot(f"{test_name}_exception")
349+
self.fail(f"Exception in {test_name}: {e}")
350+
351+
if __name__ == '__main__':
352+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)