diff --git a/.env b/.env deleted file mode 100644 index 04745946..00000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -# DO NOT COMMIT THIS FILE - -BCSS_PASS= diff --git a/pages/base_page.py b/pages/base_page.py index c611cf2b..e9f7f292 100644 --- a/pages/base_page.py +++ b/pages/base_page.py @@ -286,7 +286,7 @@ def handle_dialog(dialog: Dialog, accept: bool = False): expected_text in actual_text ), f"Expected dialog to contain '{expected_text}', but got '{actual_text}'" except AssertionError as e: - self._dialog_assertion_error = e + raise AssertionError(f"Dialog text assertion failed: {e}") from e if accept: try: dialog.accept() diff --git a/pages/organisations/parameters_page.py b/pages/organisations/parameters_page.py new file mode 100644 index 00000000..b60d6347 --- /dev/null +++ b/pages/organisations/parameters_page.py @@ -0,0 +1,310 @@ +from typing import Optional +from playwright.sync_api import Page +from pages.base_page import BasePage +from utils.table_util import TableUtils +import logging +from utils.oracle.oracle_specific_functions.organisation_parameters import ( + get_org_parameter_value, + get_national_parameter_value, +) +from datetime import datetime, timedelta +from utils.calendar_picker import CalendarPicker + +displayrs_str = "#displayRS" + + +class ParametersPage(BasePage): + """Parameters Page locators, and methods for interacting with the page.""" + + def __init__(self, page: Page): + super().__init__(page) + + self.parameters_table = TableUtils(self.page, displayrs_str) + self.parameters_output_table = TableUtils(self.page, displayrs_str) + self.add_new_parameter_value_button = self.page.get_by_role( + "button", name="Add new parameter value" + ) + self.parameter_value_input_field = self.page.locator("#A_C_VALUE") + self.parameter_effective_from_date_input_field = self.page.locator( + "#A_C_START_DATE" + ) + self.parameter_reason_input_field = self.page.locator("#A_C_AUDIT_REASON") + self.save_button = self.page.get_by_role("button", name="Save") + + def get_current_value_of_parameter(self, parameter_id: str) -> str: + """ + Gets the current value of a parameter from the parameters table. + + Args: + parameter_id (str): The ID of the parameter to look for. + Returns: + str: The current value of the parameter. + """ + row_index = self.parameters_table.get_row_index("ID", parameter_id) + if row_index is None: + raise ValueError( + f"Parameter ID {parameter_id} not found in parameters table." + ) + return self.parameters_table.get_cell_value("Current Value", row_index) + + def assert_parameter_value_matches_expected( + self, parameter_id: str, expected_value: str + ) -> None: + """ + Asserts that the current value of a parameter matches the expected value. + Args: + parameter_id (str): The ID of the parameter to look for. + expected_value (str): The expected value of the parameter. + """ + actual_value = self.get_current_value_of_parameter(parameter_id) + assert ( + actual_value == expected_value + ), f"Parameter ID {parameter_id} value mismatch: expected {expected_value}, got {actual_value}" + logging.info( + f"Parameter ID {parameter_id} value matches expected: {expected_value}" + ) + + def get_parameter_lower_value(self, parameter_id: str) -> str: + """ + Gets the lower value of a parameter from the parameters table. + Args: + parameter_id (str): The ID of the parameter to look for. + Returns: + str: The lower value of the parameter. + """ + row_index = self.parameters_table.get_row_index("ID", parameter_id) + return self.parameters_table.get_cell_value("Lower Value", row_index) + + def get_parameter_upper_value(self, parameter_id: str) -> str: + """ + Gets the upper value of a parameter from the parameters table. + Args: + parameter_id (str): The ID of the parameter to look for. + Returns: + str: The upper value of the parameter. + """ + row_index = self.parameters_table.get_row_index("ID", parameter_id) + return self.parameters_table.get_cell_value("Upper Value", row_index) + + def click_parameter_id_link(self, parameter_id: str) -> None: + """Clicks on the parameter ID link in the parameters table. + + Args: + parameter_id (str): The ID of the parameter to click. + """ + self.click(self.page.get_by_role("link", name=parameter_id, exact=True)) + + def click_add_new_parameter_value_button(self) -> None: + """Clicks the 'Add new parameter value' button.""" + self.click(self.add_new_parameter_value_button) + + def select_new_parameter_value(self, new_parameter_value: str) -> None: + """ + Selects a new value from the parameter value input field dropdown. + Args: + new_parameter_value (str): The new parameter value to select. + """ + self.parameter_value_input_field.select_option(label=new_parameter_value) + + def enter_new_parameter_value(self, new_value: str) -> None: + """ + Enters a new value into the parameter value input field. + Args: + new_value (str): The new parameter value to enter. + """ + self.parameter_value_input_field.fill(new_value) + + def enter_new_parameter_effective_from_date(self, new_date: datetime) -> None: + """ + Enters a new effective from date into the parameter effective from date input field. + Args: + new_date (datetime): The new effective from date to enter. + """ + CalendarPicker(self.page).calendar_picker_ddmmyyyy( + new_date, self.parameter_effective_from_date_input_field + ) + + def enter_new_parameter_effective_from_date_str(self, new_date: str) -> None: + """ + Enters a new effective from date (string) into the parameter effective from date input field. + Args: + new_date (str): The new effective from date (string) to enter. + """ + self.parameter_effective_from_date_input_field.fill(new_date) + + def enter_parameter_reason(self, reason: str) -> None: + """ + Enters a reason into the parameter reason input field. + Args: + reason (str): The reason for the parameter change. + """ + self.parameter_reason_input_field.fill(reason) + + def click_save_button(self) -> None: + """Clicks the 'Save' button.""" + self.click(self.save_button) + + def click_save_button_and_accept_dialog(self) -> None: + """Clicks the 'Save' button and accepts the confirmation dialog.""" + self.safe_accept_dialog(self.save_button) + + def complete_parameter_page_form(self, criteria: dict) -> None: + """ + Completes the parameter page form with the provided details. + Args: + criteria (dict): A dictionary containing the details to enter into the form. + """ + for field, value in criteria.items(): + match field: + case "input value": + self.enter_new_parameter_value(value) + case "select value": + self.select_new_parameter_value(value) + case "effective from date": + if isinstance(value, datetime): + self.enter_new_parameter_effective_from_date(value) + elif isinstance(value, str): + self.enter_new_parameter_effective_from_date_str(value) + case "reason for change": + self.enter_parameter_reason(value) + + def get_next_available_date(self) -> datetime: + """ + Gets the next available date for entering a new parameter value. + Returns: + datetime: The next available date for entering a new parameter value. + """ + most_recent_date = ParameterDetails(self.page).get_most_recent_parameter_date() + today = datetime.today() + # If no date found or most recent date is in the past, use today + base_date = ( + today + if not most_recent_date or most_recent_date < today + else most_recent_date + ) + return base_date + timedelta(days=1) + + def select_screening_centre_parameters_organisation(self, org: str) -> None: + """ + Selects the organisation for screening centre parameters. + Args: + org (str): The organisation to select. + """ + self.click(self.page.get_by_role("link", name=org)) + + +class ParameterDetails(BasePage): + + def __init__(self, page: Page): + super().__init__(page) + self.parameters_table = TableUtils(self.page, displayrs_str) + self.warning_messages = self.page.locator("th.warningHeader") + + def get_most_recent_parameter_date(self) -> Optional[datetime]: + """ + Gets the datetime of the most recent parameter value showing in the parameter details table. + Returns: + datetime: The datetime of the most recent parameter value showing in the parameter details table. + """ + num_rows = self.parameters_table.get_row_count() + + most_recent_date = self.parameters_table.get_cell_value( + "Effective From Date", num_rows + ) + if most_recent_date == "": + return None + return datetime.strptime(most_recent_date, "%d/%m/%Y") + + def get_most_recent_value_of_parameter(self, effective_from_date: datetime) -> str: + """ + Gets the most value of a parameter from the parameters table. + Args: + effective_from_date (datetime): The date to look for. + Returns: + str: The most recent value of the parameter. + """ + date_text = datetime.strftime(effective_from_date, "%d/%m/%Y") + + row_index = self.parameters_table.get_row_index( + "Effective From Date", date_text + ) + if row_index is None: + raise ValueError(f"Parameter with date {date_text} not found in table.") + return self.parameters_table.get_cell_value("Value", row_index) + + def search_for_warning(self, message: str) -> bool: + """ + Searches for a warning message in the parameter details page. + Args: + message (str): The warning message to search for. + Returns: + bool: True if the warning message is found, False otherwise. + """ + for warning_index in range(self.warning_messages.count()): + if self.warning_messages.nth(warning_index).inner_text().strip() == message: + return True + return False + + +class Parameter: + """Class representing a parameter with its attributes.""" + + def __init__( + self, + page: Page, + param_id: str, + ): + self.page = page + self.param_id = param_id + self.lower_value = ParametersPage(self.page).get_parameter_lower_value( + self.param_id + ) + self.upper_value = ParametersPage(self.page).get_parameter_upper_value( + self.param_id + ) + self.current_value = ParametersPage(self.page).get_current_value_of_parameter( + self.param_id + ) + self.current_value_db = self.get_current_value_from_db() + self.national_parameter_value = get_national_parameter_value(int(self.param_id)) + + def get_current_value_from_db(self) -> str: + """ + Gets the current value of the parameter from the database, using the most recent effective_from date. + Returns: + str: The current value of the parameter from the database. + """ + now = datetime.now() + # First check for organisation parameters + param_df = get_org_parameter_value(int(self.param_id), "23159") + if not param_df.empty: + # Convert effective_from to datetime + param_df["effective_from"] = param_df["effective_from"].apply( + lambda x: ( + x + if isinstance(x, datetime) + else datetime.strptime(x, "%Y-%m-%dT%H:%M") + ) + ) + # Filter for dates <= now + valid_rows = param_df[param_df["effective_from"] <= now] + if not valid_rows.empty: + # Get the row with the latest effective_from + latest_row = valid_rows.loc[valid_rows["effective_from"].idxmax()] + return str(latest_row["val"]) + # Then check for screening centre parameters + param_df2 = get_org_parameter_value(int(self.param_id), "23162") + if not param_df2.empty: + param_df2["effective_from"] = param_df2["effective_from"].apply( + lambda x: ( + x + if isinstance(x, datetime) + else datetime.strptime(x, "%Y-%m-%dT%H:%M") + ) + ) + valid_rows2 = param_df2[param_df2["effective_from"] <= now] + if not valid_rows2.empty: + latest_row2 = valid_rows2.loc[valid_rows2["effective_from"].idxmax()] + return str(latest_row2["val"]) + # Otherwise use the national default value + return get_national_parameter_value(int(self.param_id)) diff --git a/pytest.ini b/pytest.ini index 5ccc82a8..faf2fb17 100644 --- a/pytest.ini +++ b/pytest.ini @@ -58,3 +58,4 @@ markers = lynch_self_referral_tests: tests that are part of the lynch self referral test suite surveillance_regression_tests: tests that are part of the surveillance regression test suite lynch_regression_tests: tests that are part of the lynch regression test suite + parameter_212: tests the parameter 212 changes diff --git a/tests/regression/organisation/test_parameter_pages.py b/tests/regression/organisation/test_parameter_pages.py new file mode 100644 index 00000000..9c082d51 --- /dev/null +++ b/tests/regression/organisation/test_parameter_pages.py @@ -0,0 +1,447 @@ +import pytest +from playwright.sync_api import Page +from pages.base_page import BasePage +from pages.organisations.organisations_page import ( + OrganisationSwitchPage, + OrganisationsPage, +) +from pages.organisations.parameters_page import ( + ParametersPage, + Parameter, + ParameterDetails, +) +from utils.user_tools import UserTools +from utils.date_time_utils import DateTimeUtils +import logging +from pages.logout.log_out_page import LogoutPage + + +@pytest.mark.vpn_required +@pytest.mark.regression +def test_parameter_pages(page: Page) -> None: + """ + Test various interactions on the Organisation Parameters page, including adding new parameter values + and validating their correctness against database values. + 1. Log in as "Hub Manager" + 2. Navigate to Organisation Parameters page + 3. Validate integer parameter (ID 25) + 4. Validate Yes/No parameter (ID 199) + 5. Validate string parameter (ID 182) + 6. Navigate to Screening Centre Parameters page + 7. Validate time parameter (ID 28) + 8. Log out + """ + + # Given I log in to BCSS "England" as user role "Hub Manager" + UserTools.user_login(page, "Hub Manager at BCS01") + + # When I navigate to the Organisation Parameters + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_organisation_parameters_page() + + # Checking integer fields - Parameter 25 + chosen_parameter = Parameter(page, "25") + ParametersPage(page).assert_parameter_value_matches_expected( + chosen_parameter.param_id, chosen_parameter.current_value_db + ) + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # When I add a new parameter value that is lower than the allowed minimum + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": str(int(chosen_parameter.lower_value) - 1), + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + + # Then I get a confirmation prompt that contains "Value must not be less than Lower Value." + ParametersPage(page).assert_dialog_text( + "Value must not be less than Lower Value.", True + ) + ParametersPage(page).click_save_button() + + # When I add a new parameter value that is higher than the allowed maximum + ParametersPage(page).complete_parameter_page_form( + { + "input value": str(int(chosen_parameter.upper_value) + 1), + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + + # Then I get a confirmation prompt that contains "Value must not exceed Upper Value." + ParametersPage(page).assert_dialog_text("Value must not exceed Upper Value.", True) + ParametersPage(page).click_save_button() + + # When I add a new parameter value for my parameter that is empty + ParametersPage(page).click_back_button() + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": "", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then it will default to the national value + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + chosen_parameter.national_parameter_value + ) == most_recent_value, f"Parameter value does not match national value when empty value entered. Expected {chosen_parameter.national_parameter_value}, got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value matches national value when empty value entered:\n Expected Value: {chosen_parameter.national_parameter_value}\n Actual Value: {most_recent_value}" + ) + + # When I add a new parameter value for my parameter that is valid + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": "15", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then the parameter is saved with the correct value + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + most_recent_value == "15" + ), f"Parameter value does not match the expect value. Expected {chosen_parameter.national_parameter_value}, got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value matches expected value:\n Expected Value: 15\n Actual Value: {most_recent_value}" + ) + + # When I add a new parameter value for my parameter that is of an incorrect type + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": "test incorrect type", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then a warning message appears on the following page + assert ParameterDetails(page).search_for_warning( + "The update has failed, your changes have not been saved" + ) + assert ParameterDetails(page).search_for_warning( + "Parameter value is of the wrong data type" + ) + logging.info( + "[UI ASSERTIONS COMPLETE] Warning messages displayed for incorrect parameter value type" + ) + + # When I go pack to "Organisation Parameters" page + ParametersPage(page).click_back_button() + ParametersPage(page).click_back_button() + + # Checking Yes/No fields - Parameter 199 + chosen_parameter = Parameter(page, "199") + ParametersPage(page).assert_parameter_value_matches_expected( + chosen_parameter.param_id, chosen_parameter.current_value_db + ) + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # When I select "Yes" for my parameter + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "select value": "Yes", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then it will save the value as "Y" + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + most_recent_value == "Y" + ), f"Parameter value not saved as 'Y' when 'Yes' selected. Got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value saved as 'Y' when 'Yes' selected:\n Expected Value: Y\n Actual Value: {most_recent_value}" + ) + + # When I select "No" for my parameter + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "select value": "No", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then it will save the value as "N" + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + most_recent_value == "N" + ), f"Parameter value not saved as 'N' when 'No' selected. Got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value saved as 'N' when 'No' selected:\n Expected Value: N\n Actual Value: {most_recent_value}" + ) + + # When I select "Yes" for my parameter but do not enter a reason + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "select value": "Yes", + "effective from date": next_available_date, + "reason for change": "", + } + ) + + # Then I get a confirmation prompt that contains "The fields 'Parameter Value', 'Effective From Date' and 'Reason For Change or Addition' are mandatory." + ParametersPage(page).assert_dialog_text( + "The fields 'Parameter Value', 'Effective From Date' and 'Reason For Change or Addition' are mandatory.", + True, + ) + ParametersPage(page).click_save_button() + + # When I go back to the "Organisation Parameters" page + ParametersPage(page).click_back_button() + ParametersPage(page).click_back_button() + + # Checking string fields - Parameter 182 + chosen_parameter = Parameter(page, "182") + ParametersPage(page).assert_parameter_value_matches_expected( + chosen_parameter.param_id, chosen_parameter.current_value_db + ) + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # When I add a new parameter value for my parameter that is valid + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": "Valid String Test", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then the parameter is saved with the correct value + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + most_recent_value == "Valid String Test" + ), f"Parameter value does not match the expect value. Expected 'Valid String Test', got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value matches expected value:\n Expected Value: 'Valid String Test'\n Actual Value: {most_recent_value}" + ) + + # When I navigate to the Screening Centre Parameters + ParametersPage(page).click_main_menu_link() + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_screening_centre_parameters_page() + ParametersPage(page).select_screening_centre_parameters_organisation("BCS001") + + # Checking time fields - Parameter 28 + chosen_parameter = Parameter(page, "28") + ParametersPage(page).assert_parameter_value_matches_expected( + chosen_parameter.param_id, chosen_parameter.current_value_db + ) + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # When I add a value for my parameter that is lower than the allowed minimum + next_available_date = ParametersPage(page).get_next_available_date() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": DateTimeUtils.add_time_to_time_string( + chosen_parameter.lower_value, -1 + ), + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then a warning message appears on the following page + assert ParameterDetails(page).search_for_warning( + "The update has failed, your changes have not been saved" + ) + assert ParameterDetails(page).search_for_warning( + "Parameter value is not within the defined range of values" + ) + logging.info( + "[UI ASSERTIONS COMPLETE] Warning messages displayed for out-of-bounds time parameter value" + ) + + # When I add a value for my parameter that is higher than the allowed maximum + ParametersPage(page).click_back_button() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": DateTimeUtils.add_time_to_time_string( + chosen_parameter.upper_value, 1 + ), + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then a warning message appears on the following page + assert ParameterDetails(page).search_for_warning( + "The update has failed, your changes have not been saved" + ) + assert ParameterDetails(page).search_for_warning( + "Parameter value is not within the defined range of values" + ) + logging.info( + "[UI ASSERTIONS COMPLETE] Warning messages displayed for out-of-bounds time parameter value" + ) + + # When I add a valid value for my parameter + ParametersPage(page).click_back_button() + ParametersPage(page).click_add_new_parameter_value_button() + ParametersPage(page).complete_parameter_page_form( + { + "input value": "10:00", + "effective from date": next_available_date, + "reason for change": "Automated Test", + } + ) + ParametersPage(page).click_save_button_and_accept_dialog() + + # Then the parameter is saved with the correct value + most_recent_value = ParameterDetails(page).get_most_recent_value_of_parameter( + next_available_date + ) + assert ( + most_recent_value == "10:00" + ), f"Parameter value does not match the expect value. Expected '10:00', got {most_recent_value}" + logging.info( + f"[UI ASSERTIONS COMPLETE] Parameter value matches expected value:\n Expected Value: '10:00'\n Actual Value: {most_recent_value}" + ) + + # Finally, I log out + LogoutPage(page).log_out() + + +@pytest.mark.parameter_212 +def test_parameter_212(page: Page) -> None: + """ + Test viewing and adding new values for Parameter ID "212" across different user roles. + 1. Log in as "Screening Centre Manager" and verify inability to edit new parameter value. + 2. Switch to "Hub Manager" and verify inability to edit new parameter value. + 3. Switch to "BCSS Support - SC" and verify ability to edit new parameter value. + 4. Switch to "BCSS Support - HUB" and verify ability to edit new parameter value. + 5. Log out. + """ + # Given I log in to BCSS "England" as user role "Screening Centre Manager" + UserTools.user_login(page, "Screening Centre Manager at BCS001") + + # When I navigate to the Organisation Parameters page + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_organisation_parameters_page() + + # Then I can see that Parameter ID "212" has the correct value from the database + chosen_parameter = Parameter(page, "212") + ParametersPage(page).assert_parameter_value_matches_expected( + chosen_parameter.param_id, chosen_parameter.current_value_db + ) + + # I am able to view Parameter ID "212" as a Screening Centre Manager + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # I "cannot" add a new parameter value + assert ParametersPage( + page + ).add_new_parameter_value_button.is_hidden(), "Add New Parameter Value button is visible, but should be hidden for Screening Centre Manager" + logging.info( + "[UI ASSERTIONS COMPLETE] 'Add New Parameter Value' button is hidden for Screening Centre Manager" + ) + + # When I switch users to BCSS "England" as user role "Hub Manager" + LogoutPage(page).log_out(close_page=False) + BasePage(page).go_to_log_in_page() + UserTools.user_login(page, "Hub Manager at BCS01") + + # Then I go to the Screening Centre Parameters page + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_screening_centre_parameters_page() + ParametersPage(page).select_screening_centre_parameters_organisation("BCS001") + + # I am able to view Parameter ID "212" as a hub manager + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # I "cannot" add a new parameter value + assert ParametersPage( + page + ).add_new_parameter_value_button.is_hidden(), "Add New Parameter Value button is visible, but should be hidden for Hub Manager" + logging.info( + "[UI ASSERTIONS COMPLETE] 'Add New Parameter Value' button is hidden for Hub Manager" + ) + + # When I switch users to BCSS "England" as user role "BCSS Support - SC" + LogoutPage(page).log_out(close_page=False) + BasePage(page).go_to_log_in_page() + UserTools.user_login(page, "BCSS Support - SC at BCS001") + + # Then I go to the Organisation Parameters page + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_organisation_parameters_page() + + # I am able to view Parameter ID "212" as a support user + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # I "can" add a new parameter value + assert ParametersPage( + page + ).add_new_parameter_value_button.is_visible(), "Add New Parameter Value button is hidden, but should be visible for BCSS Support - SC" + logging.info( + "[UI ASSERTIONS COMPLETE] 'Add New Parameter Value' button is visible for BCSS Support - SC" + ) + + # When I switch users to BCSS "England" as user role "BCSS Support - HUB" + LogoutPage(page).log_out(close_page=False) + BasePage(page).go_to_log_in_page() + UserTools.user_login(page, "BCSS Support - HUB") + OrganisationSwitchPage(page).select_organisation_by_id("BCS01") + OrganisationSwitchPage(page).click_continue() + + # Then i go to organisation parameters page + BasePage(page).go_to_organisations_page() + OrganisationsPage(page).go_to_screening_centre_parameters_page() + ParametersPage(page).select_screening_centre_parameters_organisation("BCS001") + + # I am able to view Parameter ID "212" as a support user + ParametersPage(page).click_parameter_id_link(chosen_parameter.param_id) + + # I "can" add a new parameter value + assert ParametersPage( + page + ).add_new_parameter_value_button.is_visible(), "Add New Parameter Value button is hidden, but should be visible for BCSS Support - HUB" + logging.info( + "[UI ASSERTIONS COMPLETE] 'Add New Parameter Value' button is visible for BCSS Support - HUB" + ) + + # Finally, I log out + LogoutPage(page).log_out() diff --git a/users.json b/users.json index 6c1ba5af..540a5f15 100644 --- a/users.json +++ b/users.json @@ -154,6 +154,14 @@ "Wolverhampton SC" ] }, + "BCSS Support - HUB": { + "username": "BCSS306", + "role_id": 202245, + "org_code": "BCS01", + "roles": [ + "Midlands and North West Bowel Cancer Screening Programme Hub, BCSS Hub Support" + ] + }, "BCSS Bureau Staff at X26": { "username": "BCSS33", "role_id": 5042, diff --git a/utils/date_time_utils.py b/utils/date_time_utils.py index 3937f335..614402e9 100644 --- a/utils/date_time_utils.py +++ b/utils/date_time_utils.py @@ -229,3 +229,40 @@ def calculate_birth_date_for_age(age: int) -> date: except ValueError: # Handles February 29 for non-leap years return today.replace(month=2, day=28, year=today.year - age) + + @staticmethod + def add_time_to_time_string(time_str: str, add_minutes: int) -> str: + """ + Adds minutes to a "hh:mm" formatted time string and returns the new time string. + Args: + time_str (str): Time in "hh:mm" format. + add_minutes (int): Minutes to add. + Returns: + str: New time in "hh:mm" format. + """ + + def to_minutes(time_str: str) -> int: + """ + Converts "hh:mm" formatted string to total minutes. + Args: + time_str (str): Time in "hh:mm" format. + Returns: + int: Total minutes. + """ + h, m = map(int, time_str.split(":")) + return h * 60 + m + + def to_hh_mm(minutes: int) -> str: + """ + Converts total minutes to "hh:mm" formatted string. + Args: + minutes (int): Total minutes. + Returns: + str: Time in "hh:mm" format. + """ + h = minutes // 60 + m = minutes % 60 + return f"{h:02d}:{m:02d}" + + total_minutes = to_minutes(time_str) + add_minutes + return to_hh_mm(total_minutes) diff --git a/utils/oracle/oracle.py b/utils/oracle/oracle.py index 35ae26ee..96d3589a 100644 --- a/utils/oracle/oracle.py +++ b/utils/oracle/oracle.py @@ -73,12 +73,12 @@ def exec_bcss_timed_events( for subject_id in subject_ids: try: - logging.info( + logging.debug( f"[ORACLE] Attempting to execute stored procedure: 'bcss_timed_events', [{subject_id}, 'Y']" ) cursor = conn.cursor() cursor.callproc("bcss_timed_events", [subject_id, "Y"]) - logging.info("Stored procedure execution successful!") + logging.debug("Stored procedure execution successful!") except Exception as spExecutionError: logging.error( f"[ORACLE] Failed to execute stored procedure with execution error: {spExecutionError}" @@ -103,7 +103,7 @@ def get_subject_id_from_nhs_number(self, nhs_number: str) -> str: subject_id (str): The subject id for the provided nhs number """ conn = self.connect_to_db() - logging.info( + logging.debug( f"[ORACLE] Attempting to get subject_id from nhs number: {nhs_number}" ) cursor = conn.cursor() @@ -112,7 +112,7 @@ def get_subject_id_from_nhs_number(self, nhs_number: str) -> str: ) result = cursor.fetchall() subject_id = result[0][0] - logging.info(f"Able to extract subject ID: {subject_id}") + logging.debug(f"Able to extract subject ID: {subject_id}") return subject_id def populate_ui_approved_users_table( @@ -181,11 +181,11 @@ def execute_query(self, query: str, parameters: dict | None = None) -> pd.DataFr try: if parameters: params_str = pprint.pformat(parameters, indent=2) - logging.info( + logging.debug( f"[ORACLE] Executing query: {query} with parameters:\n{params_str}" ) else: - logging.info(f"[ORACLE] Executing query: {query}") + logging.debug(f"[ORACLE] Executing query: {query}") df = ( pd.read_sql(query, engine) if parameters == None @@ -221,13 +221,13 @@ def execute_stored_procedure( if conn is None: conn = self.connect_to_db() try: - logging.info(f"[ORACLE] Executing stored procedure: {procedure}") + logging.debug(f"[ORACLE] Executing stored procedure: {procedure}") cursor = conn.cursor() params = self._prepare_params(cursor, in_params, out_params) cursor.callproc(procedure, params) results = self._collect_outputs(params, out_params, in_params) conn.commit() - logging.info("[ORACLE] Stored procedure execution successful") + logging.debug("[ORACLE] Stored procedure execution successful") return results except Exception as executionError: raise RuntimeError( @@ -306,7 +306,7 @@ def update_or_insert_data_to_table( conn = self.connect_to_db() try: logging.debug("Attempting to insert/update table") - logging.info( + logging.debug( f"[ORACLE] Executing query: {statement} with parameters:\n{pprint.pformat(params, indent=2)}" ) cursor = conn.cursor() diff --git a/utils/oracle/oracle_specific_functions/organisation_parameters.py b/utils/oracle/oracle_specific_functions/organisation_parameters.py index 5ec79dc9..be4e8abc 100644 --- a/utils/oracle/oracle_specific_functions/organisation_parameters.py +++ b/utils/oracle/oracle_specific_functions/organisation_parameters.py @@ -97,3 +97,20 @@ def check_parameter(param_id: int, org_id: str, expected_param_value: str) -> bo logging.warning(f"Parameter {param_id} is not set correctly, updating parameter.") return False + + +def get_national_parameter_value(param_id: int) -> str: + """ + Retrieves the default value of a national parameter from the database. + Args: + param_id (int): The ID of the national parameter to retrieve. + """ + query = """SELECT p.default_value + FROM parameters p + WHERE p.param_id = :param_id""" + params = {"param_id": param_id} + df = OracleDB().execute_query(query, params) + if not df.empty: + return df.iloc[0]["default_value"] + else: + raise ValueError(f"Parameter ID {param_id} not found in parameters table.") diff --git a/utils/table_util.py b/utils/table_util.py index 0f25a36e..7cd8ee71 100644 --- a/utils/table_util.py +++ b/utils/table_util.py @@ -52,7 +52,7 @@ def get_column_index(self, column_name: str) -> int: # Extract first-row headers (general headers) header_texts = headers.evaluate_all("ths => ths.map(th => th.innerText.trim())") - logging.info(f"First Row Headers Found: {header_texts}") + logging.debug(f"First Row Headers Found: {header_texts}") # Attempt to extract a second row of headers (commonly used for filters or alternate titles) second_row_headers = self.table.locator( @@ -65,7 +65,7 @@ def get_column_index(self, column_name: str) -> int: for h in second_row_headers ): header_texts = second_row_headers - logging.info(f"Second Row Headers Found: {second_row_headers}") + logging.debug(f"Second Row Headers Found: {second_row_headers}") for index, header in enumerate(header_texts): if column_name.strip().lower() == header.strip().lower(): @@ -395,3 +395,29 @@ def verify_value_for_label(self, label_text: str, expected_text: str) -> None: logging.info( f"Verified: label '{label_text}' has expected text '{expected_text}'" ) + + def get_row_index(self, column_name: str, row_value: str) -> int: + """ + Returns the 1-based index of the first row where the specified column contains the given value. + Args: + column_name (str): The name of the column to search in. + row_value (str): The value to match in the specified column. + Returns: + int: The 1-based index of the matching row, or -1 if not found. + """ + column_index = self.get_column_index(column_name) + if column_index == -1: + raise ValueError(f"Column '{column_name}' not found in table") + + row_count = self.get_row_count() + for row_index in range(row_count): + cell_locator = self.page.locator( + f"{self.table_id} > tbody tr:nth-child({row_index+1}) td:nth-child({column_index})" + ) + if cell_locator.count() > 0: + cell_text = cell_locator.first.inner_text().strip() + if cell_text == row_value: + return row_index + 1 # 1-based index + raise ValueError( + f"No row found with the value '{row_value}' in the column '{column_name}'." + )