diff --git a/selenium/bundle_test.py b/selenium/bundle_test.py new file mode 100644 index 00000000..17f2f8ad --- /dev/null +++ b/selenium/bundle_test.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import unittest + +from selenium import webdriver +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +from helper.Authentication import AuthenticationHelper +from helper.Bundle import BundleHelper, BundleSelectors +from helper.Clinic import ClinicHelper +from helper.Condition import ConditionHelper +from helper.Language import LanguageHelper, LanguageSelectors +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper, QuestionType +from helper.Questionnaire import QuestionnaireHelper +from helper.Score import ScoreHelper +from helper.SeleniumUtils import SeleniumUtils, ErrorSelectors +from helper.Survey import SurveyHelper, SurveyAssertHelper +from utils.imiseleniumtest import IMISeleniumBaseTest, IMISeleniumChromeTest + + +class URLPaths: + ADMIN_INDEX = "/admin/index" + ADMIN_INDEX_DE = "/admin/index?lang=de_DE" + MOBILE_USER_LOGIN = "/mobile/user/login" + MOBILE_USER_LOGIN_DE = "/mobile/user/login?lang=de_DE" + MOBILE_SURVEY_INDEX = "/mobile/survey/index" + LOGIN_BAD_CREDENTIALS = "/mobile/user/login?message=BadCredentialsException" + LOGIN_DISABLED_EXCEPTION = "/mobile/user/login?message=DisabledException" + PASSWORD_FORGOT = "/mobile/user/password" + + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + + +class CustomTest(IMISeleniumChromeTest, unittest.TestCase): + seleniumMode: IMISeleniumBaseTest.SeleniumMode = IMISeleniumBaseTest.SeleniumMode.LOCAL + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_url = "localhost:8080" + cls.https_base_url = "http://localhost:8080" + + def setUp(self): + chrome_options = Options() + # chrome_options.add_argument("--headless=new") + chrome_options.add_argument("start-maximized") + driver = webdriver.Chrome(options=chrome_options, service=Service(ChromeDriverManager().install())) + # Initialize the WebDriver + self.driver = webdriver.Chrome(options=chrome_options) + + # Initialize Navigation and Utils + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + # Initialize other helpers + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, navigation_helper=self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, navigation_helper=self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, navigation_helper=self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, navigation_helper=self.navigation_helper) + + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + self.language_helper = LanguageHelper(self.driver, self.navigation_helper) + + def test_bundle_list(self): + bundle={} + created_questionnaire={} + clinic={} + + # Arrange + if not self.secret.get('admin-username') or not self.secret.get('admin-username'): + self.skipTest("User AD credentials missing. Test skipped.") + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + except Exception as e: + self.fail(f"Failed to navigate to 'Bundles' page: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.TABLE_BUNDLE, "Bundle table not found") + + try: + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle['id']=self.bundle_helper.save_bundle(bundle_name=bundle['name']) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.CELL_FLAGICON, "Flag icon not found") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"] = self.clinic_helper.create_clinic(bundles=[bundle], + configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}]) + clinic['id']=self.clinic_helper.save_clinic(clinic_name=clinic['name']) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + # Assert - Find clinics assigned to the bundle + try: + self.navigation_helper.navigate_to_manage_bundles() + self.utils.search_item(bundle["name"], "bundle") + bundle_row = self.bundle_helper.get_first_bundle_row() + bundle_row.find_element(By.CSS_SELECTOR, "td:nth-child(3)") + clinic_link = bundle_row.find_element(By.CSS_SELECTOR, "ul > li > a") + self.assertEqual(clinic_link.text, clinic["name"], f"Clinic name '{clinic["name"]}' not found in bundle row.") + + except Exception as e: + self.fail(f"Failed to find clinic assigned to bundle: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.INPUT_BUNDLE_SEARCH, "Bundle table search box not found") + + self.utils.check_visibility_of_element(BundleSelectors.PAGINATION_BUNDLE, "Bundle table pagination not found") + + self.utils.check_visibility_of_element(BundleSelectors.BUTTON_ADD_BUNDLE, "Bundle add button not found") + + try: + pass + finally: + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + def test_bundle_fill(self): + created_questionnaire = {} + # Arrange + if not self.secret.get('admin-username') or not self.secret.get('admin-username'): + self.skipTest("User AD credentials missing. Test skipped.") + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + except Exception as e: + self.fail(f"Failed to navigate to 'Bundles' page: {e}") + + self.utils.click_element(BundleSelectors.BUTTON_ADD_BUNDLE) + self.language_helper.open_language_dropdown() + self.utils.check_visibility_of_element(LanguageSelectors.LANGUAGE_DROPDOWN, "Failed to open language dropdown") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_NAME, "Failed to locate bundle input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_EDITABLE_DESCRIPTION, "Failed to locate bundle description input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_WELCOME_TEXT, "Failed to locate welcome input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_END_TEXT, "Failed to locate end input") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_PUBLISH, "Failed to locate publish checkbox") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_NAME_PROGRESS, "Failed to locate name progress checkbox") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_PROGRESS_WHOLE_PACKAGE, "Failed to locate progress whole package checkbox") + self.utils.check_visibility_of_element(BundleSelectors.TABLE_AVAILABLE_QUESTIONNAIRES, "Available questionnaires table not found") + self.utils.check_visibility_of_element(BundleSelectors.TABLE_ASSIGNED_QUESTIONNAIRES, "Assigned questionnaires table not found") + + #Assert - Test for assigning questionnaire to bundle + try: + self.bundle_helper.assign_multiple_questionnaires_to_bundle([created_questionnaire]) + except Exception as e: + self.fail(f"Failed to assign questionnaire to bundle: {e}") + + #Assert - Test for removing questionnaire to bundle + try: + self.bundle_helper.remove_multiple_questionnaires_from_bundle([created_questionnaire]) + except Exception as e: + self.fail(f"Failed to assign questionnaire to bundle: {e}") + + #Assert - Check form validation + try: + self.utils.click_element(BundleSelectors.BUTTON_SAVE) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + self.assertEqual(len(validation_errors), 2, "Expected 2 validation errors, but found {len(validation_errors)}") + except Exception as e: + self.fail(f"Failed to save bundle: {e}") + + + #Finally + finally: + if(created_questionnaire): + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + def tearDown(self): + if self.driver: + self.driver.quit() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/selenium/clinic_test.py b/selenium/clinic_test.py new file mode 100644 index 00000000..5011a06b --- /dev/null +++ b/selenium/clinic_test.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import unittest + +from selenium import webdriver +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +from helper.Authentication import AuthenticationHelper +from helper.Bundle import BundleHelper +from helper.Clinic import ClinicHelper, ClinicSelectors +from helper.Condition import ConditionHelper +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper, QuestionType +from helper.Questionnaire import QuestionnaireHelper +from helper.Score import ScoreHelper +from helper.SeleniumUtils import SeleniumUtils, ErrorSelectors +from helper.Survey import SurveyHelper, SurveyAssertHelper +from utils.imiseleniumtest import IMISeleniumBaseTest, IMISeleniumChromeTest + + +class URLPaths: + ADMIN_INDEX = "/admin/index" + ADMIN_INDEX_DE = "/admin/index?lang=de_DE" + MOBILE_USER_LOGIN = "/mobile/user/login" + MOBILE_USER_LOGIN_DE = "/mobile/user/login?lang=de_DE" + MOBILE_SURVEY_INDEX = "/mobile/survey/index" + LOGIN_BAD_CREDENTIALS = "/mobile/user/login?message=BadCredentialsException" + LOGIN_DISABLED_EXCEPTION = "/mobile/user/login?message=DisabledException" + PASSWORD_FORGOT = "/mobile/user/password" + + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + + +class CustomTest(IMISeleniumChromeTest, unittest.TestCase): + seleniumMode: IMISeleniumBaseTest.SeleniumMode = IMISeleniumBaseTest.SeleniumMode.LOCAL + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_url = "localhost:8080" + cls.https_base_url = "http://localhost:8080" + + def setUp(self): + chrome_options = Options() + # chrome_options.add_argument("--headless=new") + chrome_options.add_argument("start-maximized") + driver = webdriver.Chrome(options=chrome_options, service=Service(ChromeDriverManager().install())) + # Initialize the WebDriver + self.driver = webdriver.Chrome(options=chrome_options) + + # Initialize Navigation and Utils + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + # Initialize other helpers + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, navigation_helper=self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, navigation_helper=self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, navigation_helper=self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, navigation_helper=self.navigation_helper) + + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + + def test_clinic_list(self): + clinic={} + + # Arrange + if not self.secret.get('admin-username') or not self.secret.get('admin-username'): + self.skipTest("User AD credentials missing. Test skipped.") + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + self.navigation_helper.navigate_to_manage_clinics() + except Exception as e: + self.fail(f"Failed to navigate to 'Clinic' page: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"] = self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}]) + clinic['id']=self.clinic_helper.save_clinic(clinic_name=clinic['name']) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_CLINIC, "Clinic table not found") + self.utils.check_visibility_of_element(ClinicSelectors.PAGINATION_CLINIC_TABLE, "Clinic table pagination not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_SEARCH, "Clinic table search not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ACTION_BUTTONS, "Clinic table action buttons not found") + self.utils.check_visibility_of_element(ClinicSelectors.BUTTON_ADD_CLINIC, "Add new clinic button not found") + + try: + pass + finally: + self.utils.search_and_delete_item(clinic["name"],clinic["id"],"clinic") + self.authentication_helper.logout() + + def test_clinic_fill(self): + clinic={} + created_questionnaire={} + bundle={} + + # Arrange + if not self.secret.get('admin-username') or not self.secret.get('admin-username'): + self.skipTest("User AD credentials missing. Test skipped.") + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + #Arrange + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + self.navigation_helper.navigate_to_manage_bundles() + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle['id']=self.bundle_helper.save_bundle(bundle_name=bundle['name']) + except Exception as e: + self.fail(f"Failed to setup questionnaire and bundle: {e}") + + self.navigation_helper.navigate_to_manage_clinics() + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(ClinicSelectors.BUTTON_ADD_CLINIC)) + self.utils.click_element(ClinicSelectors.BUTTON_ADD_CLINIC) + + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_CLINIC_NAME, "Clinic name input not found") + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_EDITABLE_DESCRIPTION, "Clinic description input not found") + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_CLINIC_EMAIL, "Clinic email input not found") + + #Assert - Check if the clinic configuration is displayed + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ClinicSelectors.DIV_CLINIC_CONFIGURATION) + ) + clinic_configuration = self.driver.find_element(*ClinicSelectors.DIV_CLINIC_CONFIGURATION) + clinic_configuration_list = clinic_configuration.find_elements(*ClinicSelectors.CLINIC_CONFIGURATION_LIST) + self.assertGreaterEqual(len(clinic_configuration_list), 1, "Clinic configuration list should have at least one item") + except: + self.fail( + f"Clinic configuration not found") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_AVAIALBLE_BUNDLES, "Available bundles table not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ASSIGNED_BUNDLES, "Assigned bundles table not found") + + + #Assert - Check if the bundles can be added to the clinic + try: + self.clinic_helper.assign_multiple_bundes_to_clinic([{'id': bundle["id"], 'name': bundle["name"]}]) + except Exception as e: + self.fail(f"Failed to assign bundle to clinic: {e}") + + #Assert - Check if the bundles can be removed from the clinic + try: + self.clinic_helper.remove_multiple_bundes_from_clinic([{'id': bundle["id"], 'name': bundle["name"]}]) + except Exception as e: + self.fail(f"Failed to remove bundle from clinic: {e}") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_AVAIALBLE_USERS, "Available users table not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ASSIGNED_USERS, "Assigned users table not found") + + #Assert - Check if the users can be added to the clinic + try: + self.clinic_helper.assign_multiple_users_to_clinic([self.secret.get('admin-username')]) + except Exception as e: + self.fail(f"Failed to assign users to clinic: {e}") + + #Assert - Check if the users can be removed from the clinic + try: + self.clinic_helper.remove_multiple_users_from_clinic([self.secret.get('admin-username')]) + except Exception as e: + self.fail(f"Failed to remove users from clinic: {e}") + + #Assert - Check form validation + try: + self.utils.click_element(ClinicSelectors.BUTTON_SAVE) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + configuration_errors = self.driver.find_elements(*ErrorSelectors.CONFIGURATION_ERROR_SELECTOR) + self.assertEqual(len(validation_errors), 2, "Expected 2 validation errors, but found {len(validation_errors)}") + self.assertEqual(len(configuration_errors), 1, "Expected 1 configuration errors, but found {len(configuration_errors)}") + except Exception as e: + self.fail(f"Failed to save bundle: {e}") + + #Assert - Check if the clinic can be created + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic['name']=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[{'id': bundle["id"], 'name': bundle["name"]}], users=[self.secret.get('admin-username')]) + clinic['id'] = self.clinic_helper.save_clinic(clinic["name"]) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ClinicSelectors.TABLE_CLINIC) + ) + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + finally: + if clinic["id"]: + self.utils.search_and_delete_item(clinic['name'],clinic["id"],"clinic") + if bundle["id"]: + self.utils.search_and_delete_item(bundle["name"],bundle["id"],"bundle") + if created_questionnaire: + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + self.authentication_helper.logout() + + def tearDown(self): + if self.driver: + self.driver.quit() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/selenium/configuration_test.py b/selenium/configuration_test.py new file mode 100644 index 00000000..13a7a0c2 --- /dev/null +++ b/selenium/configuration_test.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import time +import unittest + +from helper.User import UserSelector +from selenium import webdriver +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +from helper.Authentication import AuthenticationHelper +from helper.Bundle import BundleHelper +from helper.Clinic import ClinicHelper +from helper.Condition import ConditionHelper +from helper.Language import LanguageHelper +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper +from helper.Questionnaire import QuestionnaireHelper +from helper.Score import ScoreHelper +from helper.SeleniumUtils import SeleniumUtils, ErrorSelectors +from helper.Survey import SurveyHelper, SurveyAssertHelper +from helper.Configuration import ConfigurationSelectors, ConfigurationHelper +from utils.imiseleniumtest import IMISeleniumBaseTest, IMISeleniumChromeTest + + + +class URLPaths: + ADMIN_INDEX = "/admin/index" + ADMIN_INDEX_DE = "/admin/index?lang=de_DE" + MOBILE_USER_LOGIN = "/mobile/user/login" + MOBILE_USER_LOGIN_DE = "/mobile/user/login?lang=de_DE" + MOBILE_SURVEY_INDEX = "/mobile/survey/index" + LOGIN_BAD_CREDENTIALS = "/mobile/user/login?message=BadCredentialsException" + LOGIN_DISABLED_EXCEPTION = "/mobile/user/login?message=DisabledException" + PASSWORD_FORGOT = "/mobile/user/password" + + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + + +class CustomTest(IMISeleniumChromeTest, unittest.TestCase): + seleniumMode: IMISeleniumBaseTest.SeleniumMode = IMISeleniumBaseTest.SeleniumMode.LOCAL + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_url = "localhost:8080" + cls.https_base_url = "http://localhost:8080" + + def setUp(self): + chrome_options = Options() + # chrome_options.add_argument("--headless=new") + chrome_options.add_argument("start-maximized") + driver = webdriver.Chrome(options=chrome_options, service=Service(ChromeDriverManager().install())) + # Initialize the WebDriver + self.driver = webdriver.Chrome(options=chrome_options) + + # Initialize Navigation and Utils + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + # Initialize other helpers + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, navigation_helper=self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, navigation_helper=self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, navigation_helper=self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, navigation_helper=self.navigation_helper) + + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + self.language_helper = LanguageHelper(self.driver, self.navigation_helper) + self.configuration_helper = ConfigurationHelper(self.driver, self.navigation_helper) + + def test_configuration_edit(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_configuration() + + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_LANGUAGE, "Select Language not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_CASE_NUMBER_TYPE, "Case Number Type input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_STORAGE_PATH_FOR_UPLOADS, "Storage Path for Uploads input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_BASE_URL, "Base URL input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_PATH_UPLOAD_IMAGES, self.configuration_helper.DEFAUL_EXPORT_IMAGE_UPLOAD_PATH) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PATH_MAIN_DIRECTORY, "Path for Main Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_AD_AUTH) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_AD, "URL AD input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_DOMAIN_AD, "Domain AD input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_DEFAULT_LANGUAGE_AD, "Default Language AD select not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_NUMBER_AD, "Phone Number AD input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_PATIENT_LOOKUP) + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_PATIENT_LOOKUP_IMPLEMENTATION, "Patient Lookup Implementation select not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HOST_PATIENT_LOOKUP, "Host Patient Lookup input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_PORT_PATIENT_LOOKUP, self.configuration_helper.DEFAULT_PORT_HL7_PATIENT_LOOKUP) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PORT_PATIENT_LOOKUP, "Port Patient Lookup input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PSEUDONYMIZATION_URL, "Pseudonymization URL input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PSEUDONYMIZATION_API_KEY, "Pseudonymization API Key input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_OID, "OID input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_ODM_TO_PDF, "URL ODM to PDF input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SYSTEM_URI_FOR_FHIR, "System URI for FHIR input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_EXPORT_PATH_ORBIS, self.configuration_helper.DEFAULT_EXPORT_PATH) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_HL7_DIRECTORY, "Export Path HL7 Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_VIA_SERVER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HL7_HOST, "HL7 Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HL7_PORT, "HL7 Port input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SENDING_FACILITY, "Sending Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_RECEIVING_APPLICATION, "Receiving Application input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_RECEIVING_FACILITY, "Receiving Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_FILE_ORDER_NUMBER, "File Order Number input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENCRYPT_MESSAGE_TLS) + self.utils.check_visibility_of_element(ConfigurationSelectors.FILE_PATH_CERTIFICATE, "File Path Certificate input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_USE_CERTIFICATE) + self.utils.check_visibility_of_element(ConfigurationSelectors.FILE_PKCS_ARCHIVE, "File PKCS Archive input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PASSWORD_PKCS_ARCHIVE, "Password PKCS Archive input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_ODM_DIRECTORY, "Export Path ODM Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_REST) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_REST_INTERFACE, "URL REST Interface input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_HL7) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_HL7_HOST, "ODM HL7 Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_HL7_PORT, "ODM HL7 Port input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_SENDING_FACILITY, "ODM Sending Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_RECEIVING_APPLICATION, "ODM Receiving Application input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_RECEIVING_FACILITY, "ODM Receiving Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_FILE_ORDER_NUMBER, "ODM File Order Number input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_FHIR_DIRECTORY, "Export Path FHIR Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_COMMUNICATION_SERVER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_FHIR_HOST, "FHIR Host input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_REDCAP, "Export Path REDCap input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_VIA_REST) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_REDCAP, "URL REDCap input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TOKEN_REDCAP, "Token REDCap input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER, "Time Delete Incomplete Encounter input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE, "Time Delete Incomplete Encounter and Not Sent Questionnaire input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_EMAIL_COMPLETED_ENCOUNTER, "Time Delete Email Completed Encounter input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTERS, "Time Delete Incomplete Scheduled Encounters input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE, "Time Delete Incomplete Scheduled Encounter and Not Sent Questionnaire input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_UTILIZE_MAILER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_HOST, "Mail Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_PORT, "Mail Port input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENABLE_TLS) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_SMTP_AUTHENTICATION) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_USERNAME_MAILER, "Username Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PASSWORD_MAILER, "Password Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SENDER_MAILER, "Sender Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EMAIL_ADDRESS_MAILER, "Email Address Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_MAILER, "Phone Mailer input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_MAIL_SUPPORT, self.configuration_helper.DEFAULT_MAIL_SUPPORT) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_SUPPORT, "Mail Support input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_SUPPORT, "Phone Support input not found") + + self.configuration_helper.save_configuration() + + # Count all divs with class config_error + error_divs = self.driver.find_elements(By.CLASS_NAME, "config_error") + error_count = len(error_divs) + + # Expect 7 and throw error if less + expected_error_count = 7 + if error_count < expected_error_count: + raise AssertionError(f"Expected at least {expected_error_count} divs with class 'config_error', but found {error_count}") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_AD_AUTH, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_PATIENT_LOOKUP, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_VIA_SERVER, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENCRYPT_MESSAGE_TLS, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_USE_CERTIFICATE, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_REST, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_HL7, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_COMMUNICATION_SERVER, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_VIA_REST, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_SMTP_AUTHENTICATION, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENABLE_TLS, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_UTILIZE_MAILER, False) + + self.configuration_helper.add_additional_logo() + + self.configuration_helper.save_configuration() + self.utils.scroll_to_bottom() + self.utils.check_visibility_of_element(ConfigurationSelectors.IMAGE_ADDITIONAL_LOGO, "Additional Logo not found") + + def tearDown(self): + if self.driver: + self.driver.quit() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/selenium/docker-compose.yml b/selenium/docker-compose.yml index 304fa3e9..435ba4fe 100644 --- a/selenium/docker-compose.yml +++ b/selenium/docker-compose.yml @@ -34,6 +34,13 @@ services: - "8080:8080" networks: - selenoid-network + volumes: + - /var/lib/mopatImages + - /var/lib/mopatExport + - /var/lib/mopatExport/ODM + - /var/lib/mopatExport/FHIR + - /var/lib/mopatExport/REDCap + environment: - MYSQL_USER=mopat - MYSQL_PASSWORD=mopat diff --git a/selenium/encounter_test.py b/selenium/encounter_test.py new file mode 100644 index 00000000..d55654cd --- /dev/null +++ b/selenium/encounter_test.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import datetime +import unittest + +from helper.Encounter import EncounterSelectors, EncounterHelper, EncounterScheduleType +from selenium import webdriver +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +from helper.Authentication import AuthenticationHelper +from helper.Bundle import BundleHelper +from helper.Clinic import ClinicHelper +from helper.Condition import ConditionHelper +from helper.Language import LanguageHelper +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper, QuestionType +from helper.Questionnaire import QuestionnaireHelper +from helper.Score import ScoreHelper +from helper.SeleniumUtils import SeleniumUtils +from helper.Survey import SurveyHelper, SurveyAssertHelper +from utils.imiseleniumtest import IMISeleniumBaseTest, IMISeleniumChromeTest + + +class URLPaths: + ADMIN_INDEX = "/admin/index" + ADMIN_INDEX_DE = "/admin/index?lang=de_DE" + MOBILE_USER_LOGIN = "/mobile/user/login" + MOBILE_USER_LOGIN_DE = "/mobile/user/login?lang=de_DE" + MOBILE_SURVEY_INDEX = "/mobile/survey/index" + LOGIN_BAD_CREDENTIALS = "/mobile/user/login?message=BadCredentialsException" + LOGIN_DISABLED_EXCEPTION = "/mobile/user/login?message=DisabledException" + PASSWORD_FORGOT = "/mobile/user/password" + + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + + +class CustomTest(IMISeleniumChromeTest, unittest.TestCase): + seleniumMode: IMISeleniumBaseTest.SeleniumMode = IMISeleniumBaseTest.SeleniumMode.LOCAL + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_url = "localhost:8080" + cls.https_base_url = "http://localhost:8080" + + def setUp(self): + chrome_options = Options() + # chrome_options.add_argument("--headless=new") + chrome_options.add_argument("start-maximized") + driver = webdriver.Chrome(options=chrome_options, service=Service(ChromeDriverManager().install())) + # Initialize the WebDriver + self.driver = webdriver.Chrome(options=chrome_options) + + # Initialize Navigation and Utils + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + # Initialize other helpers + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, navigation_helper=self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, navigation_helper=self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, navigation_helper=self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, navigation_helper=self.navigation_helper) + + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + self.encounter_helper = EncounterHelper(self.driver, self.navigation_helper) + + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + self.language_helper = LanguageHelper(self.driver, self.navigation_helper) + + def test_encounter_list(self): + created_questionnaire = {} + bundle={} + clinic={} + + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle["id"]=self.bundle_helper.save_bundle(bundle["name"]) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePatientDataLookup > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[bundle]) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + # Act + self.navigation_helper.navigate_to_manage_surveys() + + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_ENCOUNTER_TABLE, "Encounter Table button not found") + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE, "Encounter Schedule Table button not found") + + # Act - Click on "All Encounters" tab + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_TABLE) + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_ALL_ENCOUNTERS, "All Encounters table not found") + + self.utils.check_visibility_of_element(EncounterSelectors.PAGINATION_ENCOUNTER_TABLE, "Pagination for All Encounters table not found") + self.utils.check_visibility_of_element(EncounterSelectors.SEARCH_ALL_ENCOUNTERS, "Search for All Encounters table not found") + + #TODO: Action column, number of exports [after create survey function implementation] + + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_EXECUTE_ENCOUNTER, "Execute Encounter button not found") + + # Act - Click on "Scheduled Encounters" tab + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_SCHEDULED_ENCOUNTERS, "Scheduled Encounters table not found") + self.utils.check_visibility_of_element(EncounterSelectors.PAGINATION_ENCOUNTER_SCHEDULE_TABLE, "Pagination for Scheduled Encounters table not found") + + self.utils.check_visibility_of_element(EncounterSelectors.SEARCH_SCHEDULED_ENCOUNTERS, "Search for Scheduled Encounters table not found") + + encounter_id = None + try: + self.utils.click_element(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + encounter_id = self.encounter_helper.schedule_encounter("123456", clinic["name"], bundle["name"], "test@email.com", EncounterScheduleType.UNIQUELY,(datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")) + except Exception as e: + self.fail(f"Failed to schedule encounter: {e}") + + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_ACTION_COLUMN, "Action column for Scheduled Encounters table not found") + + #TODO: number of exports [after survey schedule function implementation] + + #Assert - Check for button for scheduling an encounter + try: + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + ) + except TimeoutException: + self.fail("Schedule Encounter button not found") + + finally: + self.encounter_helper.delete_scheduled_encounter(encounter_id, "123456") + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + + def test_encounter_schedule(self): + clinic={} + bundle={} + created_questionnaire = {} + + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + bundle=self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle["id"]=self.bundle_helper.save_bundle(bundle["name"]) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePatientDataLookup > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[bundle]) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + try: + self.navigation_helper.navigate_to_manage_surveys() + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + self.utils.click_element(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + except Exception as e: + self.fail(f"Failed to navigate to Schedule Encounter form: {e}") + + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_SCHEDULE_CASE_NUMBER, "Case Number input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SCHEDULE_CLINIC, "Clinic select not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SCHEDULE_BUNDLE, "Bundle select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_SCHEDULE_EMAIL, "Email input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SURVEY_TYPE, "Survey Type select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_DATE, "Date input not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_END_DATE, "End Date input not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_TIME_PERIOD, "Time Period input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_LANGUAGE, "Language select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_PERSONAL_TEXT, "Personal Text input not found") + + + encounter_id = None + try: + encounter_id = self.encounter_helper.schedule_encounter("123456", clinic["name"], bundle["name"], "test@email.com", EncounterScheduleType.UNIQUELY,(datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")) + except Exception as e: + self.fail(f"Failed to schedule encounter: {e}") + + finally: + self.encounter_helper.delete_scheduled_encounter(encounter_id, "123456") + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + def tearDown(self): + if self.driver: + self.driver.quit() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/selenium/helper/Authentication.py b/selenium/helper/Authentication.py new file mode 100644 index 00000000..755e857c --- /dev/null +++ b/selenium/helper/Authentication.py @@ -0,0 +1,267 @@ +from selenium.common import TimeoutException, NoSuchElementException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class URLPathsAuthentication: + MOBILE_USER_LOGIN = "/mobile/user/login" + FORGOT_PASSWORD_PATH = "/mobile/user/password" + +class AuthenticationSelectors: + + class Login: + BUTTON = (By.ID, "loginButton") + USERNAME = (By.ID, "username") + PASSWORD = (By.ID, "password") + LANGUAGE_SELECT = (By.ID, "localeChanger") + LOGO = (By.CSS_SELECTOR, "img.mopatImage") + FOOTER_TEXT = (By.CSS_SELECTOR, ".footer-text") + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + LANGUAGE_OPTIONS = (By.TAG_NAME, "option") + + class Logout: + ADMIN_BUTTON = (By.ID, "adminLogoutButton") + MOBILE_BUTTON = (By.ID, "logoutButton") + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + SUBMIT_BUTTON = (By.ID, "submitRequest") + LOGIN_LINK = (By.ID, "loginLink") + +class AdminIndexSelectors: + HEADER = (By.ID, "header") + NAVIGATION_BAR = (By.ID, "adminNav") + FOOTER_WRAPPER = (By.CSS_SELECTOR, ".footer-wrapper") + MOPAT_LOGO = (By.ID, "mopatImageFooter") + CLINIC_LOGO = (By.CSS_SELECTOR, "footer img.clinicLogo") + WELCOME_TEXT = (By.CSS_SELECTOR, ".panel-body") + PHONE_NUMBER = (By.CSS_SELECTOR, ".contact-phone") + EMAIL = (By.CSS_SELECTOR, ".contact-email") + IMPRESSUM_MODAL = (By.ID, "imprintDialog") + MODAL_BODY = (By.CSS_SELECTOR, ".modal-body") + BUTTON_CLOSE_MODAL = (By.CSS_SELECTOR, "button.btn-close") + IMPRESSUM_LINK = (By.ID, "imprintLink") + +class AdminIndexExpectedContent: + LOGO_SRC = "/images/logo.svg" + FOOTER_TEXT = "© 2024 Institut für Medizinische Informatik,\nUniversität Münster" + EXPECTED_LANGUAGES = ["Deutsch (Deutschland)", "Englisch (Vereinigtes Königreich)", "Spanisch (Spanien)"] + FORGOT_PASSWORD_TEXT = "Passwort vergessen?" + PHONE_NUMBER = "+49 (251) 83-55262" + EMAIL = "imi@uni-muenster.de" + WELCOME_TEXT_DE = "Willkommen zur Mobilen Patientenbefragung (MoPat)!" + + +class AuthenticationHelper: + + def __init__(self, driver): + self.driver = driver + + def login(self, username, password): + """ + :param username: The username for login. + :param password: The password for login. + """ + username_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AuthenticationSelectors.Login.USERNAME)) + + username_input.clear() + username_input.send_keys(username) + + password_input = self.driver.find_element(*AuthenticationSelectors.Login.PASSWORD) + password_input.clear() + password_input.send_keys(password) + + submit_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + AuthenticationSelectors.Login.BUTTON)) + + submit_button.click() + + def admin_login(self, username=None, password=None): + username = username or "" + password = password or "" + self.login(username, password) + + def user_login(self, username=None, password=None): + username = username or "" + password = password or "" + self.login(username, password) + + def logout(self): + current_url = self.driver.current_url + # Check if already logged out + if URLPathsAuthentication.MOBILE_USER_LOGIN in current_url: + return + + logout_selectors = [ + AuthenticationSelectors.Logout.ADMIN_BUTTON, + AuthenticationSelectors.Logout.MOBILE_BUTTON + ] + for selector in logout_selectors: + try: + logout_button = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable( + selector)) + + logout_button.click() + return + except: + continue + + def back_to_login(self): + back_to_login = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + PasswordResetSelectors.LOGIN_LINK)) + + back_to_login.click() + +class AuthenticationAssertHelper(AuthenticationHelper): + + def assert_mobile_user_login(self): + # Validate username field + username_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AuthenticationSelectors.Login.USERNAME)) + assert username_input.is_displayed(), "Username input field is not visible." + + # Validate password field + password_input = self.driver.find_element(*AuthenticationSelectors.Login.PASSWORD) + assert password_input.is_displayed(), "Password input field is not visible." + + # Validate login button + submit_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + AuthenticationSelectors.Login.BUTTON)) + assert submit_button.is_displayed(), "Login button is not visible." + + # Validate language select dropdown + try: + language_select = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + AuthenticationSelectors.Login.LANGUAGE_SELECT)) + assert language_select.is_displayed(), "Language select dropdown is not visible." + except TimeoutException: + raise AssertionError("Language select dropdown was not found or is not visible.") + + # Check available languages + options = language_select.find_elements(*AuthenticationSelectors.Login.LANGUAGE_OPTIONS) + available_languages = [option.text.strip() for option in options if option.is_displayed()] + assert set(available_languages) == set(AdminIndexExpectedContent.EXPECTED_LANGUAGES), ( + f"Languages mismatch. Expected: {AdminIndexExpectedContent.EXPECTED_LANGUAGES}, Found: {available_languages}" + ) + + # Validate MoPat logo + logo = self.driver.find_element(*AuthenticationSelectors.Login.LOGO) + assert logo.is_displayed(), "MoPat logo is not visible." + assert logo.get_attribute("src").endswith(AdminIndexExpectedContent.LOGO_SRC), "Logo source is incorrect." + + # Validate footer text + footer_text = self.driver.find_element(*AuthenticationSelectors.Login.FOOTER_TEXT).text.strip() + + # Normalize spaces and newlines for comparison + normalized_footer_text = " ".join(footer_text.split()) + normalized_expected_footer_text = " ".join(AdminIndexExpectedContent.FOOTER_TEXT.split()) + + assert normalized_footer_text == normalized_expected_footer_text, ( + f"Footer text is incorrect. Expected: '{AdminIndexExpectedContent.FOOTER_TEXT}', Found: '{footer_text}'" + ) + + # Validate 'Forgot password' link + forget_password_link = self.driver.find_element(*AuthenticationSelectors.Login.FORGOT_PASSWORD_LINK) + assert forget_password_link.is_displayed(), "'Forgot password' link is not visible." + assert forget_password_link.get_attribute("href").endswith(URLPathsAuthentication.FORGOT_PASSWORD_PATH), ( + "'Forgot password' link URL is incorrect." + ) + assert forget_password_link.text.strip() == AdminIndexExpectedContent.FORGOT_PASSWORD_TEXT, ( + "'Forgot password' link text is incorrect." + ) + + def assert_mobile_user_password(self): + try: + forgot_password_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + PasswordResetSelectors.FORGOT_PASSWORD_LINK)) + forgot_password_link.click() + + username_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + PasswordResetSelectors.USERNAME_INPUT)) + assert username_input.is_displayed(), "Username input field is not displayed." + + submit_button = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + PasswordResetSelectors.SUBMIT_BUTTON)) + assert submit_button.is_displayed(), "Submit button is not displayed." + + login_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + PasswordResetSelectors.LOGIN_LINK)) + assert login_link.is_displayed(), "Link to switch back to login is not displayed." + + except TimeoutException as e: + raise AssertionError(f"Timeout while asserting elements in '/mobile/user/password' view: {e}") + + def assert_admin_index(self): + try: + header = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AdminIndexSelectors.HEADER)) + assert header.is_displayed(), "Header is not displayed." + + nav_bar = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AdminIndexSelectors.NAVIGATION_BAR)) + assert nav_bar.is_displayed(), "Navigation bar is not displayed." + + footer = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AdminIndexSelectors.FOOTER_WRAPPER)) + assert footer.is_displayed(), "Footer wrapper is not displayed." + self.assert_footer_logo() + + try: + clinic_logo = self.driver.find_element(*AdminIndexSelectors.CLINIC_LOGO) + assert clinic_logo.is_displayed(), "Clinic logo is not displayed in the footer." + except NoSuchElementException: + pass + + welcome_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AdminIndexSelectors.WELCOME_TEXT)) + assert AdminIndexExpectedContent.WELCOME_TEXT_DE in welcome_text.text, "Welcome text is incorrect or not displayed." + + self.assert_contact_information() + + except TimeoutException as e: + raise AssertionError(f"Timeout while asserting elements in '/admin/index': {e}") + + def assert_footer_logo(self): + try: + mopat_logo = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + AdminIndexSelectors.MOPAT_LOGO)) + + # CSS-Validation + logo_visibility = self.driver.execute_script("return window.getComputedStyle(arguments[0]).visibility;", mopat_logo) + logo_display = self.driver.execute_script("return window.getComputedStyle(arguments[0]).display;", mopat_logo) + + assert logo_visibility == "visible", "MoPat logo is not visible (visibility is not 'visible')." + assert logo_display != "none", "MoPat logo is not displayed (display is 'none')." + + except TimeoutException: + raise AssertionError("MoPat logo not found in the footer.") + except AssertionError as e: + raise e + + def assert_contact_information(self): + try: + # Open the imprint modal + imprint_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + AdminIndexSelectors.IMPRESSUM_LINK)) + imprint_link.click() + + imprint_modal = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + AdminIndexSelectors.IMPRESSUM_MODAL)) + + modal_body = imprint_modal.find_element(*AdminIndexSelectors.MODAL_BODY) + modal_text = modal_body.text + + # Verify phone number and email address + assert AdminIndexExpectedContent.PHONE_NUMBER in modal_text, f"Expected phone number '{AdminIndexExpectedContent.PHONE_NUMBER}' is not in the modal." + assert AdminIndexExpectedContent.EMAIL in modal_text, f"Expected email address '{AdminIndexExpectedContent.EMAIL}' is not in the modal." + + close_button = imprint_modal.find_element(*AdminIndexSelectors.BUTTON_CLOSE_MODAL) + close_button.click() + + except TimeoutException: + raise AssertionError("Failed to open the imprint modal.") + except AssertionError as e: + raise e \ No newline at end of file diff --git a/selenium/helper/Bundle.py b/selenium/helper/Bundle.py new file mode 100644 index 00000000..c4e08e2f --- /dev/null +++ b/selenium/helper/Bundle.py @@ -0,0 +1,211 @@ +import datetime + +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver import WebDriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SearchBoxSelectors +from helper.SeleniumUtils import SeleniumUtils + + +class BundleSelectors: + BUTTON_ADD_BUNDLE = (By.ID, "addBundle") + BUTTON_SAVE = (By.ID, "saveButton") + BUTTON_MOVE_ITEM = lambda questionnaire_id: (By.ID, f"move_{questionnaire_id}") + + CHECKBOX_PUBLISH = (By.ID, "isPublished1") + CHECKBOX_NAME_PROGRESS = (By.ID, "deactivateProgressAndNameDuringSurvey1") + CHECKBOX_PROGRESS_WHOLE_PACKAGE = (By.ID, "showProgressPerBundle1") + + INPUT_NAME = (By.ID, "name") + INPUT_EDITABLE_DESCRIPTION = (By.CSS_SELECTOR, "div.note-editable") + INPUT_WELCOME_TEXT = (By.CSS_SELECTOR, "#localizedWelcomeTextCollapsableText_de_DE > div > div.note-editing-area > div.note-editable") + INPUT_END_TEXT = (By.CSS_SELECTOR, "#localizedFinalTextCollapsableText_de_DE > div:nth-child(2) > div:nth-child(2) > div:nth-child(3)") + INPUT_BUNDLE_SEARCH = (By.ID, "bundleTable_filter") + INPUT_QUESTIONNAIRE = lambda questionnaire_id: (By.ID, f"questionnaire_{questionnaire_id}") + INPUT_QUESTIONNAIRE_AVAILABLE_SEARCH = (By.ID, "availableQuestionnairesFilter") + INPUT_QUESTIONNAIRE_ASSIGNED_SEARCH = (By.ID, "assignedQuestionnairesFilter") + + BUNDLE_LINK = lambda bundle_id: (By.ID, f"bundleLink_{bundle_id}") + BUNDLE_LINK_BY_NAME = lambda bundle_name: (By.XPATH, f"//table[@id='bundleTable']//a[text()='{bundle_name}']") + + TABLE_ROWS = (By.CSS_SELECTOR, "#DataTables_Table_0 tbody tr") + TABLE_BUNDLE = (By.ID, "bundleTable") + TABLE_AVAILABLE_QUESTIONNAIRES = (By.ID, "availableQuestionnairesTable") + TABLE_ASSIGNED_QUESTIONNAIRES = (By.ID, "assignedQuestionnairesTable") + + CELL_FLAGICON=(By.CSS_SELECTOR, "#bundleTable > tbody > tr > td:nth-child(2) > img") + PAGINATION_BUNDLE = (By.CSS_SELECTOR, "#bundleTable_paginate") + +class BundleHelper: + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.navigation_helper = navigation_helper + self.utils = SeleniumUtils(driver) + + def bundle_exists(self, bundle_name): + """ + :param bundle_name: Name of the bundle to search for. + :return: True if the bundle exists, False otherwise. + """ + try: + # Fill the search box with the bundle name + self.utils.fill_text_field(BundleSelectors.INPUT_BUNDLE_SEARCH, bundle_name) + + # Check if the bundle link with the specified name exists + WebDriverWait(self.driver, 2).until( + EC.presence_of_element_located(BundleSelectors.BUNDLE_LINK_BY_NAME(bundle_name)) + ) + return True + except TimeoutException: + # No rows found within the timeout + return False + except Exception as e: + raise Exception(f"Error while checking if bundle '{bundle_name}' exists: {e}") + + def search_and_open_bundle(self, bundle_name): + """ + :param bundle_name: Name of the bundle to search for. + """ + try: + self.utils.fill_text_field(SearchBoxSelectors.BUNDLE, bundle_name) + + bundle_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + BundleSelectors.BUNDLE_LINK_BY_NAME(bundle_name))) + bundle_link.click() + + except TimeoutException: + raise Exception(f"Timeout while searching for bundle '{bundle_name}' to open.") + except Exception as e: + raise Exception(f"Error while opening bundle '{bundle_name}': {e}") + + def create_bundle(self, bundle_name=None, publish_bundle=False, questionnaires=None): + """ + :param bundle_name: Name of the bundle to create. + :param publish_bundle: Whether to publish the bundle. + :param questionnaires: List of questionnaires to assign to the bundle (optional). + """ + timestamp: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + bundle_name = bundle_name or f"Test Bundle {timestamp}" + + try: + self.navigation_helper.navigate_to_manage_bundles() + + self.utils.click_element(BundleSelectors.BUTTON_ADD_BUNDLE) + + # Add name + self.utils.fill_text_field(BundleSelectors.INPUT_NAME, bundle_name) + + # Add description + description_field = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + BundleSelectors.INPUT_EDITABLE_DESCRIPTION)) + + self.utils.scroll_to_element(BundleSelectors.INPUT_EDITABLE_DESCRIPTION) + description_field.click() + description_field.send_keys("This bundle contains questionnaires.") + + # Assign questionnaires if provided + if questionnaires: + self.assign_multiple_questionnaires_to_bundle(questionnaires) + + # Publish the bundle if requested + if publish_bundle: + self.utils.toggle_checkbox(BundleSelectors.CHECKBOX_PUBLISH, enable=True) + + return { + "name": bundle_name + } + + except Exception as e: + raise Exception(f"Error while creating bundle '{bundle_name}': {e}") + + def save_bundle(self, bundle_name): + """ + :param bundle_name: Name of the bundle to locate after saving. + :return: The extracted bundle ID. + """ + try: + # Save the bundle + self.utils.click_element(BundleSelectors.BUTTON_SAVE) + + # Wait for redirection to the bundle list + WebDriverWait(self.driver, 15).until(EC.url_contains("/bundle/list")) + + # Search for the bundle by name + self.utils.fill_text_field(SearchBoxSelectors.BUNDLE, bundle_name) + + # Extract the bundle ID from the link + bundle_link = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + (By.LINK_TEXT, bundle_name))) + + return bundle_link.get_attribute("href").split("id=")[1] + except Exception as e: + raise Exception(f"Error while saving bundle '{bundle_name}': {e}") + + def assign_multiple_questionnaires_to_bundle(self, questionnaires, bundle_id=None): + """ + :param questionnaires: List of dictionaries containing questionnaire details (e.g., {'id': str, 'name': str}). + :param bundle_id: ID of the bundle to which the questionnaires will be assigned (optional). + """ + try: + if bundle_id: + self.search_and_open_bundle(bundle_id) + + for questionnaire in questionnaires: + try: + self.utils.fill_text_field(BundleSelectors.INPUT_QUESTIONNAIRE_AVAILABLE_SEARCH, questionnaire['name']) + + questionnaire_selector = BundleSelectors.INPUT_QUESTIONNAIRE(questionnaire['id']) + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + questionnaire_selector)) + + self.utils.scroll_to_element(BundleSelectors.BUTTON_MOVE_ITEM(questionnaire['id'])) + self.utils.click_element(BundleSelectors.BUTTON_MOVE_ITEM(questionnaire['id'])) + except TimeoutException: + raise Exception(f"Timeout while assigning questionnaire '{questionnaire['name']}'.") + except Exception as e: + raise Exception(f"Error while assigning questionnaire '{questionnaire['name']}': {e}") + except Exception as e: + raise Exception(f"Error while assigning multiple questionnaires: {e}") + + def get_bundle_id(self): + edit_link = self.get_first_bundle_row().find_element(By.CSS_SELECTOR, "a[href^='fill?id=']") + href = edit_link.get_attribute('href') + # Extract ID from href using split + bundle_id = href.split('id=')[1] + return bundle_id + + def get_first_bundle_row(self): + return self.driver.find_element(*BundleSelectors.TABLE_BUNDLE) \ + .find_element(By.TAG_NAME, "tbody") \ + .find_element(By.TAG_NAME, "tr") + + def remove_multiple_questionnaires_from_bundle(self, questionnaires, bundle_id=None): + """ + :param questionnaires: List of dictionaries containing questionnaire details (e.g., {'id': str, 'name': str}). + :param bundle_id: ID of the bundle from which the questionnaires will be removed (optional). + """ + try: + if bundle_id: + self.search_and_open_bundle(bundle_id) + + for questionnaire in questionnaires: + try: + self.utils.fill_text_field(BundleSelectors.INPUT_QUESTIONNAIRE_ASSIGNED_SEARCH, + questionnaire['name']) + + questionnaire_selector = BundleSelectors.INPUT_QUESTIONNAIRE(questionnaire['id']) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(questionnaire_selector) + ) + self.utils.scroll_to_element(BundleSelectors.BUTTON_MOVE_ITEM(questionnaire['id'])) + self.utils.click_element(BundleSelectors.BUTTON_MOVE_ITEM(questionnaire['id'])) + except TimeoutException: + raise Exception(f"Timeout while removing questionnaire '{questionnaire['name']}'.") + except Exception as e: + raise Exception(f"Error while removing questionnaire '{questionnaire['name']}': {e}") + except Exception as e: + raise Exception(f"Error while removing multiple questionnaires: {e}") diff --git a/selenium/helper/Clinic.py b/selenium/helper/Clinic.py new file mode 100644 index 00000000..798949b8 --- /dev/null +++ b/selenium/helper/Clinic.py @@ -0,0 +1,295 @@ +import datetime + +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver import WebDriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SearchBoxSelectors, DropdownMethod +from helper.SeleniumUtils import SeleniumUtils + +class URLPathsClinic: + CLINIC_LIST = "/clinic/list" + +class ClinicSelectors: + BUTTON_ADD_CLINIC = (By.ID, "addClinic") + BUTTON_SAVE = (By.ID, "saveButton") + BUTTON_DELETE_CLINIC = lambda clinic_id: (By.ID, f"removeClinic_{clinic_id}") + BUTTON_MOVE_ITEM = lambda bundle_id: (By.ID, f"move_{bundle_id}") + + DROPDOWN_CONFIG = lambda parent_id: (By.CSS_SELECTOR, f"li[parentid='{parent_id}'] select") + + INPUT_CLINIC_NAME = (By.ID, "name") + INPUT_CLINIC_EMAIL = (By.ID, 'email') + INPUT_EDITABLE_DESCRIPTION = (By.CSS_SELECTOR, "div.note-editable") + INPUT_USER_AVAILABLE_SEARCH = (By.ID, "availableUsersFilter") + INPUT_USER_ASSIGNED_SEARCH = (By.ID, "assignedUsersFilter") + INPUT_USER = lambda username: (By.ID, f"user_{username}") + INPUT_BUNDLE_AVAILABLE_SEARCH = (By.ID, "availableBundlesFilter") + INPUT_BUNDLE_ASSIGNED_SEARCH = (By.ID, "assignedBundlesFilter") + INPUT_BUNDLE = lambda bundle_id: (By.ID, f"bundle_{bundle_id}") + + # Configuration selectors + DIV_CLINIC_CONFIGURATION = (By.ID, "clinicConfigurations") + CLINIC_CONFIGURATION_LIST = (By.CSS_SELECTOR, "#clinicConfigurations > div > div > ul > li") + CONFIG_USE_PATIENT_DATA_LOOKUP = (By.CSS_SELECTOR, "li#usePatientDataLookup input[type='checkbox']") + CONFIG_USE_PSEUDONYMIZATION_SERVICE = (By.CSS_SELECTOR, "li#usePseudonymizationService input[type='checkbox']") + CONFIG_REGISTER_PATIENT_DATA = (By.CSS_SELECTOR, "li#registerPatientData input[type='checkbox']") + + TABLE_ROW_LINK = (By.CSS_SELECTOR, "#clinicTable > tbody > tr > td.dtr-control > a") + TABLE_CLINIC= (By.CSS_SELECTOR, "#clinicTable") + TABLE_SEARCH = (By.ID, 'clinicTable_filter') + TABLE_ACTION_BUTTONS= (By.CSS_SELECTOR, "td.actionColumn") + + TABLE_AVAIALBLE_BUNDLES = (By.ID, "availableBundlesTable") + TABLE_ASSIGNED_BUNDLES = (By.ID, "assignedBundlesTable") + TABLE_AVAIALBLE_USERS = (By.ID, "availableUsersTable") + TABLE_ASSIGNED_USERS = (By.ID, "assignedUsersTable") + + PAGINATION_CLINIC_TABLE = (By.ID, "clinicTable_paginate") + +class ClinicHelper: + + DEFAULT_DESCRIPTION = "This description of the clinic is a dummy text." + + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.navigation_helper = navigation_helper + self.utils = SeleniumUtils(driver) + + def create_clinic(self, clinic_name=None, clinic_description=None, configurations=None, bundles=None, users=None): + """ + :param clinic_name: Name of the clinic to be created. + :param clinic_description: Description of the clinic. + :param configurations: List of configurations to enable. Each configuration is a dict with: + - 'selector': The selector for the configuration checkbox. + - 'dropdown_value': Optional dropdown value to select for the configuration (default: None). + :param bundles: List of bundles to be associated with the clinic (optional). + param users: A list of users to be assigned to the clinic + """ + timestamp: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + clinic_name = clinic_name or f"Test Clinic {timestamp}" + clinic_description = clinic_description or self.DEFAULT_DESCRIPTION + + try: + # Click "Add Clinic" button + self.utils.click_element(ClinicSelectors.BUTTON_ADD_CLINIC) + + # Fill in the clinic name + self.utils.fill_text_field(ClinicSelectors.INPUT_CLINIC_NAME, clinic_name) + + # Add description + description_field = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + ClinicSelectors.INPUT_EDITABLE_DESCRIPTION)) + description_field.click() # self.utils.click_element(ClinicSelectors.INPUT_EDITABLE_DESCRIPTION) + description_field.send_keys(clinic_description) + + # Configure settings if provided + if configurations: + for config in configurations: + selector = config.get('selector') + dropdown_value = config.get('dropdown_value') + if selector: + self.configure_clinic_setting(selector, dropdown_value) + + # Assign bundles if provided + if bundles: + self.assign_multiple_bundes_to_clinic(bundles) + + # Assign users if provided + if users: + self.assign_multiple_users_to_clinic(users) + + return clinic_name + + except Exception as e: + raise Exception(f"Error while creating clinic '{clinic_name}': {e}") + + def save_clinic(self, clinic_name): + """ + :param clinic_name: Name of the clinic being saved. + """ + try: + # Save the clinic + self.utils.click_element(ClinicSelectors.BUTTON_SAVE) + + # Search for the bundle by name + self.utils.fill_text_field(SearchBoxSelectors.CLINIC, clinic_name) + + # Extract the bundle ID from the link + clinic_link = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + (By.LINK_TEXT, clinic_name))) + + return clinic_link.get_attribute("href").split("id=")[1] + except Exception as e: + raise Exception(f"Error while saving clinic '{clinic_name}': {e}") + + def delete_clinic(self, clinic_id): + """ + Deletes a clinic by its ID. + + :param clinic_id: ID of the clinic to be deleted. + :return: True if the clinic was successfully deleted, False if it does not exist. + """ + try: + # Navigate to "Manage Clinics" + self.navigation_helper.navigate_to_manage_clinics() + + # Wait for and click the delete button for the specified clinic + delete_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + ClinicSelectors.BUTTON_DELETE_CLINIC(clinic_id))) + delete_button.click() + return True + except TimeoutException: + # Clinic not found + return False + except Exception as e: + raise Exception(f"Error while deleting clinic with ID '{clinic_id}': {e}") + + def configure_clinic_setting(self, config_selector, config_dropdown_value=None): + """ + :param config_selector: The selector for the configuration element (e.g., ClinicSelectors.CONFIG_USE_PATIENT_DATA_LOOKUP). + :param config_dropdown_value: Optional dropdown value to select for the configuration. + """ + try: + # Enable the checkbox for the configuration + checkbox = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + config_selector)) + + self.utils.scroll_to_element(config_selector) + self.utils.toggle_checkbox(config_selector, enable=True) + + # Select the value from the dropdown if specified + if config_dropdown_value: + parent_id = checkbox.get_attribute("triggerid") + self.utils.select_dropdown(ClinicSelectors.DROPDOWN_CONFIG(parent_id), config_dropdown_value, DropdownMethod.VALUE) + except Exception as e: + raise Exception( + f"Error configuring setting '{config_selector}' with dropdown value '{config_dropdown_value}': {e}") + + def assign_multiple_bundes_to_clinic(self, bundles, clinic_id=None): + """ + :param bundles: List of dictionaries containing bundle details (e.g., {'id': str, 'name': str}). + :param clinic_id: ID of the clinic to which the bundles will be assigned (optional). + """ + try: + if clinic_id: + self.search_and_open_clinic(clinic_id) + + for bundle in bundles: + try: + self.utils.fill_text_field(ClinicSelectors.INPUT_BUNDLE_AVAILABLE_SEARCH, bundle['name']) + + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + ClinicSelectors.INPUT_BUNDLE(bundle['id']))) + + self.utils.scroll_to_element(ClinicSelectors.BUTTON_MOVE_ITEM(bundle['id'])) + self.utils.click_element(ClinicSelectors.BUTTON_MOVE_ITEM(bundle['id'])) + except TimeoutException: + raise Exception(f"Timeout while assigning bundle '{bundle['name']}'.") + except Exception as e: + raise Exception(f"Error while assigning bundle '{bundle['name']}': {e}") + except Exception as e: + raise Exception(f"Error while assigning multiple bundles: {e}") + + def assign_multiple_users_to_clinic(self, usernames, clinic_id=None): + """ + :param usernames: List of usernames. + :param clinic_id: ID of the clinic to which the bundles will be assigned (optional). + """ + try: + if clinic_id: + self.search_and_open_clinic(clinic_id) + + for username in usernames: + try: + self.utils.fill_text_field(ClinicSelectors.INPUT_USER_AVAILABLE_SEARCH, username) + + user_selector = ClinicSelectors.INPUT_USER(username) + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + user_selector)) + self.utils.scroll_to_element(ClinicSelectors.BUTTON_MOVE_ITEM(username)) + self.utils.click_element(ClinicSelectors.BUTTON_MOVE_ITEM(username)) + except TimeoutException: + raise Exception(f"Timeout while assigning user '{username}'.") + except Exception as e: + raise Exception(f"Error while assigning user '{username}': {e}") + except Exception as e: + raise Exception(f"Error while assigning multiple users: {e}") + + def search_and_open_clinic(self, clinic_name): + """ + :param clinic_name: Name of the clinic to search for. + """ + # Navigate to the clinic management page if not already there + if URLPathsClinic.CLINIC_LIST not in self.driver.current_url: + self.navigation_helper.navigate_to_manage_clinics() + + try: + # Fill the search box with the clinic name + self.utils.fill_text_field(SearchBoxSelectors.CLINIC, clinic_name) + + # Wait for the clinic link to be clickable + clinic_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + ClinicSelectors.TABLE_ROW_LINK)) + + # Click the clinic link + clinic_link.click() + except TimeoutException: + raise Exception(f"Timeout while searching for clinic '{clinic_name}' to open.") + except Exception as e: + raise Exception(f"Error while opening clinic '{clinic_name}': {e}") + + def remove_multiple_bundes_from_clinic(self, bundles, clinic_id=None): + """ + :param bundles: List of dictionaries containing bundle details (e.g., {'id': str, 'name': str}). + :param clinic_id: ID of the clinic to which the bundles will be removed (optional). + """ + try: + if clinic_id: + self.search_and_open_clinic(clinic_id) + + for bundle in bundles: + try: + self.utils.fill_text_field(ClinicSelectors.INPUT_BUNDLE_ASSIGNED_SEARCH, bundle['name']) + + bundle_selector = ClinicSelectors.INPUT_BUNDLE(bundle['id']) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(bundle_selector) + ) + self.utils.scroll_to_element(ClinicSelectors.BUTTON_MOVE_ITEM(bundle['id'])) + self.utils.click_element(ClinicSelectors.BUTTON_MOVE_ITEM(bundle['id'])) + except TimeoutException: + raise Exception(f"Timeout while assigning bundle '{bundle['name']}'.") + except Exception as e: + raise Exception(f"Error while assigning bundle '{bundle['name']}': {e}") + except Exception as e: + raise Exception(f"Error while assigning multiple bundles: {e}") + + def remove_multiple_users_from_clinic(self, usernames, clinic_id=None): + """ + :param usernames: List of usernames. + :param clinic_id: ID of the clinic to which the bundles will be removed (optional). + """ + try: + if clinic_id: + self.search_and_open_clinic(clinic_id) + + for username in usernames: + try: + self.utils.fill_text_field(ClinicSelectors.INPUT_USER_ASSIGNED_SEARCH, username) + + user_selector = ClinicSelectors.INPUT_USER(username) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(user_selector) + ) + self.utils.scroll_to_element(ClinicSelectors.BUTTON_MOVE_ITEM(username)) + self.utils.click_element(ClinicSelectors.BUTTON_MOVE_ITEM(username)) + except TimeoutException: + raise Exception(f"Timeout while assigning user '{username}'.") + except Exception as e: + raise Exception(f"Error while assigning user '{username}': {e}") + except Exception as e: + raise Exception(f"Error while assigning multiple users: {e}") \ No newline at end of file diff --git a/selenium/helper/Condition.py b/selenium/helper/Condition.py new file mode 100644 index 00000000..42a9c1c0 --- /dev/null +++ b/selenium/helper/Condition.py @@ -0,0 +1,439 @@ +from selenium.common import NoSuchElementException, TimeoutException +from selenium.webdriver import Keys, ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + +from helper.Question import QuestionType +from helper.Questionnaire import QuestionnaireHelper +from helper.SeleniumUtils import DropdownMethod + + +class ConditionSelectors: + BUTTON_ADD_CONDITION = (By.ID, 'addCondition') + BUTTON_ADD_TARGET = (By.ID, 'addTargetButton') + BUTTON_SAVE_CONDITION = (By.ID, 'saveCondition') + BUTTON_CANCEL = (By.ID, "cancelButton") + BUTTON_BACK_TO_QUESTIONNAIRE = (By.ID, 'backToQuestionnaire') + BUTTON_DELETE_CONDITION = lambda condition_id : (By.XPATH, f"//a[contains(@href, 'remove?id={condition_id}')]") + + DROPDOWN_TRIGGER = (By.ID, "triggerId") + DROPDOWN_QUESTION = (By.ID, 'questionDropDown0') + + INPUT_THRESHOLD = (By.ID, 'conditionDTOs0.thresholdValue') + + TABLE_CONDITIONS_QUESTION = (By.ID, "DataTables_Table_0") + TABLE_CONDITIONS_ANSWER = (By.ID, "DataTables_Table_1") + TABLE_CONDITIONS_QUESTIONNAIRE = (By.ID, "DataTables_Table_2") + TABLE_LAST_ROW = (By.XPATH, ".//tbody/tr[last()]") + + # Search input fields for each table + SEARCH_INPUT_CONDITIONS_QUESTION = (By.CSS_SELECTOR, "#DataTables_Table_0_filter input") + SEARCH_INPUT_CONDITIONS_ANSWER = (By.CSS_SELECTOR, "#DataTables_Table_1_filter input") + SEARCH_INPUT_CONDITIONS_QUESTIONNAIRE = (By.CSS_SELECTOR, "#DataTables_Table_2_filter input") + + # Empty row in each table + EMPTY_ROW = (By.CSS_SELECTOR, "tbody .dataTables_empty") + + CONDITION_LINK = lambda question_id: (By.XPATH, f'//a[contains(@href, "/condition/listQuestionConditions?questionId={question_id}")]') + + THRESHOLD_TYPE = (By.ID, "conditionDTOs0.thresholdType") + class ThresholdType: + SMALLER_THAN = "SMALLER_THAN" + SMALLER_THAN_EQUALS = "SMALLER_THAN_EQUALS" + EQUALS = "EQUALS" + BIGGER_THAN_EQUALS = "BIGGER_THAN_EQUALS" + BIGGER_THAN = "BIGGER_THAN" + NOT_EQUALS = "NOT_EQUALS" + + DROPDOWN_TARGET_CLASS = (By.ID, "targetClass0") + class TargetType: + QUESTION = "de.imi.mopat.model.Question" + ANSWER = "de.imi.mopat.model.SelectAnswer" + QUESTIONNAIRE = "de.imi.mopat.model.Questionnaire" + + DROPDOWN_TARGET_QUESTION = (By.ID, 'targetAnswerQuestionDropDown0') + DROPDOWN_TARGET_ANSWER = (By.ID, 'questionDropDown0') + DROPDOWN_ANSWER_TARGET = (By.ID, 'answerDropDown0') + + DROPDOWN_ACTION = (By.ID, 'action0') + class ActionType: + DISABLE = "DISABLE" + ENABLE = "ENABLE" + +class ConditionHelper(QuestionnaireHelper): + + def can_add_condition(self, question_id): + """ + :param question_id: The ID of the question. + :return: True if a condition can be added, False otherwise. + """ + try: + self.driver.find_element(*ConditionSelectors.CONDITION_LINK(question_id)) + return True + except NoSuchElementException: + return False + + def set_target_and_action(self, target_class, target_option_index, answer_target_index, action): + """ + :param target_class: The type of the target (e.g., 'ANSWER' or 'QUESTION'). + :param target_option_index: The index of the target question/answer in the dropdown. + :param answer_target_index: The index of the answer target if the target class is 'ANSWER'. + :param action: The action to perform (e.g., 'DISABLE' or 'ENABLE'). + """ + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_TARGET_CLASS, target_class, DropdownMethod.VALUE) + + if target_option_index > 0: + if target_class == ConditionSelectors.TargetType.ANSWER: + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_TARGET_QUESTION, target_option_index, DropdownMethod.INDEX) + else: + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_TARGET_QUESTION, target_option_index,DropdownMethod.INDEX) + self.utils.handle_popup_alert(accept=True) + + if target_class == ConditionSelectors.TargetType.ANSWER: + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_ANSWER_TARGET, answer_target_index, DropdownMethod.INDEX) + + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_ACTION, action, DropdownMethod.VALUE) + + def add_condition_for_triggered_questions(self, trigger_index=0, + target_type=ConditionSelectors.TargetType.QUESTION, target_option_index=0, + answer_target_index=0, action=ConditionSelectors.ActionType.DISABLE): + """ + :param trigger_index: The index of the trigger option in the dropdown (default is 0). + :param target_type: The target type for the condition (e.g., 'QUESTION', 'ANSWER'). + :param target_option_index: The index of the target question/answer in the dropdown. + :param answer_target_index: The index of the answer target if the target class is 'ANSWER'. + :param action: The action to perform (e.g., 'DISABLE' or 'ENABLE'). + """ + self.driver.find_element(*ConditionSelectors.BUTTON_ADD_CONDITION).click() + + # Set the trigger option + if trigger_index > 0: + self.utils.select_dropdown(ConditionSelectors.DROPDOWN_TRIGGER, trigger_index, DropdownMethod.INDEX) + self.utils.handle_popup_alert(True) + + # Set the target and action + self.set_target_and_action(target_type, target_option_index, answer_target_index, action) + + # Save the condition + self.driver.find_element(*ConditionSelectors.BUTTON_SAVE_CONDITION).click() + + return self.get_last_added_condition_id(target_type) + + + def add_condition_for_threshold_questions(self, threshold_type=ConditionSelectors.ThresholdType.EQUALS, + threshold_steps=1, target_type=ConditionSelectors.TargetType.QUESTION, target_option_index=0, + answer_target_index=0, action=ConditionSelectors.ActionType.DISABLE): + """ + Adds a condition for threshold-based question types like Slider, Number Input, and Number Checkbox. + + :param threshold_type: The type of threshold to use (e.g., 'EQUALS'). + :param threshold_steps: The number of arrow-up key presses to set the threshold value. + :param target_type: The target type for the condition (e.g., 'QUESTION', 'ANSWER'). + :param target_option_index: The index of the target question/answer in the dropdown. + :param answer_target_index: The index of the answer target if the target class is 'ANSWER'. + :param action: The action to perform (e.g., 'DISABLE' or 'ENABLE'). + """ + self.driver.find_element(*ConditionSelectors.BUTTON_ADD_CONDITION).click() + + # Set the threshold type and value + self.utils.select_dropdown(ConditionSelectors.THRESHOLD_TYPE, threshold_type, DropdownMethod.VALUE) + if threshold_type != ConditionSelectors.ThresholdType.SMALLER_THAN: + self.utils.handle_popup_alert(True) + + # Increment the threshold value using the arrow key + for _ in range(threshold_steps): + threshold_input = self.driver.find_element(*ConditionSelectors.INPUT_THRESHOLD) + threshold_input.click() + threshold_input.send_keys(Keys.ARROW_UP) + self.utils.handle_popup_alert(True) + + # Set the target and action + self.set_target_and_action(target_type, target_option_index, answer_target_index, action) + + # Save the condition + self.driver.find_element(*ConditionSelectors.BUTTON_SAVE_CONDITION).click() + + return self.get_last_added_condition_id(target_type) + + + def add_basic_condition_based_on_question_type(self, target_type, question): + """ + :param target_type: The target type for the condition ('QUESTION', 'ANSWER', 'QUESTIONNAIRE'). + :param question: The question details as a dictionary. + """ + if question['type'] in [QuestionType.SLIDER, QuestionType.NUMBER_INPUT, QuestionType.NUMBER_CHECKBOX]: + return self.add_condition_for_threshold_questions(target_type=target_type) + elif question['type'] in [QuestionType.DROP_DOWN, QuestionType.MULTIPLE_CHOICE]: + return self.add_condition_for_triggered_questions(target_type=target_type) + else: + raise ValueError(f"Unsupported QuestionType: {question['type']}") + + def navigate_back_to_questions_of_questionnaire(self): + try: + back_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + ConditionSelectors.BUTTON_BACK_TO_QUESTIONNAIRE)) + back_button.click() + except TimeoutException: + print("Failed to find the 'Back to Questionnaire' button.") + + def open_conditions_of_question(self, question_id): + self.driver.find_element(*ConditionSelectors.CONDITION_LINK(question_id)).click() + + def click_add_condition(self): + self.driver.find_element(*ConditionSelectors.BUTTON_ADD_CONDITION).click() + + def cancel_condition_editing(self): + self.utils.click_element(ConditionSelectors.BUTTON_CANCEL) + + def get_last_added_condition_id(self, target_type): + """ + Retrieves the ID of the most recently added condition from the appropriate table. + + :param target_type: The target type of the condition (e.g., 'QUESTION', 'ANSWER', 'QUESTIONNAIRE'). + :return: The ID of the last condition as a string. + :raises ValueError: If the target type is unsupported. + :raises Exception: If the ID cannot be found. + """ + # Map the target type to the corresponding table selector + if target_type == ConditionSelectors.TargetType.QUESTION: + target_table_selector = ConditionSelectors.TABLE_CONDITIONS_QUESTION + elif target_type == ConditionSelectors.TargetType.ANSWER: + target_table_selector = ConditionSelectors.TABLE_CONDITIONS_ANSWER + elif target_type == ConditionSelectors.TargetType.QUESTIONNAIRE: + target_table_selector = ConditionSelectors.TABLE_CONDITIONS_QUESTIONNAIRE + else: + raise ValueError(f"Unsupported TargetType: {target_type}") + + # Wait for the table to be present + WebDriverWait(self.driver, 30).until(EC.presence_of_element_located(target_table_selector)) + + # Locate the last row in the table + table = self.driver.find_element(*target_table_selector) + last_row = table.find_element(*ConditionSelectors.TABLE_LAST_ROW) + + # Extract the ID from the last row + condition_id = last_row.get_attribute("id") + + # Ensure the ID is found + if not condition_id: + raise Exception("The ID of the last condition could not be found.") + + return condition_id + + + def delete_condition(self, condition_id, questionnaire=None, question_id=None): + """ + :param condition_id: The ID of the condition to delete. + :param questionnaire: The questionnaire containing the condition (optional). + :param question_id: The question associated with the condition (optional). + """ + try: + # Navigate to the questionnaire if provided + if questionnaire: + self.navigation_helper.navigate_to_questions_of_questionnaire(questionnaire['id'], questionnaire['name']) + + # Open the conditions of the specified question if provided + if question_id: + self.open_conditions_of_question(question_id) + + # Locate and click the delete button for the condition + delete_button = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(ConditionSelectors.BUTTON_DELETE_CONDITION(condition_id)) + ) + delete_button.click() + + except Exception as e: + raise AssertionError(f"Error deleting condition with ID {condition_id}: {e}") + +class ConditionAssertHelper(ConditionHelper): + + def assert_condition_in_section(self, table_selector, condition_text): + """ + :param table_selector: The table selector to check. + :param condition_text: The condition text to search for. + """ + try: + table = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(table_selector)) + rows = table.find_elements(By.CSS_SELECTOR, "tbody tr") + + assert any(condition_text in row.text for row in rows), ( + f"Condition '{condition_text}' not found in table {table_selector}." + ) + + except Exception as e: + raise AssertionError(f"Error asserting condition in section: {e}") + + def assert_search_functionality(self, search_input_selector, table_selector, condition_text): + """ + :param search_input_selector: The search input field selector. + :param table_selector: The table selector to check. + :param condition_text: The condition text to search for. + """ + try: + # Input search text + search_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(search_input_selector) + ) + search_input.clear() + search_input.send_keys(condition_text) + + # Verify the condition appears in the table + self.assert_condition_in_section(table_selector, condition_text) + + search_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(search_input_selector) + ) + search_input.clear() + except Exception as e: + raise AssertionError(f"Error asserting search functionality: {e}") + + def assert_condition_list_and_search_de(self): + + # Assert that the sections are displayed + try: + # Wait for and verify the three tables are visible + tables = [ + ConditionSelectors.TABLE_CONDITIONS_QUESTION, + ConditionSelectors.TABLE_CONDITIONS_ANSWER, + ConditionSelectors.TABLE_CONDITIONS_QUESTIONNAIRE + ] + + for table in tables: + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(table)) + assert self.driver.find_element(*table).is_displayed(), f"Table {table} is not displayed." + + except Exception as e: + raise AssertionError(f"Error asserting conditions sections: {e}") + + # Assert conditions in respective sections + self.assert_condition_in_section( + ConditionSelectors.TABLE_CONDITIONS_QUESTION, "Die Frage" + ) + self.assert_condition_in_section( + ConditionSelectors.TABLE_CONDITIONS_ANSWER, "Die Antwort" + ) + self.assert_condition_in_section( + ConditionSelectors.TABLE_CONDITIONS_QUESTIONNAIRE, "Den Fragebogen" + ) + + # Test search functionality + self.assert_search_functionality( + ConditionSelectors.SEARCH_INPUT_CONDITIONS_QUESTION, + ConditionSelectors.TABLE_CONDITIONS_QUESTION, + "Die Frage" + ) + + + def assert_add_condition_page(self, questionnaire): + + questions = questionnaire['questions'] + number_of_questions = len(questions) + + info_text_question = next((question for question in questions if question['type'] == QuestionType.INFO_TEXT), None) + + if not info_text_question: + self.click_add_question_button() + info_text_question = self.question_helper.add_question_by_type_default_value(QuestionType.INFO_TEXT) + info_text_question['id'] = self.question_helper.save_question() + number_of_questions += 1 + + self.reorder_question(info_text_question['id'], number_of_questions-1) + + try: + for question in questions: + if question['type'] not in [QuestionType.SLIDER, QuestionType.MULTIPLE_CHOICE, QuestionType.NUMBER_CHECKBOX, QuestionType.DROP_DOWN, QuestionType.NUMBER_INPUT]: + continue + # Open the 'Add Condition' page + self.open_conditions_of_question(question['id']) + self.click_add_condition() + # Validate the inputs for adding a condition + self.validate_condition_inputs(question) + self.cancel_condition_editing() + self.navigate_back_to_questions_of_questionnaire() + + question = next((question for question in questions if question['type'] in [QuestionType.SLIDER, QuestionType.MULTIPLE_CHOICE, QuestionType.NUMBER_CHECKBOX, QuestionType.DROP_DOWN, QuestionType.NUMBER_INPUT]),None) + self.open_conditions_of_question(question['id']) + + target_type_question = ConditionSelectors.TargetType.QUESTION + # Assert no conditions are initially present in the table + self.assert_condition_count(target_type_question, expected_count=0) + + condition_id = self.add_basic_condition_based_on_question_type(target_type_question, question) + + # Assert the condition is successfully created and added to the table + self.assert_condition_count(target_type_question, expected_count=1) + + self.delete_condition(condition_id) + + except Exception as e: + raise AssertionError(f"Error in assert_condition_addCondition_page: {e}") + + def validate_condition_inputs(self, question): + """ + :param question: A dictionary containing the details of the question. + :raises AssertionError: If any expected input is not displayed. + """ + try: + # Common inputs for all question types + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ConditionSelectors.DROPDOWN_TARGET_CLASS)) + assert self.driver.find_element( + *ConditionSelectors.DROPDOWN_TARGET_CLASS).is_displayed(), "Target class dropdown is not displayed." + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(ConditionSelectors.DROPDOWN_ACTION)) + assert self.driver.find_element( + *ConditionSelectors.DROPDOWN_ACTION).is_displayed(), "Action dropdown is not displayed." + + # Specific inputs based on question type + if question['type'] in [QuestionType.SLIDER, QuestionType.NUMBER_CHECKBOX, QuestionType.NUMBER_INPUT]: + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(ConditionSelectors.THRESHOLD_TYPE)) + assert self.driver.find_element( + *ConditionSelectors.THRESHOLD_TYPE).is_displayed(), "Threshold type dropdown is not displayed." + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(ConditionSelectors.INPUT_THRESHOLD)) + assert self.driver.find_element( + *ConditionSelectors.INPUT_THRESHOLD).is_displayed(), "Threshold input is not displayed." + + elif question['type'] in [QuestionType.MULTIPLE_CHOICE, QuestionType.DROP_DOWN]: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ConditionSelectors.DROPDOWN_TRIGGER)) + assert self.driver.find_element( + *ConditionSelectors.DROPDOWN_TRIGGER).is_displayed(), "Trigger dropdown is not displayed." + + else: + raise ValueError(f"Unsupported QuestionType: {question['type']}") + + except Exception as e: + raise AssertionError(f"Error validating condition inputs for question type '{question['type']}': {e}") + + def assert_condition_count(self, target_type, expected_count): + """ + Asserts the number of conditions in the table matches the expected count. + + :param target_type: The target type for the condition (e.g., 'QUESTION', 'ANSWER', 'QUESTIONNAIRE'). + :param expected_count: The expected number of conditions in the table. + """ + # Determine the table selector based on the target type + if target_type == ConditionSelectors.TargetType.QUESTION: + table_selector = ConditionSelectors.TABLE_CONDITIONS_QUESTION + elif target_type == ConditionSelectors.TargetType.ANSWER: + table_selector = ConditionSelectors.TABLE_CONDITIONS_ANSWER + else: + table_selector = ConditionSelectors.TABLE_CONDITIONS_QUESTIONNAIRE + + # Wait for the table to be present + table = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(table_selector) + ) + + # Find all rows with an `id` attribute in the table + rows_with_id = table.find_elements(By.XPATH, ".//tbody/tr[@id]") + + # Check if the number of rows with an `id` matches the expected count + if not rows_with_id and expected_count == 0: + # Special case: Table is empty, and no conditions are expected + return + + # Assert the count of rows with an `id` + assert len(rows_with_id) == expected_count, ( + f"Expected {expected_count} conditions, but found {len(rows_with_id)}." + ) \ No newline at end of file diff --git a/selenium/helper/Configuration.py b/selenium/helper/Configuration.py new file mode 100644 index 00000000..ec938c53 --- /dev/null +++ b/selenium/helper/Configuration.py @@ -0,0 +1,143 @@ +import os +from selenium.common import NoSuchElementException, TimeoutException +from selenium.webdriver import Keys, ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + +from helper.Question import QuestionType +from helper.Questionnaire import QuestionnaireHelper +from helper.SeleniumUtils import DropdownMethod, SeleniumUtils +from helper.Navigation import NavigationHelper + + +class ConfigurationSelectors: + SELECT_LANGUAGE = (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs0\\.value") + INPUT_ADDITIONAL_LOGO = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.general > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(2) > div:nth-child(1) > div:nth-child(2) > div:nth-child(4) > div:nth-child(3) > div:nth-child(1) > div:nth-child(5) > input:nth-child(3)") + INPUT_CASE_NUMBER_TYPE= (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs2\\.value") + INPUT_STORAGE_PATH_FOR_UPLOADS = (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs3\\.value") + INPUT_BASE_URL = (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs4\\.value") + INPUT_PATH_UPLOAD_IMAGES = (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs5\\.value") + INPUT_PATH_MAIN_DIRECTORY = (By.ID, "configurationGroupDTOsgeneral0\\.configurationDTOs6\\.value") + + CHECKBOX_AD_AUTH = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.activeDirectoryAuthentication > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_URL_AD = (By.ID, "configurationGroupDTOsactiveDirectoryAuthentication0.configurationDTOs0.children0.value") + INPUT_DOMAIN_AD = (By.ID, "configurationGroupDTOsactiveDirectoryAuthentication0.configurationDTOs0.children1.value") + SELECT_DEFAULT_LANGUAGE_AD = (By.ID, "configurationGroupDTOsactiveDirectoryAuthentication0.configurationDTOs0.children2.value") + INPUT_PHONE_NUMBER_AD = (By.ID, "configurationGroupDTOsactiveDirectoryAuthentication0.configurationDTOs0.children3.value") + + CHECKBOX_PATIENT_LOOKUP = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.usePatientLookUp > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + SELECT_PATIENT_LOOKUP_IMPLEMENTATION = (By.ID, "configurationGroupDTOsusePatientLookUp0\\.configurationDTOs1\\.value") + INPUT_HOST_PATIENT_LOOKUP = (By.ID, "configurationGroupDTOsusePatientLookUp0\\.configurationDTOs2\\.value") + INPUT_PORT_PATIENT_LOOKUP = (By.ID, "configurationGroupDTOsusePatientLookUp0\\.configurationDTOs3\\.value") + + INPUT_PSEUDONYMIZATION_URL = (By.ID, "configurationGroupDTOspseudonymization0\\.configurationDTOs0\\.value") + INPUT_PSEUDONYMIZATION_API_KEY = (By.ID, "configurationGroupDTOspseudonymization0\\.configurationDTOs1\\.value") + + INPUT_OID = (By.ID, "configurationGroupDTOsmetadataExporter0\\.configurationDTOs0\\.value") + INPUT_URL_ODM_TO_PDF = (By.ID, "configurationGroupDTOsmetadataExporter0\\.configurationDTOs1\\.value") + INPUT_SYSTEM_URI_FOR_FHIR = (By.ID, "configurationGroupDTOsmetadataExporter0\\.configurationDTOs2\\.value") + + INPUT_EXPORT_PATH_ORBIS = (By.ID, "configurationGroupDTOsORBIS0\\.configurationDTOs0\\.value") + + CHECKBOX_EXPORT_HL7_INTO_DIRECTORY = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_EXPORT_PATH_HL7_DIRECTORY = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs0\\.children0\\.value") + + CHECKBOX_EXPORT_HL7_VIA_SERVER = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(3) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_HL7_HOST = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children0\\.value") + INPUT_HL7_PORT = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children1\\.value") + INPUT_SENDING_FACILITY = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children2\\.value") + INPUT_RECEIVING_APPLICATION = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children3\\.value") + INPUT_RECEIVING_FACILITY = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children4\\.value") + INPUT_FILE_ORDER_NUMBER = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs1\\.children5\\.value") + + CHECKBOX_ENCRYPT_MESSAGE_TLS = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(10) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + FILE_PATH_CERTIFICATE = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(11) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div:nth-child(3) > div:nth-child(1) > input:nth-child(1)") + + CHECKBOX_USE_CERTIFICATE = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(12) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + FILE_PKCS_ARCHIVE = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.HLSeven > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(13) > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div:nth-child(3) > div:nth-child(1) > input:nth-child(1)") + INPUT_PASSWORD_PKCS_ARCHIVE = (By.ID, "configurationGroupDTOsHLSeven0\\.configurationDTOs3\\.children1\\.value") + + CHECKBOX_EXPORT_ODM_INTO_DIRECTORY = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.ODM > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_EXPORT_PATH_ODM_DIRECTORY = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs0\\.children0\\.value") + + CHECKBOX_EXPORT_ODM_VIA_REST = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.ODM > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(3) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_URL_REST_INTERFACE = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs1\\.children0\\.value") + + CHECKBOX_EXPORT_ODM_VIA_HL7 = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.ODM > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(5) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_ODM_HL7_HOST = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children0\\.value") + INPUT_ODM_HL7_PORT = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children1\\.value") + INPUT_ODM_SENDING_FACILITY = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children2\\.value") + INPUT_ODM_RECEIVING_APPLICATION = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children3\\.value") + INPUT_ODM_RECEIVING_FACILITY = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children4\\.value") + INPUT_ODM_FILE_ORDER_NUMBER = (By.ID, "configurationGroupDTOsODM0\\.configurationDTOs2\\.children5\\.value") + + CHECKBOX_EXPORT_FHIR_INTO_DIRECTORY = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.FHIR > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_EXPORT_PATH_FHIR_DIRECTORY = (By.ID, "configurationGroupDTOsFHIR0\\.configurationDTOs0\\.children0\\.value") + + CHECKBOX_EXPORT_FHIR_INTO_COMMUNICATION_SERVER = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.FHIR > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(3) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_FHIR_HOST = (By.ID, "configurationGroupDTOsFHIR0\\.configurationDTOs1\\.children0\\.value") + + CHECKBOX_EXPORT_REDCAP_INTO_DIRECTORY = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.REDCap > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_EXPORT_PATH_REDCAP = (By.ID, "configurationGroupDTOsREDCap0\\.configurationDTOs0\\.children0\\.value") + + CHECKBOX_EXPORT_REDCAP_VIA_REST = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.REDCap > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(3) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_URL_REDCAP = (By.ID, "configurationGroupDTOsREDCap0\\.configurationDTOs1\\.children0\\.value") + INPUT_TOKEN_REDCAP = (By.ID, "configurationGroupDTOsREDCap0\\.configurationDTOs1\\.children1\\.value") + + INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER = (By.ID, "configurationGroupDTOsencounter0\\.configurationDTOs0\\.value") + INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE = (By.ID, "configurationGroupDTOsencounter0\\.configurationDTOs1\\.value") + INPUT_TIME_DELETE_EMAIL_COMPLETED_ENCOUNTER = (By.ID, "configurationGroupDTOsencounter0\\.configurationDTOs2\\.value") + INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTERS = (By.ID, "configurationGroupDTOsencounter0\\.configurationDTOs3\\.value") + INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE = (By.ID, "configurationGroupDTOsencounter0\\.configurationDTOs4\\.value") + + CHECKBOX_UTILIZE_MAILER = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.mail > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(1) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + INPUT_MAIL_HOST = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children0\\.value") + INPUT_MAIL_PORT = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children1\\.value") + + CHECKBOX_ENABLE_TLS = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.mail > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(4) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + CHECKBOX_SMTP_AUTHENTICATION = (By.CSS_SELECTOR, "#configurationGroup\\.label\\.mail > fieldset:nth-child(2) > ul:nth-child(2) > li:nth-child(5) > div:nth-child(1) > div:nth-child(2) > label:nth-child(1) > input:nth-child(2)") + + INPUT_USERNAME_MAILER = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children4\\.value") + INPUT_PASSWORD_MAILER = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children5\\.value") + INPUT_SENDER_MAILER = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children6\\.value") + INPUT_EMAIL_ADDRESS_MAILER = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children7\\.value") + INPUT_PHONE_MAILER = (By.ID, "configurationGroupDTOsmail0\\.configurationDTOs0\\.children8\\.value") + + INPUT_MAIL_SUPPORT = (By.ID,"configurationGroupDTOssupport0\\.configurationDTOs0\\.value") + INPUT_PHONE_SUPPORT = (By.ID,"configurationGroupDTOssupport0\\.configurationDTOs1\\.value") + + BUTTON_SAVE_CONFIGURATION = (By.ID, "saveButton") + + IMAGE_ADDITIONAL_LOGO = (By.CSS_SELECTOR, "div.footer-bar.d-flex.align-items-center.justify-content-between > div.d-bp-flex.d-none.justify-content-end.d-flex") + + +class ConfigurationHelper: + + DEFAULT_IMAGE_UPLOAD_PATH = "test_images" + DEFAULT_IMAGE_FILE_NAME = "test_logo.png" + + DEFAUL_EXPORT_IMAGE_UPLOAD_PATH = "/var/lib/mopatImages" + DEFAULT_EXPORT_PATH = "/var/lib/mopatExport" + DEFAULT_EXPORT_PATH_ODM = os.path.join(DEFAULT_EXPORT_PATH, "ODM") + DEFAULT_EXPORT_PATH_FHIR = os.path.join(DEFAULT_EXPORT_PATH, "FHIR") + DEFAULT_EXPORT_PATH_REDCap = os.path.join(DEFAULT_EXPORT_PATH, "REDCap") + DEFAULT_PORT_HL7_PATIENT_LOOKUP = 1234 + + DEFAULT_MAIL_SUPPORT = "test@test.com" + + def __init__(self, driver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver, navigation_helper) + + def add_additional_logo(self, upload_path=DEFAULT_IMAGE_UPLOAD_PATH, file_name=DEFAULT_IMAGE_FILE_NAME): + try: + image_path = os.path.join(os.path.dirname(__file__), upload_path, file_name) + assert os.path.exists(image_path), f"Test image not found at path: {image_path}" + + self.driver.find_element(*ConfigurationSelectors.INPUT_ADDITIONAL_LOGO).send_keys(image_path) + except (NoSuchElementException, TimeoutException) as e: + print(f"An error occurred while adding additional logo: {e}") + + def save_configuration(self): + self.utils.click_element(ConfigurationSelectors.BUTTON_SAVE_CONFIGURATION) \ No newline at end of file diff --git a/selenium/helper/Encounter.py b/selenium/helper/Encounter.py new file mode 100644 index 00000000..e0071ef6 --- /dev/null +++ b/selenium/helper/Encounter.py @@ -0,0 +1,167 @@ +from enum import Enum + +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver import WebDriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + +from helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SearchBoxSelectors, SeleniumUtils, DropdownMethod + + +class EncounterSurveyLanguages(Enum): + DE_DE = "de_DE" + EN_GB = "en_GB" + ES_ES = "es_ES" + FR_FR = "fr_FR" + HI_IN = "hi_IN" + IT_IT = "it_IT" + NL_NL = "nl_NL" + NO_NO = "no_NO" + PL_PL = "pl_PL" + PT_PT = "pt_PT" + RU_RU = "ru_RU" + SV_SE = "sv_SE" + TR_TR = "tr_TR" + AR = "ar" + FA_IR = "fa_IR" + DARI = "dari" + KU = "ku" + +class EncounterScheduleType(Enum): + UNIQUELY = "UNIQUELY" + REPEATEDLY = "REPEATEDLY" + WEEKLY = "WEEKLY" + MONTHLY = "MONTHLY" + + +class EncounterSelectors: + BUTTON_ENCOUNTER_TABLE = (By.ID, "encounter-tablink") + BUTTON_ENCOUNTER_SCHEDULE_TABLE = (By.ID, "encounterScheduled-tablink") + BUTTON_EXECUTE_ENCOUNTER = (By.ID, "executeEncounter") + BUTTON_SCHEDULE_ENCOUNTER = (By.ID, "scheduleEncounter") + BUTTON_SAVE_SCHEDULED_ENCOUNTER = (By.ID, "saveButton") + + INPUT_SCHEDULE_CASE_NUMBER = (By.ID, "caseNumber") + INPUT_SCHEDULE_EMAIL = (By.ID, "email") + INPUT_DATE= (By.ID, "startDate") + INPUT_TIME_PERIOD = (By.ID, "repeatPeriod") + INPUT_END_DATE = (By.ID, "endDate") + INPUT_PERSONAL_TEXT= (By.ID, "personalText") + + SELECT_SCHEDULE_CLINIC = (By.ID, "activeClinicDTO") + SELECT_SCHEDULE_BUNDLE = (By.ID, "bundleDTO.id") + SELECT_SURVEY_TYPE = (By.ID, "encounterScheduledSerialType") + SELECT_LANGUAGE = (By.ID, "locale") + + TABLE_ALL_ENCOUNTERS = (By.ID, "encounterTable") + TABLE_SCHEDULED_ENCOUNTERS = (By.ID, "encounterScheduled") + TABLE_ACTION_COLUMN=(By.CSS_SELECTOR, "td.actionColumn") + + PAGINATION_ENCOUNTER_SCHEDULE_TABLE = (By.ID, "encounterScheduled_paginate") + PAGINATION_ENCOUNTER_TABLE = (By.ID, "encounterTable_paginate") + + SEARCH_ALL_ENCOUNTERS = (By.ID, "encounterTable_filter") + SEARCH_SCHEDULED_ENCOUNTERS = (By.ID, "encounterScheduled_filter") + +class EncounterHelper: + + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver) + self.navigation_helper = navigation_helper + + def schedule_encounter(self, case_number, clinic, bundle, email, survey_type, start_date, time_period_days=7, end_date=None, language=EncounterSurveyLanguages.DE_DE.value, message=""): + """ + :param case_number: case number of type string. + :param clinic: clinic name of type string. + :param bundle: bundle name of type string. + :param email: email of type string. + :param survey_type: survey type of type EncounterScheduleType enum. + :param start_date: A string representing the date to set (e.g., "2024-12-20"). + :param time_period_days: time period in days of type int. + :param end_date: A string representing the date to set (e.g., "2024-12-20"). + :param language: language of type EncounterSurveyLanguages enum. + :param message: message of type string. + """ + + try: + self.utils.fill_text_field(EncounterSelectors.INPUT_SCHEDULE_CASE_NUMBER, case_number) + self.utils.select_dropdown(EncounterSelectors.SELECT_SCHEDULE_CLINIC, clinic) + self.utils.select_dropdown(EncounterSelectors.SELECT_SCHEDULE_BUNDLE, bundle) + self.utils.fill_text_field(EncounterSelectors.INPUT_SCHEDULE_EMAIL, email) + self.driver.execute_script("document.getElementById('startDate').valueAsDate = new Date(arguments[0]);", start_date) + self.driver.execute_script("document.getElementById('startDate').dispatchEvent(new Event('blur'));") + + except Exception as e: + print("Error filling schedule encounter form elements ", e) + + try: + if survey_type == EncounterScheduleType.REPEATEDLY: + self.utils.select_dropdown(EncounterSelectors.SELECT_SURVEY_TYPE, "REPEATEDLY", method=DropdownMethod.VALUE) + self.utils.fill_text_field(EncounterSelectors.INPUT_TIME_PERIOD, time_period_days) + if end_date is not None: + self.driver.execute_script("document.getElementById('endDate').valueAsDate = new Date(arguments[0]);", end_date) + self.driver.execute_script("document.getElementById('endDate').dispatchEvent(new Event('blur'));") + elif survey_type == EncounterScheduleType.WEEKLY: + self.utils.select_dropdown(EncounterSelectors.SELECT_SURVEY_TYPE, "WEEKLY", method=DropdownMethod.VALUE) + if end_date is not None: + self.driver.execute_script("document.getElementById('endDate').valueAsDate = new Date(arguments[0]);", end_date) + self.driver.execute_script("document.getElementById('endDate').dispatchEvent(new Event('blur'));") + elif survey_type == EncounterScheduleType.MONTHLY: + self.utils.select_dropdown(EncounterSelectors.SELECT_SURVEY_TYPE, "MONTHLY", method=DropdownMethod.VALUE) + if end_date is not None: + self.driver.execute_script("document.getElementById('endDate').valueAsDate = new Date(arguments[0]);", end_date) + self.driver.execute_script("document.getElementById('endDate').dispatchEvent(new Event('blur'));") + else: + self.utils.select_dropdown(EncounterSelectors.SELECT_SURVEY_TYPE, "UNIQUELY", method=DropdownMethod.VALUE) + + except Exception as e: + print("Error filling schedule encounter form elements ", e) + + try: + self.utils.select_dropdown(EncounterSelectors.SELECT_LANGUAGE, language, method=DropdownMethod.VALUE) + self.utils.fill_text_field(EncounterSelectors.INPUT_PERSONAL_TEXT, message) + self.utils.click_element(EncounterSelectors.BUTTON_SAVE_SCHEDULED_ENCOUNTER) + + except Exception as e: + print("Error clicking schedule encounter button ", e) + + self.navigation_helper.navigate_to_manage_surveys() + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + + # Search for the bundle by name + self.utils.fill_text_field(SearchBoxSelectors.SCHEDULED_ENCOUNTER, case_number) + + scheduled_encounter_id=None + try: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.XPATH, f"//span[text()='{email}']")) + ) + span = self.driver.find_element(By.XPATH, f"//span[text()='{email}']") + + href = span.get_attribute("href") + scheduled_encounter_id = href.split("#")[1].split("_")[0] + except TimeoutException: + print(f"Could not find the span with email: {email}") + + return scheduled_encounter_id + + + def delete_scheduled_encounter(self, scheduled_encounter_id, case_number): + + self.navigation_helper.navigate_to_manage_surveys() + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + search_box_selector = SearchBoxSelectors.SCHEDULED_ENCOUNTER + button_id = f"removeScheduleEncounter_{scheduled_encounter_id}" + + + try: + # Search for the element using the appropriate search box + self.utils.fill_text_field(search_box_selector, case_number) + + # click the remove button + self.utils.click_element((By.ID, button_id)) + except TimeoutException: + print(f"Failed to delete scheduled encounter '{scheduled_encounter_id}'.") \ No newline at end of file diff --git a/selenium/helper/Language.py b/selenium/helper/Language.py new file mode 100644 index 00000000..c10f4edb --- /dev/null +++ b/selenium/helper/Language.py @@ -0,0 +1,23 @@ +from helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SeleniumUtils + +from selenium.common import TimeoutException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class LanguageSelectors: + LANGUAGE_DROPDOWN_BUTTON = (By.CSS_SELECTOR, "#languageDropdown > a") + LANGUAGE_DROPDOWN = (By.ID, "addLanguageDropdown") + +class LanguageHelper: + def __init__(self, driver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver, navigation_helper) + + def open_language_dropdown(self): + try: + self.utils.click_element(LanguageSelectors.LANGUAGE_DROPDOWN_BUTTON) + except Exception as e: + raise Exception(f"Failed to open language dropdown: {e}") + \ No newline at end of file diff --git a/selenium/helper/Navigation.py b/selenium/helper/Navigation.py new file mode 100644 index 00000000..5d2ffc57 --- /dev/null +++ b/selenium/helper/Navigation.py @@ -0,0 +1,152 @@ +from selenium.common import TimeoutException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +from helper.SeleniumUtils import SeleniumUtils + + +class NavigationBarSelectors: + class UserMenu: + BUTTON = (By.ID, "userDropdownLink") + MAIL_TO_ALL_LINK = (By.ID, "mailToAllLink") + MANAGE_USER_LINK = (By.ID, "managerUserLink") + MANAGE_INVITATIONS_LINK = (By.ID, "manageInvitationLink") + + class QuestionnaireMenu: + BUTTON = (By.ID, "questionnaireDropdownLink") + MANAGE_LINK = (By.ID, "manageQuestionnaireLink") + IMPORT_LINK = (By.ID, "importQuestionnaireLink") + + class BundleMenu: + LINK = (By.ID, "bundleLink") + + class ClinicMenu: + LINK = (By.ID, "clinicLink") + + class SurveysMenu: + BUTTON = (By.ID, "surveyDropdownLink") + MANAGE_SURVEY_LINK = (By.ID, "manageSurveyLink") + SCHEDULE_SURVEY_LINK = (By.ID, "scheduleSurveyLink") + EXECUTE_SURVEY_LINK = (By.ID, "executeSurveyLink") + CONFIGURATION_LINK = (By.ID, "configurationLink") + +class QuestionTableSelectors: + EDIT_QUESTION_LINK = lambda question_id: (By.XPATH, f'//a[@href="fill?id={question_id}"]') + EDIT_QUESTIONS_CONDITIONS_LINK = lambda question_id: (By.XPATH, f'//a[@href="/condition/listQuestionConditions?questionId={question_id}"]') + +class QuestionnaireTableSelectors: + FILTER_INPUT = (By.CSS_SELECTOR, "#questionnaireTable_filter input[type='search']") + FIRST_RESULT_LINK = (By.CSS_SELECTOR, "#questionnaireTable tbody tr td a") + EDIT_QUESTIONS_LINK = lambda questionnaire_id: (By.XPATH, f'//a[@href="/question/list?id={questionnaire_id}"]') + EDIT_SCORES_LINK = lambda questionnaire_id: (By.XPATH, f'//a[@href="/score/list?id={questionnaire_id}"]') + +class NavigationHelper: + def __init__(self, driver): + self.driver = driver + self.utils = SeleniumUtils(self.driver) + + def navigate_to_manage_questionnaires(self): + try: + self.utils.click_element(NavigationBarSelectors.QuestionnaireMenu.BUTTON) + self.utils.click_element(NavigationBarSelectors.QuestionnaireMenu.MANAGE_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Questionnaires': {e}") + + def navigate_to_questions_of_questionnaire(self, questionnaire_id, questionnaire_name): + self.navigate_to_manage_questionnaires() + self.utils.fill_text_field(QuestionnaireTableSelectors.FILTER_INPUT, questionnaire_name) + link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionnaireTableSelectors.EDIT_QUESTIONS_LINK(questionnaire_id))) + link.click() + + def navigate_to_scores_of_questionnaire(self, questionnaire_id, questionnaire_name): + self.navigate_to_manage_questionnaires() + self.utils.fill_text_field(QuestionnaireTableSelectors.FILTER_INPUT, questionnaire_name) + scores_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionnaireTableSelectors.EDIT_SCORES_LINK(questionnaire_id))) + scores_link.click() + + def navigate_to_manage_bundles(self): + try: + self.utils.click_element(NavigationBarSelectors.BundleMenu.LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Bundles': {e}") + + def navigate_to_manage_clinics(self): + try: + self.utils.click_element(NavigationBarSelectors.ClinicMenu.LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Clinics': {e}") + + def navigate_to_email_to_all_users(self): + try: + self.utils.click_element(NavigationBarSelectors.UserMenu.BUTTON) + self.utils.click_element(NavigationBarSelectors.UserMenu.MAIL_TO_ALL_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'E-Mail to All Users': {e}") + + def search_and_open_questionnaire(self, questionnaire_name): + try: + self.navigate_to_manage_questionnaires() + self.utils.fill_text_field(QuestionnaireTableSelectors.FILTER_INPUT, questionnaire_name) + + # Click the first result + first_result_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionnaireTableSelectors.FIRST_RESULT_LINK)) + first_result_link.click() + except TimeoutException: + raise Exception(f"Failed to search and open questionnaire '{questionnaire_name}'.") + + + def open_question(self, question_id): + question_link = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionTableSelectors.EDIT_QUESTION_LINK(question_id))) + question_link.click() + + def navigate_to_manage_surveys(self): + try: + self.utils.click_element(NavigationBarSelectors.SurveysMenu.BUTTON) + self.utils.click_element(NavigationBarSelectors.SurveysMenu.MANAGE_SURVEY_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Surveys': {e}") + + def navigate_to_schedule_survey(self): + try: + self.utils.click_element(NavigationBarSelectors.SurveysMenu.BUTTON) + self.utils.click_element(NavigationBarSelectors.SurveysMenu.SCHEDULE_SURVEY_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Schedule Survey': {e}") + + def navigate_to_execute_survey(self): + try: + self.utils.click_element(NavigationBarSelectors.SurveysMenu.BUTTON) + self.utils.click_element(NavigationBarSelectors.SurveysMenu.EXECUTE_SURVEY_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Execute Survey': {e}") + + def navigate_to_manager_user(self): + try: + self.utils.click_element(NavigationBarSelectors.UserMenu.BUTTON) + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(NavigationBarSelectors.UserMenu.MANAGE_USER_LINK) + ) + self.utils.click_element(NavigationBarSelectors.UserMenu.MANAGE_USER_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Users': {e}") + + def navigate_to_manage_invitations(self): + try: + self.utils.click_element(NavigationBarSelectors.UserMenu.BUTTON) + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(NavigationBarSelectors.UserMenu.MANAGE_INVITATIONS_LINK) + ) + self.utils.click_element(NavigationBarSelectors.UserMenu.MANAGE_INVITATIONS_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Manage Invitations': {e}") + + def navigate_to_configuration(self): + try: + self.utils.click_element(NavigationBarSelectors.SurveysMenu.CONFIGURATION_LINK) + except Exception as e: + raise Exception(f"Failed to navigate to 'Configuration': {e}") \ No newline at end of file diff --git a/selenium/helper/Question.py b/selenium/helper/Question.py new file mode 100644 index 00000000..37b1310c --- /dev/null +++ b/selenium/helper/Question.py @@ -0,0 +1,1525 @@ +import os +from datetime import datetime, timedelta +from enum import Enum + +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver 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.support.ui import Select + +from helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SeleniumUtils, DropdownMethod + + +class QuestionType(Enum): + BODY_PART = "BODY_PART" + BARCODE = "BARCODE" + NUMBER_INPUT = "NUMBER_INPUT" + INFO_TEXT = "INFO_TEXT" + MULTIPLE_CHOICE = "MULTIPLE_CHOICE" + SLIDER = "SLIDER" + NUMBER_CHECKBOX = "NUMBER_CHECKBOX" + NUMBER_CHECKBOX_TEXT = "NUMBER_CHECKBOX_TEXT" + DROP_DOWN = "DROP_DOWN" + FREE_TEXT = "FREE_TEXT" + DATE = "DATE" + IMAGE = "IMAGE" + + +class BodyPartSelectors: + # SVG selectors for body parts + BODY_PART_SVG_FRONT = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} #svg-FRONT") + BODY_PART_SVG_BACK = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} #svg-BACK") + # Individual body parts (front_or_back = 'front' or 'back') (left_or_right) + HEAD = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-head") + THROAT = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-throat") + CHEST = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-chest") + STOMACH = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-stomach") + HIPS = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-hips") + GENITALS = lambda id_selector, front_or_back: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-genitals") + SHOULDER = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}Shoulder") + ELBOW_JOINT = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}ElbowJoint") + LOWER_ARM = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}LowerArm") + HAND = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}Hand") + THIGH = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}Thigh") + KNEE = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}Knee") + LOWER_LEG = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}LowerLeg") + FOOT = lambda id_selector, front_or_back, left_or_right: (By.CSS_SELECTOR, f"#{id_selector} #bodyPart-{front_or_back}-{left_or_right}Foot") + +class QuestionSelectors: + NUMBER_INPUT_SECTION = (By.ID, "numberInput") + MULTIPLE_CHOICE_AND_DROP_DOWN_SECTION = (By.ID, "multipleChoice") + SLIDER_AND_NUMBER_CHECKBOX_SECTION = (By.ID, "slider") + NUMBER_CHECKBOX_TEXT_SECTION = (By.ID, "sliderFreeText") + DATE_SECTION = (By.ID, "date") + IMAGE_SECTION = (By.ID, "image") + BODY_PART_SECTION = (By.ID, "bodyPart") + + BUTTON_ADD_QUESTION = (By.ID, "addQuestion") + BUTTON_SAVE = (By.ID, "saveButton") + BUTTON_CANCEL = (By.ID, "cancelButton") + BUTTON_ADD_ANSWER = (By.ID, "addAnswerButton") + BUTTON_SELECT_ALL_BODY_PARTS = lambda FRONT_or_BACK: (By.ID, f"selectAllBodyPartsButton-{FRONT_or_BACK}") # "FRONT" or "BACK" + BUTTON_DESELECT_ALL_BODY_PARTS = lambda FRONT_or_BACK: (By.ID, f"deleteAllBodyPartsButton-{FRONT_or_BACK}") # "FRONT" or "BACK" + + CHECKBOX_IS_REQUIRED = (By.ID, 'isRequired1') + CHECKBOX_QUESTION_INITIAL_ACTIVATION = (By.ID, "isEnabled1") + CHECKBOX_SYMBOL_ABOVE_SLIDER = (By.ID, "showIcons") + CHECKBOX_DISPLAY_VALUE_ON_SLIDER = (By.ID, "showValueOnButton") + CHECKBOX_VERTICAL = lambda index : (By.NAME, f"answers[{index}].vertical") + CHECKBOX_FREE_TEXT = lambda id_selector, index: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[{index}].isOther']") + CHECKBOX_ANSWER_INITIAL_ACTIVATION = lambda id_selector, index: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[{index}].isEnabled']") + + DROPDOWN_ADD_LANGUAGE = (By.CSS_SELECTOR, "div#languageDropdown.dropdown") + DROPDOWN_TYPE_OF_IDENTIFICATION = (By.ID, "codedValueType") + DROPDOWN_QUESTION_TYPE = (By.ID, "questionTypeDropDown") + DROPDOWN_IMAGE_TYPE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} select[name='imageType']") + + ERROR_SLIDER_CONTAINER = (By.ID, "sliderErrors") + ERROR_QUESTION_TEXT = lambda language_code: (By.XPATH, f"//textarea[@id='localizedQuestionText{language_code}']/following-sibling::div[@style='color: red']") + ERROR_MIN_ANSWERS = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='minNumberAnswers']/following-sibling::div[@style='color: red']") + ERROR_MAX_ANSWERS = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='maxNumberAnswers']/following-sibling::div[@style='color: red']") + ERROR_IDENT_CODE = lambda index: (By.XPATH, f"//input[@id='answers{index}.codedValue']/following-sibling::div[@style='color: red']") + ERROR_MIN_VALUE = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='answers0.minValue']/following-sibling::div[@style='color: red']") + ERROR_MAX_VALUE = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='answers0.maxValue']/following-sibling::div[@style='color: red']") + ERROR_STEP_SIZE = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='answers0.stepsize']/following-sibling::div[@style='color: red']") + ERROR_START_DATE = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='answers0.startDate']/following-sibling::div[@style='color: red']") + ERROR_END_DATE = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//input[@id='answers0.endDate']/following-sibling::div[@style='color: red']") + ERROR_FILE_PATH = lambda id_selector: (By.XPATH, f"//div[@id='{id_selector}']//div[@style='color: red']") + ERROR_TEXTAREA_ANSWER = lambda id_selector, index, language_code: (By.XPATH, f"//div[@id='{id_selector}']//textarea[@name='answers[{index}].localizedLabel[{language_code}]']/following-sibling::div[@style='color: red']") + + INPUT_MIN_NUMBER_ANSWERS = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[id='minNumberAnswers']") + INPUT_MAX_NUMBER_ANSWERS = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[id='maxNumberAnswers']") + INPUT_MIN_VALUE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[0].minValue']") + INPUT_MAX_VALUE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[0].maxValue']") + INPUT_STEP_SIZE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[0].stepsize']") + INPUT_START_DATE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[0].startDate']") + INPUT_END_DATE = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[0].endDate']") + INPUT_IMAGE_UPLOAD = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} input[type='file'][name='answers[0].imageFile']") + INPUT_IDENTIFICATION = lambda id_selector, index: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[{index}].codedValue']") + INPUT_SCORE = lambda id_selector, index: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[{index}].value']") + INPUT_FREETEXT_LABEL = lambda id_selector, index, language_code: (By.CSS_SELECTOR, f"#{id_selector} input[name='answers[{index}].localizedFreetextLabel[{language_code}]']") + + INPUT_WYSIWYG_QUESTION_TEXT = lambda language_code: (By.XPATH, f'//*[@id="localizedQuestionTextCollapsableText_{language_code}"]/div/div[2]/div[2]') + INPUT_WYSIWYG_SLIDER_MIN = lambda language_code: (By.XPATH, f'//*[@id="localizedMinimumTextSliderCollapsableText_{language_code}"]/div/div[2]/div[2]') + INPUT_WYSIWYG_SLIDER_MAX = lambda language_code: (By.XPATH, f'//*[@id="localizedMaximumTextSliderCollapsableText_{language_code}"]/div/div[2]/div[2]') + INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MIN = lambda language_code: (By.XPATH, f'//*[@id="localizedMinimumTextNumberCheckboxCollapsableText_{language_code}"]/div/div[2]/div[2]') + INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MAX = lambda language_code: (By.XPATH, f'//*[@id="localizedMaximumTextNumberCheckboxCollapsableText_{language_code}"]/div/div[2]/div[2]') + + TEXTAREA_ANSWER_TEXT = lambda id_selector, index, language_code: (By.CSS_SELECTOR, f"#{id_selector} textarea[name='answers[{index}].localizedLabel[{language_code}]']") + + IMAGE_FILE_CONTAINER = lambda id_selector: (By.CSS_SELECTOR, f"#{id_selector} .form-group.imageFile") + MULTIPLE_CHOICE_ANSWER_PANELS = (By.CLASS_NAME, "multipleChoiceAnswerPanel") + DELETE_BUTTON_WITHIN_PANEL = (By.XPATH, ".//button[@id='deleteAnswerButton']") + TABLE_LAST_ROW = (By.XPATH, "//tbody/tr[last()]") + TABLE_ROWS = (By.CSS_SELECTOR, "tbody > tr:not(#emptyRow)") + TABLE_QUESTIONS = (By.ID, "questionTable") + ACTION_BUTTONS = (By.CSS_SELECTOR, "td.actionColumn > div.d-none.d-xl-block > a.link") + GRIP_SELECTOR = lambda item_id: (By.ID, f"grip-{item_id}") + +class QuestionHelper: + + # Default values for questions + DEFAULT_LANGUAGE_CODE = "de_DE" + DEFAULT_LABELS = ("Low", "High") + DEFAULT_MIN_VALUE = 0 + DEFAULT_MAX_VALUE = 10 + DEFAULT_OPTIONS = ["Option 1", "Option 2", "Option 3"] + DEFAULT_QUESTION_TEXT = "Default Question Text" + DEFAULT_STEP_SIZE = 1 + DEFAULT_MC_OPTIONS =["Option 1", "Option 2", "Option 3"] + DEFAULT_MC_MIN_VALUE = 0 + DEFAULT_MC_MAX_VALUE = len(DEFAULT_MC_OPTIONS) + DEFAULT_FREE_TEXT_LABEL = "Freetext Label" + DEFAULT_IMAGE_UPLOAD_PATH = "test_images" + DEFAULT_IMAGE_FILE_NAME = "test_upload_image.png" + DEFAULT_IMAGE_TYPE = 'FRONT' + + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver) + self.navigation_helper = navigation_helper + + def save_question(self): + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + return self.get_last_added_question_id() + + def cancel_question_editing(self): + self.utils.click_element(QuestionSelectors.BUTTON_CANCEL) + + def get_question_type(self, question_id): + """ + Retrieves the type of a question by opening its edit page and checking the selected option + in the question type dropdown. + + :param question_id: The ID of the question to retrieve the type for. + :return: The QuestionType of the question. + """ + try: + # Navigate to the question's edit page + self.navigation_helper.open_question(question_id) + + # Wait for the question type dropdown to be visible + question_type_dropdown = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(QuestionSelectors.DROPDOWN_QUESTION_TYPE) + ) + + # Retrieve the selected option + selected_option = question_type_dropdown.find_element(By.CSS_SELECTOR, "option[selected]") + value = selected_option.get_attribute('value') + + self.cancel_question_editing() + + # Convert the value to a QuestionType + return QuestionType(value) + except TimeoutException: + raise Exception(f"Timeout while retrieving question type for question ID {question_id}.") + except Exception as e: + raise Exception(f"Error while retrieving question type for question ID {question_id}: {e}") + + def add_question_by_type_default_value(self, question_type: QuestionType): + if question_type == QuestionType.INFO_TEXT: + return self.add_question_info_text() + elif question_type == QuestionType.MULTIPLE_CHOICE: + return self.add_question_multiple_choice() + elif question_type == QuestionType.SLIDER: + return self.add_question_slider_question() + elif question_type == QuestionType.NUMBER_CHECKBOX: + return self.add_question_number_checkbox() + elif question_type == QuestionType.NUMBER_CHECKBOX_TEXT: + return self.add_question_number_checkbox_text() + elif question_type == QuestionType.DATE: + return self.add_question_date_question() + elif question_type == QuestionType.DROP_DOWN: + return self.add_question_dropdown() + elif question_type == QuestionType.NUMBER_INPUT: + return self.add_question_number_question() + elif question_type == QuestionType.FREE_TEXT: + return self.add_question_freetext() + elif question_type == QuestionType.IMAGE: + return self.add_question_image() + elif question_type == QuestionType.BODY_PART: + return self.add_question_body_part() + elif question_type == QuestionType.BARCODE: + return self.add_question_barcode() + else: + raise ValueError(f"Unknown QuestionType: {question_type}") + + def add_question_info_text(self, language_code=None, is_required=False, question_text=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the info text question. + :return: A dictionary containing the question type and text for validation. + """ + question_text = self.initialize_question(QuestionType.INFO_TEXT, language_code, is_required, question_text) + + return {"type": QuestionType.INFO_TEXT, "text": question_text} + + def add_question_multiple_choice(self, language_code=None, is_required=False, question_text=None, options=None, min_answers=None, max_answers=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the multiple-choice question. + :param is_required: Indicates if the question is required (default: False). + :param options: A list of answer options for the question. + :param min_answers: The minimum number of answers required. + :param max_answers: The maximum number of answers allowed. + :return: A dictionary containing the question type, text, and options for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + options = options or self.DEFAULT_OPTIONS + min_answers = min_answers or self.DEFAULT_MC_MIN_VALUE + max_answers = max_answers or self.DEFAULT_MC_MAX_VALUE + id_selector = self.get_selector_for(QuestionType.MULTIPLE_CHOICE) + + question_text = self.initialize_question(QuestionType.MULTIPLE_CHOICE, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.MULTIPLE_CHOICE_AND_DROP_DOWN_SECTION) + + # Set min and max answers + self.set_min_max_answers(id_selector, min_answers, max_answers) + # Add options + for i, option in enumerate(options): + textarea_element = self.driver.find_element(*QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, i, language_code)) + self.driver.execute_script("arguments[0].value=arguments[1];", textarea_element, option) + input_ident_element = self.driver.find_element(*QuestionSelectors.INPUT_IDENTIFICATION(id_selector, i)) + self.driver.execute_script("arguments[0].value=arguments[1];", input_ident_element, option) + + # Add new answer field except for the last option + if i < len(options) - 1: + add_button = self.driver.find_element(*QuestionSelectors.BUTTON_ADD_ANSWER) + self.driver.execute_script("arguments[0].click();", add_button) + + return { + "type": QuestionType.MULTIPLE_CHOICE, + "text": question_text, + "options": options, + "min_answers": min_answers, + "max_answers": max_answers, + } + + def add_question_slider_question(self, language_code=None, is_required=False, question_text=None, min_value=None, max_value=None, step_size=None, labels=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the slider question. + :param is_required: Indicates if the question is required (default: False). + :param min_value: The minimum value for the slider. + :param max_value: The maximum value for the slider. + :param step_size: The step size for the slider. + :param labels: A tuple containing two labels (min label, max label) for the slider. + :return: A dictionary containing the question type, text, and slider configuration for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + min_value = min_value or self.DEFAULT_MIN_VALUE + max_value = max_value or self.DEFAULT_MAX_VALUE + step_size = step_size or self.DEFAULT_STEP_SIZE + labels = labels or self.DEFAULT_LABELS + + question_text = self.initialize_question(QuestionType.SLIDER, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.SLIDER_AND_NUMBER_CHECKBOX_SECTION) + + # Set slider configuration + self.set_min_max_step_inputs(min_value, max_value, step_size, QuestionType.SLIDER) + # Set labels for the slider + slider_text_min_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_SLIDER_MIN(language_code)) + self.utils.fill_editable_div(slider_text_min_div, labels[0]) + slider_text_max_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_SLIDER_MAX(language_code)) + self.utils.fill_editable_div(slider_text_max_div, labels[1]) + + return { + "type": QuestionType.SLIDER, + "text": question_text, + "min_value": min_value, + "max_value": max_value, + "step_size": step_size, + "labels": labels, + } + + def add_question_number_checkbox(self, language_code=None, is_required=False, question_text=None, min_value=None, max_value=None, step_size=None, labels=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the numeric checkbox question. + :param is_required: Indicates if the question is required (default: False). + :param min_value: The minimum value for the numeric checkboxes. + :param max_value: The maximum value for the numeric checkboxes. + :param step_size: The step size between numeric checkbox values. + :param labels: A tuple containing two labels (min label, max label) for the numeric checkboxes. + :return: A dictionary containing the question type, text, and numeric checkbox configuration for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + min_value = min_value if min_value is not None else self.DEFAULT_MIN_VALUE + max_value = max_value if max_value is not None else self.DEFAULT_MAX_VALUE + step_size = step_size if step_size is not None else self.DEFAULT_STEP_SIZE + labels = labels or self.DEFAULT_LABELS + + question_text = self.initialize_question(QuestionType.NUMBER_CHECKBOX, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.NUMBER_CHECKBOX_TEXT_SECTION) + + # Set numeric values + self.set_min_max_step_inputs(min_value, max_value, step_size, QuestionType.NUMBER_CHECKBOX) + # Set labels + nc_text_min_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_SLIDER_MIN(language_code)) + self.utils.fill_editable_div(nc_text_min_div, labels[0]) + nc_text_max_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_SLIDER_MAX(language_code)) + self.utils.fill_editable_div(nc_text_max_div, labels[1]) + + return { + "type": QuestionType.NUMBER_CHECKBOX, + "text": question_text, + "min_value": min_value, + "max_value": max_value, + "step_size": step_size, + "labels": labels, + } + + def add_question_number_checkbox_text(self, language_code=None, is_required=False, question_text=None, min_value=None, max_value=None, step_size=None, freetext_label=None, labels=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the numeric checkbox question. + :param min_value: The minimum value for the numeric checkboxes. + :param max_value: The maximum value for the numeric checkboxes. + :param step_size: The step size between numeric checkbox values. + :param freetext_label: Label for the freetext field. + :param labels: A tuple containing two labels (min label, max label) for the numeric checkboxes + freetext. + :return: A dictionary containing the question type and configuration for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + min_value = min_value or self.DEFAULT_MIN_VALUE + max_value = max_value or self.DEFAULT_MAX_VALUE + step_size = step_size or self.DEFAULT_STEP_SIZE + freetext_label = freetext_label or self.DEFAULT_FREE_TEXT_LABEL + labels = labels or self.DEFAULT_LABELS + id_selector = self.get_selector_for(QuestionType.NUMBER_CHECKBOX_TEXT) + + question_text = self.initialize_question(QuestionType.NUMBER_CHECKBOX_TEXT, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.NUMBER_CHECKBOX_TEXT_SECTION) + + # Set numeric values + self.set_min_max_step_inputs(min_value, max_value, step_size, QuestionType.NUMBER_CHECKBOX_TEXT) + # Set labels + ncf_text_min_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MIN(language_code)) + self.utils.fill_editable_div(ncf_text_min_div, labels[0]) + ncf_text_max_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MAX(language_code)) + self.utils.fill_editable_div(ncf_text_max_div, labels[1]) + # Set freetext label + self.driver.find_element(*QuestionSelectors.INPUT_FREETEXT_LABEL(id_selector, 0, language_code)).send_keys(freetext_label) + + return { + "type": QuestionType.NUMBER_CHECKBOX_TEXT, + "text": question_text, + "min_value": min_value, + "max_value": max_value, + "step_size": step_size, + "freetext_label": freetext_label, + } + + def add_question_date_question(self, language_code=None, is_required=False, question_text=None, start_date=None, end_date=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the date question. + :param start_date: The start date for the date range (default: current date). + :param end_date: The end date for the date range (default: 7 days from the current date). + :return: A dictionary containing the question type and text for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + start_date = start_date or datetime.now().strftime("%m/%d/%Y") + end_date = end_date or ((datetime.now() + timedelta(days=7)).strftime("%m/%d/%Y")) + id_selector = self.get_selector_for(QuestionType.DATE) + + question_text = self.initialize_question(QuestionType.DATE, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.DATE_SECTION) + + # Find start and end date inputs + start_date_input = self.driver.find_element(*QuestionSelectors.INPUT_START_DATE(id_selector)) + end_date_input = self.driver.find_element(*QuestionSelectors.INPUT_END_DATE(id_selector)) + # Clear and input start/end date + start_date_input.clear() + start_date_input.send_keys(start_date) + end_date_input.clear() + end_date_input.send_keys(end_date) + + return { + "type": QuestionType.DATE, + "text": question_text, + } + + def add_question_dropdown(self, language_code=None, is_required=False, question_text=None, options=None): + """ + :param is_required: Indicates if the question is required (default: False). + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the dropdown question. + :param options: A list of dropdown options. + :return: A dictionary containing the question type, text, and options for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + options = options or self.DEFAULT_OPTIONS + id_selector = self.get_selector_for(QuestionType.DROP_DOWN) + + question_text = self.initialize_question(QuestionType.DROP_DOWN, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.MULTIPLE_CHOICE_AND_DROP_DOWN_SECTION) + + # Add options + for i, option in enumerate(options): + textarea_element = self.driver.find_element(*QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, i, language_code)) + self.driver.execute_script("arguments[0].value=arguments[1];", textarea_element, option) + input_ident_element = self.driver.find_element(*QuestionSelectors.INPUT_IDENTIFICATION(id_selector, i)) + self.driver.execute_script("arguments[0].value=arguments[1];", input_ident_element, option) + + # Add new option field + if i < len(options) - 1: + add_button = self.driver.find_element(*QuestionSelectors.BUTTON_ADD_ANSWER) + self.driver.execute_script("arguments[0].click();", add_button) + + return { + "type": QuestionType.DROP_DOWN, + "text": question_text, + "options": options, + } + + def add_question_number_question(self, language_code=None, is_required=False, question_text=None, min_value=None, max_value=None, step_size=None): + """ + :param is_required: Indicates if the question is required (default: False). + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the numeric question. + :param min_value: The minimum value for the numeric input. + :param max_value: The maximum value for the numeric input. + :param step_size: The step size for the numeric input. + :return: A dictionary containing the question type and configuration for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + min_value = min_value or self.DEFAULT_MIN_VALUE + max_value = max_value or self.DEFAULT_MAX_VALUE + step_size = step_size or self.DEFAULT_STEP_SIZE + + question_text = self.initialize_question(QuestionType.NUMBER_INPUT, language_code, is_required, question_text) + + self.ensure_section_visible(QuestionSelectors.NUMBER_INPUT_SECTION) + + # Set numeric values + self.set_min_max_step_inputs(min_value, max_value, step_size, QuestionType.NUMBER_INPUT) + + return { + "type": QuestionType.NUMBER_INPUT, + "text": question_text, + "min_value": min_value, + "max_value": max_value, + "step_size": step_size, + } + + def add_question_freetext(self, language_code=None, is_required=False, question_text=None): + """ + :param is_required: Indicates if the question is required (default: False). + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param question_text: The text content for the freetext question. + :return: A dictionary containing the question type and text for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + + question_text = self.initialize_question(QuestionType.FREE_TEXT, language_code, is_required, question_text) + + return { + "type": QuestionType.FREE_TEXT, + "text": question_text, + } + + def add_question_image(self, language_code=None, is_required=False, question_text=None, upload_path=None, file_name=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the image question. + :param upload_path: The relative path to the image directory (default: self.DEFAULT_IMAGE_UPLOAD_PATH). + :param file_name: The name of the image file to upload (default: self.DEFAULT_IMAGE_FILE_NAME). + :return: A dictionary containing the question type and text for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + upload_path = upload_path or self.DEFAULT_IMAGE_UPLOAD_PATH + file_name = file_name or self.DEFAULT_IMAGE_FILE_NAME + id_selector = self.get_selector_for(QuestionType.IMAGE) + + question_text = self.initialize_question(QuestionType.IMAGE, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.IMAGE_SECTION) + + image_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_IMAGE_UPLOAD(id_selector))) + + image_path = os.path.join(os.path.dirname(__file__), upload_path, file_name) + assert os.path.exists(image_path), f"Test image not found at path: {image_path}" + image_input.send_keys(image_path) + + return { + "type": QuestionType.IMAGE, + "text": question_text, + } + + def add_question_body_part(self, language_code=None, is_required=False, question_text=None, image_type=None, body_part_selectors=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the body part question. + :param image_type: The type of image to display (default: self.DEFAULT_IMAGE_TYPE). + :param body_part_selectors: A set of selectors for the body parts to be selected (default: HEAD(id_selector, "front")). + :return: A dictionary containing the question type, text, and selected body parts for validation. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + image_type = image_type or self.DEFAULT_IMAGE_TYPE + id_selector = self.get_selector_for(QuestionType.BODY_PART) + body_part_selectors = body_part_selectors or {BodyPartSelectors.HEAD(id_selector, "front")} + + question_text = self.initialize_question(QuestionType.BODY_PART, language_code, is_required, question_text) + + # Ensure the number input section is expanded + self.ensure_section_visible(QuestionSelectors.BODY_PART_SECTION) + + self.utils.scroll_to_bottom() + self.utils.select_dropdown(QuestionSelectors.DROPDOWN_IMAGE_TYPE(id_selector), image_type, DropdownMethod.VALUE) + self.set_min_max_answers(id_selector, 0, 1) + selected_body_parts = self.select_body_parts(body_part_selectors, deselect_first=True) + + return { + "type": QuestionType.BODY_PART, + "text": question_text, + "selected_body_parts": selected_body_parts, + } + + def add_question_barcode(self, language_code=None, is_required=False, question_text=None): + """ + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the body part question. + :return: A dictionary containing the question type, text. + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + + question_text = self.initialize_question(QuestionType.BARCODE, language_code, is_required, question_text) + + return { + "type": QuestionType.BARCODE, + "text": question_text + } + + def initialize_question(self, question_type, language_code=None, is_required=False, question_text=None): + """ + :param question_type: The type of the question (e.g., QuestionType.MULTIPLE_CHOICE). + :param language_code: The language code for localized labels (default: self.DEFAULT_LANGUAGE_CODE). + :param is_required: Indicates if the question is required (default: False). + :param question_text: The text content for the question. + :return: A dictionary containing common question details. + """ + question_text = question_text or f"{self.DEFAULT_QUESTION_TEXT} - {question_type.value}" + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + + # Wait for the dropdown to be visible and select the question type + WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionSelectors.DROPDOWN_QUESTION_TYPE)) + + + self.utils.select_dropdown(QuestionSelectors.DROPDOWN_QUESTION_TYPE, question_type.value, DropdownMethod.VALUE) + + # Wait until the page is fully loaded + WebDriverWait(self.driver, 30).until(lambda d: d.execute_script("return document.readyState") == "complete") + + # Fill the editable div for the question text (info text uses the same text field) + question_div = self.driver.find_element(*QuestionSelectors.INPUT_WYSIWYG_QUESTION_TEXT(language_code)) + self.utils.fill_editable_div(question_div, question_text) + + # Set required checkbox if needed + if is_required: + self.utils.toggle_checkbox(QuestionSelectors.CHECKBOX_IS_REQUIRED) + + return question_text + + def ensure_section_visible(self, section_selector): + """ + :param section_selector: The selector for the section container. + """ + # Wait for the page to fully load + WebDriverWait(self.driver, 30).until(lambda driver: driver.execute_script("return document.readyState") == "complete") + + # Locate the section container + section = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + section_selector)) + + # Check if the section is hidden + is_hidden = self.driver.execute_script( + "return arguments[0].querySelector('.form-group').style.display === 'none';", section + ) + + if is_hidden: + # Click the legend to expand the section + legend = section.find_element(By.TAG_NAME, "legend") + legend.click() + + # Wait until the section becomes visible + WebDriverWait(self.driver, 10).until( + lambda d: self.driver.execute_script( + "return arguments[0].querySelector('.form-group').style.display !== 'none';", section + ) + ) + + @staticmethod + def get_selector_for(question_type: QuestionType): + if question_type == QuestionType.BODY_PART: + return QuestionSelectors.BODY_PART_SECTION[1] + elif question_type == QuestionType.DATE: + return QuestionSelectors.DATE_SECTION[1] + elif question_type == QuestionType.IMAGE: + return QuestionSelectors.IMAGE_SECTION[1] + elif question_type == QuestionType.MULTIPLE_CHOICE or question_type == QuestionType.DROP_DOWN: + return QuestionSelectors.MULTIPLE_CHOICE_AND_DROP_DOWN_SECTION[1] + elif question_type == QuestionType.NUMBER_INPUT: + return QuestionSelectors.NUMBER_INPUT_SECTION[1] + elif question_type == QuestionType.SLIDER or question_type == QuestionType.NUMBER_CHECKBOX: + return QuestionSelectors.SLIDER_AND_NUMBER_CHECKBOX_SECTION[1] + elif question_type == QuestionType.NUMBER_CHECKBOX_TEXT: + return QuestionSelectors.NUMBER_CHECKBOX_TEXT_SECTION[1] + + def get_last_added_question_id(self): + """ + :return: ID of the question as a string. + """ + # Wait until the table rows are loaded + WebDriverWait(self.driver, 30).until(EC.presence_of_element_located( + QuestionSelectors.TABLE_LAST_ROW)) + + # Find the last row in the table + last_row = self.driver.find_element(*QuestionSelectors.TABLE_LAST_ROW) + + # Extract the ID from the `id` attribute of the last row + question_id = last_row.get_attribute("id") + + # Ensure the ID is found + if not question_id: + raise Exception("The ID of the last question could not be found.") + + return question_id + + def clear_min_max_step_inputs(self, question_type : QuestionType): + id_selector = self.get_selector_for(question_type) + + min_value = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MIN_VALUE(id_selector))) + + max_value = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MAX_VALUE(id_selector))) + + step_size = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_STEP_SIZE(id_selector))) + + min_value.clear() + max_value.clear() + step_size.clear() + + def set_min_max_step_inputs(self, min_value, max_value, step_size, question_type: QuestionType): + id_selector = self.get_selector_for(question_type) + + self.clear_min_max_step_inputs(question_type) + + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MIN_VALUE(id_selector))).send_keys(min_value) + + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MAX_VALUE(id_selector))).send_keys(max_value) + + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_STEP_SIZE(id_selector))).send_keys(step_size) + + def set_min_max_answers(self, id_selector, min_value, max_value): + input_min_answers = WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MIN_NUMBER_ANSWERS(id_selector))) + + input_max_answers = WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionSelectors.INPUT_MAX_NUMBER_ANSWERS(id_selector))) + + input_min_answers.clear() + input_min_answers.send_keys(min_value) + input_max_answers.clear() + input_max_answers.send_keys(max_value) + + def select_body_parts(self, body_part_selectors, deselect_first=False): + """ + :param body_part_selectors: A list of selectors for the body parts to be selected. + :param deselect_first: Indicates if all body parts should be deselected before selection (default: False). + :return: A list of successfully selected body part selectors. + """ + if deselect_first: + self.deselect_all_body_parts() + + successfully_selected = [] + + for body_part_selector in body_part_selectors: + try: + # Wait for the page to fully load + WebDriverWait(self.driver, 30).until( + lambda driver: driver.execute_script("return document.readyState") == "complete") + + WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + body_part_selector)) + + # Click the element to select + self.utils.click_element(body_part_selector) + + # Verify if the body part is selected + body_part_element = self.driver.find_element(*body_part_selector) + if "shape-selected" in body_part_element.get_attribute("class"): + successfully_selected.append(body_part_selector) + except Exception as e: + print(f"Failed to select body part with selector {body_part_selector}: {e}") + + return successfully_selected + + def deselect_all_body_parts(self): + id_selector = self.get_selector_for(QuestionType.BODY_PART) + + image_type_dropdown = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.DROPDOWN_IMAGE_TYPE(id_selector))) + + selected_image_type = Select(image_type_dropdown).first_selected_option.get_attribute("value") + + # Perform deselection based on the selected image type + if selected_image_type == "FRONT": + parts_deselect = QuestionSelectors.BUTTON_DESELECT_ALL_BODY_PARTS("FRONT") + self.utils.click_element(parts_deselect) + elif selected_image_type == "BACK": + parts_deselect = QuestionSelectors.BUTTON_DESELECT_ALL_BODY_PARTS("BACK") + self.utils.click_element(parts_deselect) + elif selected_image_type == "FRONT_BACK": + parts_deselect_front = QuestionSelectors.BUTTON_DESELECT_ALL_BODY_PARTS("FRONT") + parts_deselect_back = QuestionSelectors.BUTTON_DESELECT_ALL_BODY_PARTS("BACK") + self.utils.click_element(parts_deselect_front) + self.utils.click_element(parts_deselect_back) + else: + raise ValueError(f"Unexpected image type: {selected_image_type}") + + +class QuestionAssertHelper(QuestionHelper): + + def assert_question_fill_page(self): + language_code = self.DEFAULT_LANGUAGE_CODE + id_selector = self.get_selector_for(QuestionType.MULTIPLE_CHOICE) + try: + # Select one of the question types + WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionSelectors.DROPDOWN_QUESTION_TYPE)) + self.utils.select_dropdown(QuestionSelectors.DROPDOWN_QUESTION_TYPE, QuestionType.MULTIPLE_CHOICE.value, DropdownMethod.VALUE) + + # Validate dropdown to add language + language_dropdown = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.DROPDOWN_ADD_LANGUAGE)) + assert language_dropdown.is_displayed(), "Dropdown to add language is not displayed." + + # Validate select for question type + question_type_select = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.DROPDOWN_QUESTION_TYPE)) + assert question_type_select.is_displayed(), "Dropdown for question type is not displayed." + + # Validate WYSIWYG editor for the question text + wysiwyg_question_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_WYSIWYG_QUESTION_TEXT(language_code))) + assert wysiwyg_question_text.is_displayed(), "WYSIWYG editor for question text is not displayed." + + # Validate checkboxes for mandatory and initial activation + mandatory_checkbox = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.CHECKBOX_IS_REQUIRED)) + assert mandatory_checkbox.is_displayed(), "Checkbox to make question mandatory is not displayed." + + initial_activation_checkbox = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.CHECKBOX_ANSWER_INITIAL_ACTIVATION(id_selector, 0))) + assert initial_activation_checkbox.is_displayed(), "Checkbox to activate question initially is not displayed." + + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + # Error: Localized question text missing + error_message_element = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.ERROR_QUESTION_TEXT(self.DEFAULT_LANGUAGE_CODE))) + error_message = error_message_element.text.strip() + + expected_error = "Die Frage benötigt einen lokalisierten Fragetext" + assert error_message == expected_error, f"Unexpected error message: '{error_message}'" + + self.cancel_question_editing() + except TimeoutException: + raise AssertionError("Timed out waiting for elements on the question fill page.") + except AssertionError as e: + raise e + + def assert_question_by_type(self, question_type: QuestionType): + WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located(QuestionSelectors.DROPDOWN_QUESTION_TYPE)) + Select(self.driver.find_element(*QuestionSelectors.DROPDOWN_QUESTION_TYPE)).select_by_value(question_type.value) + # Wait for the page to fully load + WebDriverWait(self.driver, 30).until(lambda driver: driver.execute_script("return document.readyState") == "complete") + + if question_type == QuestionType.INFO_TEXT: + self.assert_fields_create_info_text_question() + elif question_type == QuestionType.MULTIPLE_CHOICE: + self.assert_fields_create_multiple_choice_question() + self.assert_validation_errors_multiple_choice() + elif question_type == QuestionType.SLIDER: + self.assert_fields_create_slider_question() + self.assert_validation_errors_sq_nc(question_type = QuestionType.SLIDER) + elif question_type == QuestionType.NUMBER_CHECKBOX: + self.assert_fields_numeric_checkbox_question() + self.assert_validation_errors_sq_nc(question_type = QuestionType.NUMBER_CHECKBOX) + elif question_type == QuestionType.NUMBER_CHECKBOX_TEXT: + self.assert_fields_numeric_checkbox_freetext_question() + self.assert_validation_errors_ncf() + elif question_type == QuestionType.DATE: + self.assert_fields_create_date_question() + self.assert_validation_errors_dq() + elif question_type == QuestionType.DROP_DOWN: + self.assert_fields_create_dropdown_question() + self.assert_validation_errors_dd() + elif question_type == QuestionType.NUMBER_INPUT: + self.assert_fields_create_number_question() + self.assert_nq_validation_errors_de() + elif question_type == QuestionType.FREE_TEXT: + self.assert_freetext_question() + elif question_type == QuestionType.IMAGE: + self.assert_fields_create_image_question() + self.assert_validation_errors_iq() + elif question_type == QuestionType.BODY_PART: + self.assert_fields_create_body_part_question() + self.assert_validation_errors_bp() + elif question_type == QuestionType.BARCODE: + self.assert_fields_create_barcode_question() + else: + raise ValueError(f"Unknown QuestionType: {question_type}") + + question_info = self.add_question_by_type_default_value(question_type) + question_id = self.save_question() + question_info['id'] = question_id + return question_info + + def assert_fields_create_multiple_choice_question(self): + self.assert_mc_dd_common_validations(QuestionType.MULTIPLE_CHOICE) + + def assert_fields_create_slider_question(self): + self.assert_sq_nc_common_validations(QuestionType.SLIDER) + + checkbox_vertical = self.driver.find_element(*QuestionSelectors.CHECKBOX_VERTICAL(0)) + assert not checkbox_vertical.is_enabled(), f"Checkbox for 'vertical' should be disabled but is enabled." + + symbols_checkbox = self.driver.find_element(*QuestionSelectors.CHECKBOX_SYMBOL_ABOVE_SLIDER) + value_checkbox = self.driver.find_element(*QuestionSelectors.CHECKBOX_DISPLAY_VALUE_ON_SLIDER) + assert symbols_checkbox.is_displayed(), "Checkbox for enable/disable symbol above slider is not displayed." + assert value_checkbox.is_displayed(), "Checkbox for enable/disable value on slider is not displayed." + + def assert_freetext_question(self): + try: + allowed_ids = { + QuestionSelectors.DROPDOWN_QUESTION_TYPE[1], + QuestionSelectors.CHECKBOX_IS_REQUIRED[1], + QuestionSelectors.CHECKBOX_QUESTION_INITIAL_ACTIVATION[1], + QuestionSelectors.BUTTON_SAVE[1], + } + self.assert_question_inputs(allowed_ids) + except AssertionError as e: + raise e + + def assert_fields_create_info_text_question(self): + try: + allowed_ids = { + QuestionSelectors.DROPDOWN_QUESTION_TYPE[1], + QuestionSelectors.CHECKBOX_IS_REQUIRED[1], + QuestionSelectors.CHECKBOX_QUESTION_INITIAL_ACTIVATION[1], + QuestionSelectors.BUTTON_SAVE[1], + } + self.assert_question_inputs(allowed_ids) + except AssertionError as e: + raise e + + def assert_fields_create_barcode_question(self): + try: + allowed_ids = { + QuestionSelectors.DROPDOWN_QUESTION_TYPE[1], + QuestionSelectors.CHECKBOX_IS_REQUIRED[1], + QuestionSelectors.CHECKBOX_QUESTION_INITIAL_ACTIVATION[1], + QuestionSelectors.BUTTON_SAVE[1], + } + self.assert_question_inputs(allowed_ids) + except AssertionError as e: + raise e + + def assert_fields_numeric_checkbox_question(self): + self.assert_sq_nc_common_validations(QuestionType.NUMBER_CHECKBOX) + checkbox_vertical = self.driver.find_element(*QuestionSelectors.CHECKBOX_VERTICAL(0)) + assert checkbox_vertical.is_enabled(), f"Checkbox for 'vertical' should be enabled but is disabled." + + def assert_fields_numeric_checkbox_freetext_question(self): + try: + language_code = self.DEFAULT_LANGUAGE_CODE + id_selector = self.get_selector_for(QuestionType.NUMBER_CHECKBOX_TEXT) + + min_value = self.driver.find_element(*QuestionSelectors.INPUT_MIN_VALUE(id_selector)) + max_value = self.driver.find_element(*QuestionSelectors.INPUT_MAX_VALUE(id_selector)) + step_size = self.driver.find_element(*QuestionSelectors.INPUT_STEP_SIZE(id_selector)) + assert min_value.is_displayed(), "Input for minimum value is not displayed." + assert max_value.is_displayed(), "Input for maximum value is not displayed." + assert step_size.is_displayed(), "Input for step size is not displayed." + + freetext_label = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_FREETEXT_LABEL(id_selector, 0, language_code))) + assert freetext_label.is_displayed(), "Freetext input field is not displayed." + + wysiwyg_min_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MIN(language_code))) + assert wysiwyg_min_text.is_displayed(), "WYSIWYG editor for text at minimum position is not displayed." + + wysiwyg_max_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_WYSIWYG_NUMERIC_CHECKBOX_FREETEXT_MAX(language_code))) + assert wysiwyg_max_text.is_displayed(), "WYSIWYG editor for text at maximum position is not displayed." + except AssertionError as e: + raise e + + def assert_fields_create_dropdown_question(self): + id_selector= self.get_selector_for(QuestionType.DROP_DOWN) + + self.assert_mc_dd_common_validations(QuestionType.DROP_DOWN) + + # If min and max should be readonly + input_min_answers = self.driver.find_element(*QuestionSelectors.INPUT_MIN_NUMBER_ANSWERS(id_selector)) + input_max_answers = self.driver.find_element(*QuestionSelectors.INPUT_MAX_NUMBER_ANSWERS(id_selector)) + assert input_min_answers.get_attribute("readonly"), "Min answers input is not readonly." + assert input_max_answers.get_attribute("readonly"), "Max answers input is not readonly." + assert input_min_answers.get_attribute("value") == "1", "Min answers input does not have the expected value '1'." + assert input_max_answers.get_attribute("value") == "1", "Max answers input does not have the expected value '1'." + + def assert_fields_create_number_question(self): + question_type = QuestionType.NUMBER_INPUT + id_selector = self.get_selector_for(question_type) + + min_value = self.driver.find_element(*QuestionSelectors.INPUT_MIN_VALUE(id_selector)) + max_value = self.driver.find_element(*QuestionSelectors.INPUT_MAX_VALUE(id_selector)) + step_size = self.driver.find_element(*QuestionSelectors.INPUT_STEP_SIZE(id_selector)) + assert min_value.is_displayed(), "Input for minimum value is not displayed." + assert max_value.is_displayed(), "Input for maximum value is not displayed." + assert step_size.is_displayed(), "Input for step size is not displayed." + + def assert_fields_create_date_question(self): + id_selector = self.get_selector_for(QuestionType.DATE) + + start_date = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_START_DATE(id_selector))) + assert start_date.is_displayed(), "Start date input field is not displayed." + + end_date = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_END_DATE(id_selector))) + assert end_date.is_displayed(), "End date input field is not displayed." + + def assert_fields_create_image_question(self): + id_selector = self.get_selector_for(QuestionType.IMAGE) + + # Locate the container for the image upload + container_locator = QuestionSelectors.IMAGE_FILE_CONTAINER(id_selector) + container = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + container_locator)) + + # Check if the container is displayed + assert container.is_displayed(), "Image upload container is not visible." + + image_input = container.find_element(*QuestionSelectors.INPUT_IMAGE_UPLOAD(id_selector)) + + # Validate attributes or further behaviors if necessary + accepted_file_types = image_input.get_attribute("accept") + assert accepted_file_types == ".png,.jpeg,.jpg", f"Unexpected accepted file types: {accepted_file_types}" + + def assert_fields_create_body_part_question(self): + id_selector = self.get_selector_for(QuestionType.BODY_PART) + + # Validate Select for the type of image + image_type_dropdown = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.DROPDOWN_IMAGE_TYPE(id_selector))) + assert image_type_dropdown.is_displayed(), "Dropdown for selecting image type is not displayed." + + # Test: Changing the image type updates the graphic + self.utils.select_dropdown(QuestionSelectors.DROPDOWN_IMAGE_TYPE(id_selector), "BACK", DropdownMethod.VALUE) + back_svg = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + BodyPartSelectors.BODY_PART_SVG_BACK(id_selector))) + assert back_svg.is_displayed(), "SVG for 'BACK' view is not displayed." + + self.utils.select_dropdown(QuestionSelectors.DROPDOWN_IMAGE_TYPE(id_selector), "FRONT", DropdownMethod.VALUE) + front_svg = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + BodyPartSelectors.BODY_PART_SVG_FRONT(id_selector))) + assert front_svg.is_displayed(), "SVG for 'FRONT' view is not displayed." + + # Validate min and max answers inputs + input_min_answers = self.driver.find_element(*QuestionSelectors.INPUT_MIN_NUMBER_ANSWERS(id_selector)) + input_max_answers = self.driver.find_element(*QuestionSelectors.INPUT_MAX_NUMBER_ANSWERS(id_selector)) + assert input_min_answers.is_displayed(), "Input for minimum answers is not displayed." + assert input_max_answers.is_displayed(), "Input for maximum answers is not displayed." + + # Validate interactable body part graphics + head = BodyPartSelectors.HEAD(id_selector, "front") + front_body_part = front_svg.find_element(*head) + assert front_body_part.is_displayed(), "Body part (head) is not displayed in the front SVG." + + + self.utils.scroll_to_bottom() + self.select_body_parts({head}) # Select the body part + WebDriverWait(self.driver, 10).until( + lambda driver: "shape-selected" in front_body_part.get_attribute("class"), + "Body part (head) was not selected." + ) + + # self.utils.click_element(front_body_part) # Deselect the body part + self.select_body_parts({head}) # Deselect the body part + WebDriverWait(self.driver, 10).until( + lambda driver: "shape-selected" not in front_body_part.get_attribute("class"), + "Body part (head) was not deselected." + ) + + # Validate select/deselect all buttons + parts_select = QuestionSelectors.BUTTON_SELECT_ALL_BODY_PARTS("FRONT") + select_all_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + parts_select)) + + parts_deselect = QuestionSelectors.BUTTON_DESELECT_ALL_BODY_PARTS("FRONT") + deselect_all_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + parts_deselect)) + + assert select_all_button.is_displayed(), "Button to select all body parts is not displayed." + assert deselect_all_button.is_displayed(), "Button to deselect all body parts is not displayed." + + self.utils.click_element(parts_select) + selected_parts = front_svg.find_elements(By.CLASS_NAME, "shape-selected") + assert len(selected_parts) > 0, "No body parts were selected after clicking 'Select All'." + + self.utils.click_element(parts_deselect) + selected_parts = front_svg.find_elements(By.CLASS_NAME, "shape-selected") + assert len(selected_parts) == 0, "Body parts were not deselected after clicking 'Deselect All'." + + def assert_nq_validation_errors_de(self): + question_type = QuestionType.NUMBER_INPUT + id_selector = self.get_selector_for(question_type) + + # Set min > max and validate the corresponding error + self.set_min_max_step_inputs(min_value="10", max_value="5", step_size="1", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_MIN_VALUE(id_selector), + "Das Minimum der Zahleneingabe war gleich oder größer als das Maximum" + ) + + # Set invalid step size (greater than max-min difference) + self.set_min_max_step_inputs(min_value="1.0", max_value="4.0", step_size="10", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_STEP_SIZE(id_selector), + "Die Schrittgröße der Zahleneingabe war größer als der Abstand zwischen Minimum und Maximum\nDer Abstand zwischen Minimum und Maximum ist nicht restlos durch die Schrittgröße teilbar" + ) + + def assert_validation_errors_dd(self): + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + + id_selector = self.get_selector_for(QuestionType.DROP_DOWN) + language_code = self.DEFAULT_LANGUAGE_CODE + + # Error: Answer text missing + self.validate_error_message( + QuestionSelectors.ERROR_TEXTAREA_ANSWER(id_selector, 0, language_code), + "Die Auswahlantwort benötigt einen lokalisierten Text" + ) + + # Error: Identification code missing + self.validate_error_message( + QuestionSelectors.ERROR_IDENT_CODE(0), + "Der Identifikationscode darf nicht leer sein" + ) + + def assert_validation_errors_multiple_choice(self): + id_selector = self.get_selector_for(QuestionType.MULTIPLE_CHOICE) + + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + + # Error: Min and Max answers missing + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Minimale Anzahl von zu beantwortenden Antworten muss vergeben werden" + ) + self.validate_error_message( + QuestionSelectors.ERROR_MAX_ANSWERS(id_selector), + "Maximale Anzahl von zu beantwortenden Antworten muss vergeben werden" + ) + + # Error: Identification code missing + self.validate_error_message( + QuestionSelectors.ERROR_IDENT_CODE(0), + "Der Identifikationscode darf nicht leer sein" + ) + + # Max number greater than existing answers + self.set_min_max_answers(id_selector, "1000", "1000") + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_MAX_ANSWERS(id_selector), + "Maximale Anzahl von zu beantwortenden Antworten ist größer als die Anzahl der vorhandenen Antworten" + ) + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Minimale Anzahl von zu beantwortenden Antworten ist größer als die Anzahl der vorhandenen Antworten" + ) + + # Add answer and check error: Min greater than Max + self.utils.scroll_and_click(QuestionSelectors.BUTTON_ADD_ANSWER) + + self.set_min_max_answers(id_selector, "2", "1") + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Minimale Anzahl von zu beantwortenden Antworten darf höchstens so groß wie die maximale Anzahl sein" + ) + + # Delete answer + panels = self.driver.find_elements(*QuestionSelectors.MULTIPLE_CHOICE_ANSWER_PANELS) + second_panel = panels[1] + delete_button = second_panel.find_element(*QuestionSelectors.DELETE_BUTTON_WITHIN_PANEL) + self.driver.execute_script("arguments[0].click();", delete_button) + + def assert_validation_errors_ncf(self): + question_type = QuestionType.NUMBER_CHECKBOX_TEXT + id_selector = self.get_selector_for(question_type) + + # Clear and validate initial errors + self.clear_min_max_step_inputs(question_type) + + # Click save and validate errors for missing values + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_MIN_VALUE(id_selector), + "Die Frage benötigt einen Minimalwert" + ) + self.validate_error_message( + QuestionSelectors.ERROR_MAX_VALUE(id_selector), + "Die Frage benötigt einen Maximalwert" + ) + self.validate_error_message( + QuestionSelectors.ERROR_STEP_SIZE(id_selector), + "Die Schrittweite der Frage entspricht nicht dem geforderten Format." + ) + + # Set min > max and validate the corresponding error + self.set_min_max_step_inputs(min_value="10", max_value="5", step_size="1", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_MIN_VALUE(id_selector), + "Das Minimum der Frage war gleich oder größer als das Maximum" + ) + + # Set invalid step size (greater than max-min difference) + self.set_min_max_step_inputs(min_value="1.0", max_value="4.0", step_size="10", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_error_message( + QuestionSelectors.ERROR_STEP_SIZE(id_selector), + "Die Schrittweite der Frage war größer als der Abstand zwischen Minimum und Maximum\nDer Abstand zwischen Minimum und Maximum ist nicht restlos durch die Schrittweite teilbar" + ) + + def assert_validation_errors_dq(self): + id_selector = self.get_selector_for(QuestionType.DATE) + + # Find start and end date inputs + start_date = self.driver.find_element(*QuestionSelectors.INPUT_START_DATE(id_selector)) + end_date = self.driver.find_element(*QuestionSelectors.INPUT_END_DATE(id_selector)) + + # Get today's date and date one week ago + today = datetime.now().strftime("%m/%d/%Y") + one_week_ago = (datetime.now() - timedelta(days=7)).strftime("%m/%d/%Y") + + start_date.clear() + start_date.send_keys(today) + end_date.clear() + end_date.send_keys(one_week_ago) + + # Save the question + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + + # Validate error message for incorrect date order + self.validate_error_message( + QuestionSelectors.ERROR_START_DATE(id_selector), + "Das späteste Datum ist früher als das früheste Datum" + ) + + def assert_validation_errors_iq(self): + id_selector = self.get_selector_for(QuestionType.IMAGE) + + # Save the question + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + + # Validate error for missing image + self.validate_error_message( + QuestionSelectors.ERROR_FILE_PATH(id_selector), + "Der Dateipfad darf nicht leer sein." + ) + + def assert_validation_errors_bp(self): + id_selector = self.get_selector_for(QuestionType.BODY_PART) + min_number_answers = QuestionSelectors.INPUT_MIN_NUMBER_ANSWERS(id_selector) + max_number_answers = QuestionSelectors.INPUT_MAX_NUMBER_ANSWERS(id_selector) + + # Clear inputs and save to trigger validation errors for missing values + min_input = self.driver.find_element(*min_number_answers) + max_input = self.driver.find_element(*max_number_answers) + min_input.clear() + max_input.clear() + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.utils.scroll_to_bottom() + + # Validate error messages for missing min and max answers + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Es muss mindestens eine Körperregion als Antwort ausgewählt sein.\nMinimale Anzahl von zu beantwortenden Antworten muss vergeben werden" + ) + self.validate_error_message( + QuestionSelectors.ERROR_MAX_ANSWERS(id_selector), + "Maximale Anzahl von zu beantwortenden Antworten muss vergeben werden" + ) + + # Select one body part and validate errors for exceeding the available selections + selected_body_parts = self.select_body_parts({BodyPartSelectors.HEAD(id_selector, "front")}) # 1 body part selected + + min_input = self.driver.find_element(*min_number_answers) + max_input = self.driver.find_element(*max_number_answers) + min_input.clear() + max_input.clear() + self.utils.fill_text_field(min_number_answers, 100) + self.utils.fill_text_field(max_number_answers, 100) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.utils.scroll_to_bottom() + + # Validate error messages when min and max exceed the number of selected body parts + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Minimale Anzahl von zu beantwortenden Antworten ist größer als die Anzahl der vorhandenen Antworten" + ) + self.validate_error_message( + QuestionSelectors.ERROR_MAX_ANSWERS(id_selector), + "Maximale Anzahl von zu beantwortenden Antworten ist größer als die Anzahl der vorhandenen Antworten" + ) + + # Select another body part and validate errors for conflicting min and max values + selected_body_parts.append( + self.select_body_parts({BodyPartSelectors.THROAT(id_selector, "front")})) # 2 body parts selected + + min_input = self.driver.find_element(*min_number_answers) + max_input = self.driver.find_element(*max_number_answers) + min_input.clear() + max_input.clear() + self.utils.fill_text_field(min_number_answers, 2) # Set min to 2 + self.utils.fill_text_field(max_number_answers, 1) # Set max to 1 (conflict with min) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.utils.scroll_to_bottom() + + # Validate error message when min exceeds max + self.validate_error_message( + QuestionSelectors.ERROR_MIN_ANSWERS(id_selector), + "Minimale Anzahl von zu beantwortenden Antworten darf höchstens so groß wie die maximale Anzahl sein" + ) + + def assert_validation_errors_sq_nc(self, question_type: QuestionType): + + # Clear and validate initial errors + self.clear_min_max_step_inputs(question_type) + + # Click save and validate errors for missing values + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + + self.validate_min_max_step_errors( + expected_errors=[ + "Die Frage benötigt einen Minimalwert", + "Die Frage benötigt einen Maximalwert", + "Die Schrittweite der Frage entspricht nicht dem geforderten Format." + ] + ) + + # Set min > max and validate the corresponding error + self.set_min_max_step_inputs(min_value="10", max_value="5", step_size="1", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_min_max_step_errors( + expected_errors=[ + "Das Minimum der Frage war gleich oder größer als das Maximum" + ] + ) + + # Set invalid step size (greater than max-min difference) + self.set_min_max_step_inputs(min_value="1.0", max_value="4.0", step_size="10", question_type=question_type) + self.utils.click_element(QuestionSelectors.BUTTON_SAVE) + self.validate_min_max_step_errors( + expected_errors=[ + "Die Schrittweite der Frage war größer als der Abstand zwischen Minimum und Maximum\nDer Abstand zwischen Minimum und Maximum ist nicht restlos durch die Schrittweite teilbar" + ] + ) + + def assert_sq_nc_common_validations(self, question_type: QuestionType): + """ + Common validation for both Slider and Numbered Checkbox questions. + :param question_type: + """ + language_code = self.DEFAULT_LANGUAGE_CODE + + id_selector = self.get_selector_for(question_type) + min_value = self.driver.find_element(*QuestionSelectors.INPUT_MIN_VALUE(id_selector)) + max_value = self.driver.find_element(*QuestionSelectors.INPUT_MAX_VALUE(id_selector)) + step_size = self.driver.find_element(*QuestionSelectors.INPUT_STEP_SIZE(id_selector)) + assert min_value.is_displayed(), "Input for minimum value is not displayed." + assert max_value.is_displayed(), "Input for maximum value is not displayed." + assert step_size.is_displayed(), "Input for step size is not displayed." + + wysiwyg_min_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_WYSIWYG_SLIDER_MIN(language_code))) + assert wysiwyg_min_text.is_displayed(), "WYSIWYG editor for text at minimum position is not displayed." + + wysiwyg_max_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.INPUT_WYSIWYG_SLIDER_MAX(language_code))) + assert wysiwyg_max_text.is_displayed(), "WYSIWYG editor for text at maximum position is not displayed." + + def assert_mc_dd_common_validations(self, question_type): + try: + language_code = self.DEFAULT_LANGUAGE_CODE + id_selector = self.get_selector_for(question_type) + + # Validate min and max answers + input_min_answers = self.driver.find_element(*QuestionSelectors.INPUT_MIN_NUMBER_ANSWERS(id_selector)) + input_max_answers = self.driver.find_element(*QuestionSelectors.INPUT_MAX_NUMBER_ANSWERS(id_selector)) + assert input_min_answers.is_displayed(), "Input for min answers is not displayed." + assert input_max_answers.is_displayed(), "Input for max answers is not displayed." + + # Shows select for type of identification + dropdown_type_of_identification = self.driver.find_element( + *QuestionSelectors.DROPDOWN_TYPE_OF_IDENTIFICATION) + assert dropdown_type_of_identification.is_displayed(), "Dropdown for type of identification is not displayed." + + # Answer text + input_answer_text = self.driver.find_element( + *QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, 0, language_code)) + assert input_answer_text.is_displayed(), "Input for answer text is not displayed." + + # Initially activated and free text + checkbox_initial_active = self.driver.find_element( + *QuestionSelectors.CHECKBOX_ANSWER_INITIAL_ACTIVATION(id_selector, 0)) + checkbox_free_text = self.driver.find_element(*QuestionSelectors.CHECKBOX_FREE_TEXT(id_selector, 0)) + assert checkbox_initial_active.is_displayed(), "Checkbox to make answer initially active is not displayed." + assert checkbox_free_text.is_displayed(), "Checkbox to activate free text field is not displayed." + + # Identification code and score + input_identification = self.driver.find_element(*QuestionSelectors.INPUT_IDENTIFICATION(id_selector, 0)) + input_score = self.driver.find_element(*QuestionSelectors.INPUT_SCORE(id_selector, 0)) + assert input_identification.is_displayed(), "Input for identification is not displayed." + assert input_score.is_displayed(), "Input for score is not displayed." + + # Validate add and delete answer buttons + add_button = self.driver.find_element(*QuestionSelectors.BUTTON_ADD_ANSWER) + assert add_button.is_displayed(), "Button to add an answer is not displayed." + + text = 'Text to duplicate' + self.utils.fill_text_field(QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, 0, language_code), text) + self.utils.scroll_and_click(QuestionSelectors.BUTTON_ADD_ANSWER) + second_answer_text = self.driver.find_element( + *QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, 1, language_code)) + second_answer_text_value = second_answer_text.get_attribute("value") + assert second_answer_text_value == text, f"Text in the second answer field does not match. Expected: '{text}', Found: '{second_answer_text_value}'" + + panels = self.driver.find_elements(*QuestionSelectors.MULTIPLE_CHOICE_ANSWER_PANELS) + second_panel = panels[1] + delete_button = second_panel.find_element(*QuestionSelectors.DELETE_BUTTON_WITHIN_PANEL) + assert delete_button.is_displayed(), "Button to delete an answer is not displayed." + self.driver.execute_script("arguments[0].click();", delete_button) + self.utils.clear_text_field(QuestionSelectors.TEXTAREA_ANSWER_TEXT(id_selector, 0, language_code)) + except AssertionError as e: + raise e + + def assert_question_inputs(self, allowed_ids_of_visible_inputs): + try: + # Wait for the question type dropdown to be visible + WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionSelectors.DROPDOWN_QUESTION_TYPE)) + + # Get all visible input, select, and textarea elements + visible_inputs = [ + element for element in self.driver.find_elements(By.CSS_SELECTOR, "input, select, textarea") + if element.is_displayed() + ] + + # Validate that each visible input has an ID in the allowed list + for element in visible_inputs: + element_id = element.get_attribute("id") + assert element_id in allowed_ids_of_visible_inputs, f"Unexpected input found with id: {element_id}. Allowed: {allowed_ids_of_visible_inputs}" + + # Ensure the count of visible inputs matches the allowed list + assert len(visible_inputs) == len(allowed_ids_of_visible_inputs), ( + f"Mismatch in visible inputs. Expected {len(allowed_ids_of_visible_inputs)}, found {len(visible_inputs)}." + ) + except AssertionError as e: + raise e + + def validate_error_message(self, selector, expected_message): + error_message_element = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + selector)) + actual_message = error_message_element.text.strip() + assert actual_message == expected_message, f"Unexpected error message: '{actual_message}'" + + def validate_min_max_step_errors(self, expected_errors): + + error_slider_container = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionSelectors.ERROR_SLIDER_CONTAINER)) + error_messages = error_slider_container.find_elements(By.XPATH, "./div") + assert len(error_messages) > 0, "No error messages displayed under the sliderErrors container." + + # Extract actual error messages + actual_errors = [error.text.strip() for error in error_messages] + + # Validate each expected error is present + for expected_error in expected_errors: + assert expected_error in actual_errors, f"Expected error message '{expected_error}' not found. Actual: {actual_errors}" + + def assert_question_table_functionality(self, expected_count=11): + """ + :param expected_count: The expected number of questions. + """ + self.assert_number_of_questions_and_buttons(expected_count) + self.validate_reordering(original_index=0, new_index=2) + self.assert_add_question_button() + + def assert_number_of_questions(self, expected_count): + """ + :param expected_count: The expected number of questions. + :raises AssertionError: If the number of questions does not match the expected count. + """ + rows = [ + row for row in self.driver.find_element(*QuestionSelectors.TABLE_QUESTIONS) + .find_element(By.TAG_NAME, "tbody") + .find_elements(By.TAG_NAME, "tr") if row.is_displayed() + ] + assert len(rows) == expected_count, f"Expected {expected_count} questions, but found {len(rows)}." + return rows + + def assert_number_of_questions_and_buttons(self, expected_count, min_buttons=3, max_buttons=4): + """ + :param expected_count: The expected number of questions. + :param min_buttons: The minimum number of action buttons per question. + :param max_buttons: The maximum number of action buttons per question. + :raises AssertionError: If the number of questions or action buttons does not match expectations. + """ + rows = self.assert_number_of_questions(expected_count) + + for index, row in enumerate(rows, start=1): + action_buttons = row.find_elements(*QuestionSelectors.ACTION_BUTTONS) + assert min_buttons <= len(action_buttons) <= max_buttons, ( + f"Row {index} does not have the expected range of action buttons ({min_buttons}-{max_buttons}). " + f"Found: {len(action_buttons)}." + ) + + def validate_reordering(self, original_index, new_index): + """ + :param original_index: The index of the question to be moved. + :param new_index: The target index where the question should be dropped. + :raises AssertionError: If the reordering does not succeed. + """ + # Get all rows + rows = WebDriverWait(self.driver, 10).until( + lambda d: d.find_elements(*QuestionSelectors.TABLE_ROWS) + ) + assert len(rows) > max(original_index, new_index), "Invalid indices for reordering." + + # Identify source and target rows + source_id = rows[original_index].get_attribute('id') + target_id = rows[new_index].get_attribute('id') + + # Use the drag-and-drop utility + self.utils.drag_and_drop(QuestionSelectors.GRIP_SELECTOR(source_id), + QuestionSelectors.GRIP_SELECTOR(target_id)) + + # Validate the reordering + reordered_rows = WebDriverWait(self.driver, 10).until( + lambda d: d.find_elements(*QuestionSelectors.TABLE_ROWS) + ) + reordered_ids = [row.get_attribute("id") for row in reordered_rows] + + # Calculate expected order directly + expected_order = [row.get_attribute("id") for row in rows] + item_to_move = expected_order.pop(original_index) + expected_order.insert(new_index, item_to_move) + + # Assert the new order matches the expected order + assert reordered_ids == expected_order, f"Reordering failed. Expected: {expected_order}, Found: {reordered_ids}" + + def assert_add_question_button(self): + add_question_button = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionSelectors.BUTTON_ADD_QUESTION)) + assert add_question_button.is_displayed(), "The 'Add Question' button is not displayed." + + + diff --git a/selenium/helper/Questionnaire.py b/selenium/helper/Questionnaire.py new file mode 100644 index 00000000..fac707de --- /dev/null +++ b/selenium/helper/Questionnaire.py @@ -0,0 +1,384 @@ +import datetime + +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver 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 helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper, QuestionType, QuestionSelectors +from helper.SeleniumUtils import SeleniumUtils + + +class QuestionnaireSelectors: + BUTTON_ADD_QUESTIONNAIRE = (By.ID, "addQuestionnaire") + BUTTON_ADD_QUESTION = (By.ID, "addQuestion") + BUTTON_SAVE = (By.ID, "saveButton") + BUTTON_SAVE_AND_EDIT = (By.ID, "saveEditButton") + BUTTON_DELETE_LANGUAGE = lambda language_code: (By.ID, f"{language_code}_Delete") + BUTTON_DELETE_LANGUAGE_MODAL = (By.XPATH, "//button[contains(@class, 'btn-danger') and text()='Entfernen']") + + DROPDOWN_ADD_LANGUAGE = (By.CSS_SELECTOR, "div#languageDropdown.dropdown") + DROPDOWN_LANGUAGE_ITEM = lambda language_id: (By.CSS_SELECTOR, f"a.dropdown-item#{language_id}") + + DELETE_LANGUAGE_MODAL = (By.ID, "deleteLanguageModal") + DELETE_LAST_LANGUAGE_MODAL = (By.ID, "deleteLastLanguageModal") + DELETE_LAST_LANGUAGE_OK_BUTTON = (By.CSS_SELECTOR, "#deleteLastLanguageModal .btn-secondary") + + INPUT_NAME = (By.ID, "name") + INPUT_LOGO = (By.ID, "file") + INPUT_LOCALIZED_DISPLAY_NAME = lambda language_code: (By.ID, f"localizedDisplayName{language_code}") + INPUT_EDITABLE_DESCRIPTION = (By.CSS_SELECTOR, "div.note-editable") + INPUT_WELCOME_TEXT_EDITABLE_DIV = lambda language_code: (By.XPATH, f'//*[@id="localizedWelcomeTextCollapsableText_{language_code}"]/div/div[2]/div[2]') + INPUT_FINAL_TEXT_EDITABLE_DIV = lambda language_code: (By.XPATH, f'//*[@id="localizedFinalTextCollapsableText_{language_code}"]/div/div[2]/div[2]') + + QUESTIONNAIRE_TABLE = (By.ID, "questionnaireTable") + TABLE_ROWS = (By.CSS_SELECTOR, "table#questionnaireTable tbody tr") + TABLE_FIRST_ROW = (By.XPATH, "//tbody/tr[not(@id='emptyRow')][1]") + FLAG_ICONS = (By.CSS_SELECTOR, "table#questionnaireTable img[title]") + PAGINATION = (By.ID, "questionnaireTable_paginate") + SEARCH_BOX = (By.CSS_SELECTOR, "#questionnaireTable_filter input[type='search']") + ACTION_BUTTONS = (By.CSS_SELECTOR, "td.actionColumn > div.d-none.d-xl-block > a.link") + LANGUAGE_FLAG_ICONS = (By.CSS_SELECTOR, ".languageLabel img") + +class QuestionnaireHelper: + + DEFAULT_DESCRIPTION = "This description of the questionnaire is a dummy text." + DEFAULT_LANGUAGE_CODE = "de_DE" + DEFAULT_LANGUAGE_CODE_EN = "en" + DEFAULT_LOCALIZED_WELCOME_TEXT = 'A welcome text for the questionnaire. Nothing special to see.' + DEFAULT_LOCALIZED_FINAL_TEXT = 'This text is shown at the end of the questionnaire.' + + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver, navigation_helper) + self.navigation_helper = navigation_helper + self.question_helper = QuestionHelper(driver, navigation_helper) + + def click_add_questionnaire_button(self): + """Clicks the 'Add Questionnaire' button.""" + self.utils.click_element(QuestionnaireSelectors.BUTTON_ADD_QUESTIONNAIRE) + + def fill_questionnaire_details(self, questionnaire_name=None, description=None, language_code=None, localized_display_name=None, + localized_welcome_text=None, localized_final_text=None, question_types=None): + """ + :param questionnaire_name: Name of the questionnaire (optional). + :param description: Description of the questionnaire (optional). + :param language_code: Language code for localized fields (e.g., 'de_DE') (optional). + :param localized_display_name: Localized display name for the questionnaire (optional). + :param localized_welcome_text: Localized welcome text (optional). + :param localized_final_text: Localized final text (optional). + """ + timestamp: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + fragetypen_text = '' + if question_types: + delimiter = "+" + fragetypen_text = f" (Question types: {delimiter.join(str(question_type.value) for question_type in question_types)})" + + questionnaire_name = questionnaire_name or f"Questionnaire {timestamp}{fragetypen_text}" + description = description or self.DEFAULT_DESCRIPTION + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + localized_display_name = localized_display_name or questionnaire_name + localized_welcome_text = localized_welcome_text or self.DEFAULT_LOCALIZED_WELCOME_TEXT + localized_final_text = localized_final_text or self.DEFAULT_LOCALIZED_FINAL_TEXT + + # Fill in the questionnaire name + self.utils.fill_text_field(QuestionnaireSelectors.INPUT_NAME, questionnaire_name) + + # Fill in the description + WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionnaireSelectors.INPUT_EDITABLE_DESCRIPTION)) + self.utils.fill_text_field(QuestionnaireSelectors.INPUT_EDITABLE_DESCRIPTION, description) + + # Fill in the localized display name + self.utils.fill_text_field(QuestionnaireSelectors.INPUT_LOCALIZED_DISPLAY_NAME(language_code), localized_display_name) + + # Fill Welcome Text + if localized_welcome_text: + welcome_text_div = WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionnaireSelectors.INPUT_WELCOME_TEXT_EDITABLE_DIV(language_code))) + self.utils.fill_editable_div(welcome_text_div, localized_welcome_text) + + # Fill Final Text + if localized_final_text: + final_text_div = WebDriverWait(self.driver, 30).until(EC.visibility_of_element_located( + QuestionnaireSelectors.INPUT_FINAL_TEXT_EDITABLE_DIV(language_code))) + self.utils.fill_editable_div(final_text_div, localized_final_text) + + return questionnaire_name + + def add_language(self, language_code): + """ + :param language_code: The Code of the language to add (e.g., 'en' for English). + """ + try: + # Click on the "Add Language" button + add_language_button = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionnaireSelectors.DROPDOWN_ADD_LANGUAGE)) + add_language_button.click() + + # Select the desired language from the dropdown + language_option = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable( + QuestionnaireSelectors.DROPDOWN_LANGUAGE_ITEM(language_code))) + language_option.click() + + except TimeoutException: + raise AssertionError(f"Timed out while trying to add language '{language_code}'.") + + + def click_add_question_button(self): + self.utils.click_element(QuestionnaireSelectors.BUTTON_ADD_QUESTION) + + def save_questionnaire_edit_question(self): + """ + :return: The ID of the newly created questionnaire. + """ + current_url = self.driver.current_url + self.utils.click_element(QuestionnaireSelectors.BUTTON_SAVE_AND_EDIT) + + # Wait for redirection and extract questionnaire ID + WebDriverWait(self.driver, 15).until(EC.url_changes(current_url)) + new_url = self.driver.current_url + try: + questionnaire_id = new_url.split("id=")[1] + return questionnaire_id + except IndexError: + raise Exception(f"Failed to extract questionnaire ID from URL: {new_url}") + + + def save_questionnaire(self): + """ + :return: The ID of the newly created questionnaire. + """ + current_url = self.driver.current_url + self.utils.click_element(QuestionnaireSelectors.BUTTON_SAVE) + + # Wait for redirection and extract questionnaire ID + WebDriverWait(self.driver, 15).until(EC.url_changes( + current_url)) + + WebDriverWait(self.driver, 30).until(EC.presence_of_element_located( + QuestionnaireSelectors.TABLE_FIRST_ROW)) + + # Find the last row in the table + first_row = self.driver.find_element(*QuestionnaireSelectors.TABLE_FIRST_ROW) + + questionnaire_id = first_row.get_attribute("id") + + if not questionnaire_id: + raise Exception("The ID of the last added questionnaire could not be found.") + + return questionnaire_id + + def create_questionnaire_with_questions(self, questionnaire_name=None, questionnaire_description=None, + questionnaire_language_code=None, questionnaire_display_name=None, + questionnaire_welcome_text=None, questionnaire_final_text=None, question_types=None): + """ + :param questionnaire_name: Name of the questionnaire. + :param questionnaire_description: Description of the questionnaire. + :param questionnaire_language_code: Language code (e.g., 'de_DE'). + :param questionnaire_display_name: Localized display name of the questionnaire. + :param questionnaire_welcome_text: Welcome text for the questionnaire. + :param questionnaire_final_text: Final text for the questionnaire. + :param question_types: A list of question types. + :return: A dictionary containing the questionnaire ID and a list of added questions. + """ + excluded_question_types = {QuestionType.IMAGE, QuestionType.BODY_PART, QuestionType.BARCODE} + question_types = question_types or [question_type for question_type in QuestionType if question_type not in excluded_question_types] + + # Navigate to "Manage Questionnaires" + self.navigation_helper.navigate_to_manage_questionnaires() + + # Create the questionnaire + self.click_add_questionnaire_button() + questionnaire_name = self.fill_questionnaire_details(questionnaire_name, questionnaire_description, + questionnaire_language_code, questionnaire_display_name, + questionnaire_welcome_text, questionnaire_final_text, question_types) + questionnaire_id = self.save_questionnaire_edit_question() + + # Add questions to the questionnaire + added_questions = [] + for question_type in question_types: + self.click_add_question_button() + question_info = self.question_helper.add_question_by_type_default_value(question_type) + question_info["id"] = self.question_helper.save_question() # Save the question and retrieve ID + added_questions.append(question_info) + + return {"id": questionnaire_id, "name": questionnaire_name, "questions": added_questions} + + + def reorder_question(self, source_question_id, new_index): + # Get all rows + rows = WebDriverWait(self.driver, 10).until( + lambda d: d.find_elements(*QuestionSelectors.TABLE_ROWS) + ) + + # Identify target rows + target_question_id = rows[new_index].get_attribute('id') + + if target_question_id == source_question_id: return + + # Use the drag-and-drop utility + self.utils.drag_and_drop(QuestionSelectors.GRIP_SELECTOR(source_question_id), + QuestionSelectors.GRIP_SELECTOR(target_question_id)) + + self.driver.refresh() + + # Validate the reordering + reordered_rows = WebDriverWait(self.driver, 10).until( + lambda d: d.find_elements(*QuestionSelectors.TABLE_ROWS) + ) + reordered_ids = [row.get_attribute("id") for row in reordered_rows] + + return reordered_ids + + +class QuestionnaireAssertHelper(QuestionnaireHelper): + + def assert_questionnaire_list(self): + try: + # Validate table with questionnaires + questionnaire_table = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.QUESTIONNAIRE_TABLE)) + assert questionnaire_table.is_displayed(), "The table with available questionnaires is not displayed." + + # Validate flag icons + flag_icons = self.driver.find_elements(*QuestionnaireSelectors.FLAG_ICONS) + assert len(flag_icons) > 0, "No flag icons are displayed for the questionnaires." + + # Validate pagination + pagination = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.PAGINATION)) + assert pagination.is_displayed(), "Pagination is not displayed." + + # Validate search box + search_box = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.SEARCH_BOX)) + assert search_box.is_displayed(), "Search box is not displayed." + + # Validate buttons in the action column + rows = self.driver.find_elements(*QuestionnaireSelectors.TABLE_ROWS) + assert len(rows) > 0, "No rows found in the questionnaire table." + + for index, row in enumerate(rows, start=1): + action_buttons = row.find_elements(*QuestionnaireSelectors.ACTION_BUTTONS) + assert len(action_buttons) == 5, f"Row {index} does not have exactly 5 action buttons. Found: {len(action_buttons)}" + + # Validate the button to create a new questionnaire + create_button = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.BUTTON_ADD_QUESTIONNAIRE)) + assert create_button.is_displayed(), "Button to create a new questionnaire is not displayed." + + except TimeoutException: + raise AssertionError("Timed out waiting for elements on the questionnaire list view.") + except AssertionError as e: + raise e + + def assert_questionnaire_fill_page(self, add_language_code = None): + add_language_code = add_language_code or self.DEFAULT_LANGUAGE_CODE_EN + try: + # Validate dropdown to add language + dropdown = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.DROPDOWN_ADD_LANGUAGE)) + assert dropdown.is_displayed(), "Language dropdown for adding a language is not displayed." + + # Validate input for questionnaire name + questionnaire_name_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_NAME)) + assert questionnaire_name_input.is_displayed(), "Input for questionnaire name is not displayed." + + # Validate input for logo + logo_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_LOGO)) + logo_display = self.driver.execute_script("return window.getComputedStyle(arguments[0]).display;",logo_input) + assert logo_display != "none", "Logo input display is set to 'none'." + + # Validate WYSIWYG editor for description + wysiwyg_description = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_EDITABLE_DESCRIPTION)) + assert wysiwyg_description.is_displayed(), "WYSIWYG editor for description is not displayed." + + # Validate input for display name + display_name_input = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_LOCALIZED_DISPLAY_NAME(self.DEFAULT_LANGUAGE_CODE))) + assert display_name_input.is_displayed(), "Input for display name is not displayed." + + # Validate WYSIWYG editor for welcome text + wysiwyg_welcome_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_WELCOME_TEXT_EDITABLE_DIV(self.DEFAULT_LANGUAGE_CODE))) + assert wysiwyg_welcome_text.is_displayed(), "WYSIWYG editor for welcome text is not displayed." + + # Validate WYSIWYG editor for end text + wysiwyg_end_text = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + QuestionnaireSelectors.INPUT_FINAL_TEXT_EDITABLE_DIV(self.DEFAULT_LANGUAGE_CODE))) + assert wysiwyg_end_text.is_displayed(), "WYSIWYG editor for end text is not displayed." + + # Validate flag icons + flag_icons = self.driver.find_elements( + *QuestionnaireSelectors.LANGUAGE_FLAG_ICONS) + assert len(flag_icons) > 0, "No flag icons are displayed for inputs." + + # Add a language + self.add_language(add_language_code) + + # Validate duplicated inputs with flag icons + new_flag_icons = self.driver.find_elements( + *QuestionnaireSelectors.LANGUAGE_FLAG_ICONS) + assert len(new_flag_icons) == len(flag_icons) * 2, "Adding a language did not duplicate inputs with flag icons." + + # Delete a language + delete_added_language_button = self.driver.find_elements( + *QuestionnaireSelectors.BUTTON_DELETE_LANGUAGE(add_language_code)) + assert len(delete_added_language_button) > 0, "No delete buttons for languages are displayed." + delete_added_language_button[0].click() + + # Close the modal + delete_language_modal = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionnaireSelectors.DELETE_LANGUAGE_MODAL)) + remove_button = delete_language_modal.find_element( + *QuestionnaireSelectors.BUTTON_DELETE_LANGUAGE_MODAL) + remove_button.click() + + # Validate deletion removes duplicated inputs + updated_flag_icons = self.driver.find_elements( + *QuestionnaireSelectors.LANGUAGE_FLAG_ICONS) + assert len(updated_flag_icons) == len(flag_icons), "Deleting a language did not remove duplicated inputs." + + # Validate deleting the last language is not possible + delete_default_language_button = self.driver.find_elements( + *QuestionnaireSelectors.BUTTON_DELETE_LANGUAGE(self.DEFAULT_LANGUAGE_CODE)) + if len(delete_default_language_button) == 1: + delete_default_language_button[0].click() + + # Wait until the modal for the last language becomes visible + last_language_modal = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionnaireSelectors.DELETE_LAST_LANGUAGE_MODAL)) + assert last_language_modal.is_displayed(), "Modal for deleting the last language is not displayed." + + # Click the 'Ok' button in the modal + ok_button = last_language_modal.find_element( + *QuestionnaireSelectors.DELETE_LAST_LANGUAGE_OK_BUTTON) + ok_button.click() + + questionnaire_name = self.fill_questionnaire_details() + + self.utils.scroll_to_element(QuestionnaireSelectors.BUTTON_SAVE) + + # Wait until the save button is visible + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + QuestionnaireSelectors.BUTTON_SAVE)) + + # Validate save button creates a questionnaire and redirects + questionnaire_id = self.save_questionnaire() + + WebDriverWait(self.driver, 10).until(EC.url_contains("/questionnaire/list")) + + return { + "id": questionnaire_id, + "name": questionnaire_name + } + except TimeoutException: + raise AssertionError("Timed out waiting for elements on the questionnaire fill page.") + except AssertionError as e: + raise e \ No newline at end of file diff --git a/selenium/helper/Score.py b/selenium/helper/Score.py new file mode 100644 index 00000000..52116ee4 --- /dev/null +++ b/selenium/helper/Score.py @@ -0,0 +1,332 @@ +from datetime import datetime + +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + +from helper.Questionnaire import QuestionnaireHelper +from helper.SeleniumUtils import DropdownMethod + +class ScoreSelectors: + BUTTON_ADD_SCORE = (By.ID, "addScore") + BUTTON_SAVE = (By.ID, "saveButton") + + DROPDOWN_OPERATOR = lambda index: (By.NAME, f"expression.expressions[{index}].operatorId") + DROPDOWN_EXPRESSION = (By.ID, "expression") + DROPDOWN_QUESTION = (By.NAME, "expression.questionId") + DROPDOWN_SCORE = (By.NAME, "expression.scoreId") + + INPUT_NAME = (By.ID, "name") + INPUT_VALUE = lambda index: (By.NAME, f"expression.expressions[{index}].value") + + TABLE_ROWS = (By.CSS_SELECTOR, "tbody > tr:not(#emptyRow)") + SCORE_TABLE = (By.ID, "scoreTable") + ACTION_BUTTONS = (By.CSS_SELECTOR, "td.actionColumn > a.link") + + DROPDOWN_EXPRESSION_TYPE = (By.ID, "expression") + EXPRESSION_OPERATOR_SELECTED = lambda path: (By.NAME, f"{path}.operatorId") + EXPRESSION_OPERATOR_UNSELECTED = lambda path: (By.NAME, f"{path}") + EXPRESSION_VALUE = lambda path: (By.NAME, f"{path}.value") + EXPRESSION_QUESTION = lambda path: (By.NAME, f"{path}.questionId") + EXPRESSION_SCORE = lambda path: (By.NAME, f"{path}.scoreId") + + # Specific score types + class ExpressionType: + ADDITION = "1" + SUBTRACTION = "2" + MULTIPLICATION = "3" + DIVISION = "4" + QUESTION_VALUE = "5" + VALUE = "6" + SUM = "7" + GREATER_THAN = "8" + GREATER_THAN_EQUALS = "9" + LESS_THAN = "10" + LESS_THAN_EQUALS = "11" + EQUALS = "12" + NOT_EQUALS = "13" + COUNTER = "14" + AVERAGE = "15" + VALUE_OF_SCORE = "16" + MAXIMUM = "17" + MINIMUM = "18" + + +class ScoreHelper(QuestionnaireHelper): + + def click_add_score_button(self): + self.utils.click_element(ScoreSelectors.BUTTON_ADD_SCORE) + + def save_score(self): + self.utils.click_element(ScoreSelectors.BUTTON_SAVE) + + def create_basic_score(self, expression_type, name_prefix=None, question_id=None, score_id=None): + """ + Creates a basic score for a given operator. + + :param expression_type: The expression type for which the score should be created (e.g., "+" or "VALUE"). + :param name_prefix: Prefix for the name of the score. + :param question_id: Question ID for QUESTION_VALUE operator. + :param score_id: Score ID for VALUE_OF_SCORE operator. + :return: A dictionary with details of the created score. + """ + timestamp: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + name_prefix = name_prefix or f"Basic Score {timestamp}" + + score_name = f"{name_prefix} - {expression_type}" + expression_tree = self._get_basic_expression_tree(expression_type, question_id=question_id, score_id=score_id) + + # Add the score using the helper + self.add_score( + name=score_name, + expression_type=expression_type, + expression_tree=expression_tree + ) + + return { + "name": score_name, + "operator": expression_type, + "expression_tree": expression_tree + } + + def _get_basic_expression_tree(self, expression_type, question_id=None, score_id=None): + """ + Generates a basic expression tree for a given operator. + + :param expression_type: The operator value for the expression. + :param question_id: The ID of the question to use (required for QUESTION_VALUE). + :param score_id: The ID of the score to use (required for VALUE_OF_SCORE). + :return: A dictionary representing a basic expression tree. + """ + if expression_type in [ScoreSelectors.ExpressionType.ADDITION, ScoreSelectors.ExpressionType.SUBTRACTION, + ScoreSelectors.ExpressionType.MULTIPLICATION, ScoreSelectors.ExpressionType.DIVISION, + ScoreSelectors.ExpressionType.GREATER_THAN, ScoreSelectors.ExpressionType.GREATER_THAN_EQUALS, + ScoreSelectors.ExpressionType.LESS_THAN, ScoreSelectors.ExpressionType.LESS_THAN_EQUALS, + ScoreSelectors.ExpressionType.EQUALS, ScoreSelectors.ExpressionType.NOT_EQUALS]: + # Binary operator: Two operands + return { + "operator": expression_type, + "nested": [ + {"operator": ScoreSelectors.ExpressionType.VALUE, "value": 10}, + {"operator": ScoreSelectors.ExpressionType.VALUE, "value": 5} + ] + } + elif expression_type in [ScoreSelectors.ExpressionType.SUM, ScoreSelectors.ExpressionType.COUNTER, + ScoreSelectors.ExpressionType.AVERAGE, ScoreSelectors.ExpressionType.MAXIMUM, + ScoreSelectors.ExpressionType.MINIMUM]: + # Multi operator: Multiple operands + return { + "operator": expression_type, + "nested": [ + {"operator": ScoreSelectors.ExpressionType.VALUE, "value": 10}, + {"operator": ScoreSelectors.ExpressionType.VALUE, "value": 20} + ] + } + elif expression_type == ScoreSelectors.ExpressionType.VALUE: + # Unary operator: Single value + return { + "operator": expression_type, + "value": 42 + } + elif expression_type == ScoreSelectors.ExpressionType.QUESTION_VALUE: + if not question_id: + raise ValueError("QUESTION_VALUE requires a valid question_id.") + # Question-based operator + return { + "operator": expression_type, + "question": question_id + } + elif expression_type == ScoreSelectors.ExpressionType.VALUE_OF_SCORE: + if not score_id: + raise ValueError("VALUE_OF_SCORE requires a valid score_id.") + # Score-based operator + return { + "operator": expression_type, + "score": score_id + } + else: + raise ValueError(f"Unsupported operator: {expression_type}") + + def add_score(self, name=None, expression_type=None, expression_tree=None): + """ + Adds a score with a specific name and an expression structure. + + :param name: Optional name of the score (generated if None). + :param expression_type: Type of the root expression (e.g., ADDITION, SUM). + :param expression_tree: Nested dictionary representing the expression tree. + :return: A dictionary with details of the created score. + """ + # Generate name if not provided + timestamp: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + name = name or f"Score {timestamp} - {expression_type}" + + # Click Add Score Button + self.utils.click_element(ScoreSelectors.BUTTON_ADD_SCORE) + self.utils.fill_text_field(ScoreSelectors.INPUT_NAME, name) + + # Build the expression tree + if expression_tree: + self._build_expression("expression", expression_tree) + + return { + "name": name, + "operator": expression_type, + "expression_tree": expression_tree + } + + def _build_expression(self, path: str, expression: dict): + """ + Recursively builds an expression. + + :param path: Path prefix for the current expression (e.g., "expression"). + :param expression: Dictionary representing the expression structure. + """ + operator = expression.get("operator") + value = expression.get("value") + question = expression.get("question") + score = expression.get("score") + nested_expressions = expression.get("nested") + + if operator: + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(path), operator, DropdownMethod.VALUE) + + if value is not None: + self.utils.fill_number_field(ScoreSelectors.EXPRESSION_VALUE(path), value) + + if question: + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_QUESTION(path), question, DropdownMethod.VALUE) + + if score: + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_SCORE(path), score, DropdownMethod.VALUE) + + if nested_expressions: + for i, nested_expression in enumerate(nested_expressions): + nested_path = f"{path}.expressions[{i}]" + self._build_expression(nested_path, nested_expression) + +class ScoreAssertHelper(ScoreHelper): + + def assert_scores_list(self): + # Arrange: Verify 'Add New Score' button is present + add_score_button = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.BUTTON_ADD_SCORE) + ) + assert add_score_button.is_displayed(), "The 'Add New Score' button is not displayed." + + # Act & Assert: Add the first score and verify + self.add_and_verify_score(ScoreSelectors.ExpressionType.ADDITION) + + # Act & Assert: Add a second score and verify + self.add_and_verify_score(ScoreSelectors.ExpressionType.MULTIPLICATION) + + # Verify: Action buttons are displayed for each row + self.assert_scores_table_and_buttons() + + def add_and_verify_score(self, expression_type): + """ + :param expression_type: The type of the score to add. + """ + + # Add a basic score + score = self.create_basic_score(expression_type) + + # Save the score + self.save_score() + + # Verify: Score table contains the new score + score_table = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + ScoreSelectors.SCORE_TABLE)) + assert score_table.is_displayed(), "Score table is not displayed." + + rows = score_table.find_elements(By.TAG_NAME, "tr") + assert any(score['name'] in row.text for row in rows), f"Score '{score['name']}' is not found in the table." + + def assert_scores_table_and_buttons(self): + # Locate table rows + rows = self.driver.find_elements(*ScoreSelectors.TABLE_ROWS) + + # Ensure the table has rows + assert len(rows) > 0, "Scores table is empty. No rows found." + + for index, row in enumerate(rows, start=1): + # Locate action buttons in the current row + action_buttons = row.find_elements(*ScoreSelectors.ACTION_BUTTONS) + + # Assert that each row has exactly two action buttons + assert len(action_buttons) == 2, ( + f"Row {index} does not have exactly two action buttons. Found: {len(action_buttons)}." + ) + + def assert_score_fill(self): + """ + Verifies that the Score Fill page: + - Displays inputs for the question score + - Shows dropdowns for selecting operators + - Allows the combination of different operators, adjusting dynamically based on the selected values + - Handles errors for invalid combinations gracefully + """ + + # Step 1: Navigate to the Score Fill page + self.click_add_score_button() + + # Step 2: Verify the presence of the input field for the score name + score_name_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.INPUT_NAME)) + assert score_name_input.is_displayed(), "Score name input field is not displayed." + + # Fill the score name + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + score_name = f"Fill Score Test {timestamp}" + self.utils.fill_text_field(ScoreSelectors.INPUT_NAME, score_name) + + # Step 3: Verify the presence of the operator dropdown + operator_dropdown = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.DROPDOWN_EXPRESSION)) + assert operator_dropdown.is_displayed(), "Operator dropdown is not displayed." + + # Select an initial operator + self.utils.select_dropdown(ScoreSelectors.DROPDOWN_EXPRESSION, ScoreSelectors.ExpressionType.ADDITION, + DropdownMethod.VALUE) + + # Step 4: Validate the appearance of nested operator inputs + first_nested_path = "expression.expressions[0]" + first_nested_operator = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(first_nested_path))) + assert first_nested_operator.is_displayed(), "First nested operator input is not displayed." + + # Add a nested operator and validate the structure + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(first_nested_path), + ScoreSelectors.ExpressionType.VALUE, DropdownMethod.VALUE) + first_value_input = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.EXPRESSION_VALUE(first_nested_path))) + assert first_value_input.is_displayed(), "Value input for the first nested operator is not displayed." + + self.utils.fill_number_field(ScoreSelectors.EXPRESSION_VALUE(first_nested_path), 3) + + # Step 5: Add a second operator and validate the DOM structure + second_nested_path = "expression.expressions[1]" + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(second_nested_path), + ScoreSelectors.ExpressionType.MULTIPLICATION, DropdownMethod.VALUE) + + # Ensure a second nested operator appears + second_nested_first_path = f"{second_nested_path}.expressions[0]" + second_nested_first_operator = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(second_nested_first_path))) + assert second_nested_first_operator.is_displayed(), "Second nested first operator is not displayed." + + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(second_nested_first_path), + ScoreSelectors.ExpressionType.VALUE, DropdownMethod.VALUE) + self.utils.fill_number_field(ScoreSelectors.EXPRESSION_VALUE(second_nested_first_path), 3) + + # Add another nested operator to complete the second branch + second_nested_second_path = f"{second_nested_path}.expressions[1]" + second_nested_second_operator = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(second_nested_second_path))) + assert second_nested_second_operator.is_displayed(), "Second nested second operator is not displayed." + + self.utils.select_dropdown(ScoreSelectors.EXPRESSION_OPERATOR_UNSELECTED(second_nested_second_path), + ScoreSelectors.ExpressionType.VALUE, DropdownMethod.VALUE) + self.utils.fill_number_field(ScoreSelectors.EXPRESSION_VALUE(second_nested_second_path), 4) + + self.save_score() + diff --git a/selenium/helper/SeleniumUtils.py b/selenium/helper/SeleniumUtils.py new file mode 100644 index 00000000..d30ed400 --- /dev/null +++ b/selenium/helper/SeleniumUtils.py @@ -0,0 +1,381 @@ +from enum import Enum + +from selenium.common.exceptions import TimeoutException, ElementClickInterceptedException +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import Select +from selenium.webdriver.support.ui import WebDriverWait + + +class RemoveButtonSelectors: + QUESTIONNAIRE = "removeQuestionnaire_{}" + BUNDLE = "removeBundle_{}" + CLINIC = "removeClinic_{}" + +class SearchBoxSelectors: + QUESTIONNAIRE = (By.CSS_SELECTOR, "#questionnaireTable_filter input[type='search']") + BUNDLE = (By.CSS_SELECTOR, "#bundleTable_filter input[type='search']") + CLINIC = (By.CSS_SELECTOR, "#clinicTable_filter input[type='search']") + SCHEDULED_ENCOUNTER = (By.CSS_SELECTOR, "#encounterScheduled_filter input[type='search']") + INVITATION = (By.CSS_SELECTOR, "#invitationTable_filter input[type='search']") + +class DropdownMethod(Enum): + VISIBLE_TEXT = "visible_text" + VALUE = "value" + INDEX = "index" + +class ErrorSelectors: + INPUT_VALIDATION_SELECTOR=(By.CLASS_NAME, "validationError") + CONFIGURATION_ERROR_SELECTOR=(By.CLASS_NAME, "configurationError") + +class SeleniumUtils: + def __init__(self, driver, navigation_helper = None): + self.driver = driver + self.navigator = navigation_helper + + def click_element(self, selector): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "element_id")). + """ + try: + element = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(selector)) + self.driver.execute_script("arguments[0].scrollIntoView(true);", element) + element.click() + except ElementClickInterceptedException: + try: + element = self.driver.find_element(*selector) + self.driver.execute_script("arguments[0].click();", element) + except Exception as e: + raise Exception(f"Failed to click element {selector} using JavaScript: {e}") + except TimeoutException: + raise Exception(f"Timeout while waiting for element {selector} to be clickable.") + + def fill_text_field(self, selector, text): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "text_field_id")). + :param text: The text to input into the text field. + """ + try: + text_field = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + selector)) + + text_field.clear() + text_field.send_keys(text) + except TimeoutException: + raise Exception(f"Timeout while waiting for text field {selector} to be visible.") + except Exception as e: + raise Exception(f"Failed to fill text field {selector}: {e}") + + def fill_number_field(self, selector, number): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "text_field_id")). + :param number: The number to input into the field. + """ + try: + text_field = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + selector)) + + text_field.clear() + text_field.send_keys(number) + except TimeoutException: + raise Exception(f"Timeout while waiting for text field {selector} to be visible.") + except Exception as e: + raise Exception(f"Failed to fill text field {selector}: {e}") + + def clear_text_field(self, selector): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "text_field_id")). + """ + try: + text_field = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + selector)) + text_field.clear() + except TimeoutException: + raise Exception(f"Timeout while waiting for text field {selector} to be visible.") + except Exception as e: + raise Exception(f"Failed to clear text field {selector}: {e}") + + def fill_editable_div(self, editable_div, text): + """ + Fills an editable div element (e.g., a WYSIWYG editor) with the specified text. + :param editable_div: WebElement representing the editable div to be filled. + :param text: The text to insert into the editable div. + """ + try: + # Scroll to the visible area + self.driver.execute_script("arguments[0].scrollIntoView(true);", editable_div) + + # Wait until the element is clickable + WebDriverWait(self.driver, 10).until(EC.visibility_of(editable_div)) + WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(editable_div)) + + ActionChains(self.driver).move_to_element(editable_div).click().perform() + + # Select all text and delete + try: + editable_div.send_keys(Keys.CONTROL + "a") # Select all (CTRL + A) + editable_div.send_keys(Keys.DELETE) # Delete selected text + except Exception: + # Fallback: Clear content using JavaScript + self.driver.execute_script("arguments[0].innerHTML = '';", editable_div) + + # Enter new text + editable_div.send_keys(text) + + except Exception as e: + raise Exception(f"Error while filling the editable div: {e}") + + def scroll_and_click(self, selector): + # Scroll to element + element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + selector)) + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) + + # Wait until the element is clickable + try: + WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(selector)).click() + except ElementClickInterceptedException: + # Fallback: JavaScript click if the click is blocked + self.driver.execute_script("arguments[0].click();", element) + + def scroll_to_element(self, selector): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "element_id")). + """ + try: + element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + selector)) + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) + except TimeoutException: + raise Exception(f"Timeout while trying to scroll to element {selector}.") + except Exception as e: + raise Exception(f"Failed to scroll to element {selector}: {e}") + + def scroll_to_bottom(self): + try: + # Wait for the page to fully load + WebDriverWait(self.driver, 30).until(lambda driver: driver.execute_script("return document.readyState") == "complete") + + # Execute JavaScript to scroll to the bottom + self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") + + # Wait for the scrolling to complete + WebDriverWait(self.driver, 10).until( + lambda driver: driver.execute_script( + "return window.scrollY + window.innerHeight") >= driver.execute_script( + "return document.body.scrollHeight") + ) + except TimeoutException: + raise Exception("Timeout while trying to scroll to the bottom of the page.") + except Exception as e: + raise Exception(f"Failed to scroll to the bottom of the page: {e}") + + def scroll_to_element_or_bottom(self, selector, timeout=10): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "element_id")). + :param timeout: Maximum time to wait for scrolling to complete. + """ + try: + # Wait for the page to fully load + WebDriverWait(self.driver, 30).until(lambda driver: driver.execute_script("return document.readyState") == "complete") + + # Locate the element + element = WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located( + selector)) + + # Scroll the element into view + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) + + # Wait until the element is approximately centered or the bottom is reached + WebDriverWait(self.driver, timeout).until( + lambda driver: abs(driver.execute_script("return arguments[0].getBoundingClientRect().top;", element) + ) < driver.execute_script("return window.innerHeight / 2") or + driver.execute_script("return window.scrollY + window.innerHeight >= document.body.scrollHeight") + ) + except TimeoutException: + raise Exception( + f"Timeout while trying to scroll to element {selector} or detecting the bottom of the page.") + + def toggle_checkbox(self, selector, enable=True): + """ + :param selector: Locator of the checkbox element. + :param enable: True to enable the checkbox, False to disable it. + """ + try: + checkbox = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + selector)) + self.scroll_to_element(selector) + if checkbox.is_selected() != enable: + self.click_element(selector) + except TimeoutException: + raise Exception(f"Timeout while waiting for checkbox {selector} to be present.") + except Exception as e: + raise Exception(f"Error while toggling checkbox {selector}: {e}") + + def select_dropdown(self, selector, value, method=DropdownMethod.VISIBLE_TEXT): + """ + :param selector: The locator tuple for the dropdown element. + :param value: The value, visible text, or index of the option to select. + :param method: The selection method, an instance of DropdownMethod Enum. + """ + try: + dropdowns = WebDriverWait(self.driver, 10).until(EC.presence_of_all_elements_located( + selector)) + + # Filter visible dropdowns + visible_dropdown = next(el for el in dropdowns if el.is_displayed()) + + # Initialize the Select object with the visible dropdown + select = Select(visible_dropdown) + + if method == DropdownMethod.VISIBLE_TEXT: + select.select_by_visible_text(value) + elif method == DropdownMethod.VALUE: + select.select_by_value(value) + elif method == DropdownMethod.INDEX: + visible_options = [ + option for option in select.options + if option.is_displayed() and not option.get_attribute("disabled") + ] + if value < 0 or value >= len(visible_options): + raise ValueError(f"Invalid index '{value}'. Must be between 0 and {len(visible_options) - 1}.") + # Click the selected visible option + visible_options[value].click() + else: + raise ValueError(f"Invalid method '{method}'. Use DropdownMethod Enum values.") + except TimeoutException: + raise Exception(f"Timeout while selecting '{value}' in dropdown {selector}.") + except Exception as e: + raise Exception(f"Error while selecting '{value}' in dropdown {selector} using method '{method}': {e}") + + def get_visible_table_rows(self, by, value): + """ + Returns a list of visible rows from a table located by the given selector. + :param by: Selenium locator strategy (e.g., By.ID). + :param value: Locator value (e.g., "table_id"). + :return: List of WebElement rows. + """ + try: + table = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + (by, value))) + tbody = table.find_element(By.TAG_NAME, "tbody") + rows = [row for row in tbody.find_elements(By.TAG_NAME, "tr") if row.is_displayed()] + return rows + except TimeoutException: + raise Exception(f"Timeout while locating table rows in table '{value}'.") + + def search_and_delete_item(self, item_name, item_id, item_type): + """ + :param item_name: The name of the item to search for. + :param item_id: The unique ID of the item to delete. + :param item_type: The type of the item to delete (e.g., "questionnaire", "bundle", "clinic"). + """ + try: + # Initialize variables based on the item type + if item_type == "questionnaire": + self.navigator.navigate_to_manage_questionnaires() + search_box_selector = SearchBoxSelectors.QUESTIONNAIRE + button_id = RemoveButtonSelectors.QUESTIONNAIRE.format(item_id) + elif item_type == "bundle": + self.navigator.navigate_to_manage_bundles() + search_box_selector = SearchBoxSelectors.BUNDLE + button_id = RemoveButtonSelectors.BUNDLE.format(item_id) + elif item_type == "clinic": + self.navigator.navigate_to_manage_clinics() + search_box_selector = SearchBoxSelectors.CLINIC + button_id = RemoveButtonSelectors.CLINIC.format(item_id) + else: + raise ValueError(f"Unknown item type: {item_type}") + + # Search for the element using the appropriate search box + self.fill_text_field(search_box_selector, item_name) + + # click the remove button + self.click_element((By.ID, button_id)) + except TimeoutException: + raise Exception(f"Failed to delete {item_type} '{item_name}' with ID {item_id}'.") + except Exception as e: + raise Exception(f"An error occurred while deleting {item_type} '{item_name}': {e}") + + def handle_popup_alert(self, accept=True, timeout=2): + """ + :param accept: Boolean indicating whether to accept (True) or dismiss (False) the alert. + :param timeout: Time to wait for the alert to appear. + """ + try: + WebDriverWait(self.driver, timeout).until(EC.alert_is_present()) + alert = self.driver.switch_to.alert + if accept: + alert.accept() + else: + alert.dismiss() + except TimeoutException: + pass # No alert appeared + + def set_value(self, field_name, steps=1): + """ + :param field_name: The name of the input field. + :param steps: Number of times to press the ARROW_UP key (default is 1). + """ + value_input = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + (By.NAME, field_name))) + value_input.click() + for _ in range(steps): + value_input.send_keys(Keys.ARROW_UP) + + + def drag_and_drop(self, source_selector, target_selector): + """ + :param source_selector: A tuple (By, value) identifying the source element to be dragged. + :param target_selector: A tuple (By, value) identifying the target element where the source should be dropped. + :raises TimeoutException: If either the source or target element is not found within the timeout. + """ + # Wait for the source and target elements to be present + source_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + source_selector)) + target_element = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + target_selector)) + + # Scroll the elements into view to ensure they are interactable + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", source_element) + self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", target_element) + + # Perform the drag-and-drop action + ActionChains(self.driver).drag_and_drop(source_element, target_element).perform() + + def search_item(self, item_name, item_type): + """ + :param item_name: The name of the item to search for. + :param item_type: The type of the item to delete (e.g., "questionnaire", "bundle", "clinic"). + """ + try: + # Initialize variables based on the item type + if item_type == "questionnaire": + self.navigator.navigate_to_manage_questionnaires() + search_box_selector = SearchBoxSelectors.QUESTIONNAIRE + elif item_type == "bundle": + self.navigator.navigate_to_manage_bundles() + search_box_selector = SearchBoxSelectors.BUNDLE + elif item_type == "clinic": + self.navigator.navigate_to_manage_clinics() + search_box_selector = SearchBoxSelectors.CLINIC + else: + raise ValueError(f"Unknown item type: {item_type}") + + # Search for the element using the appropriate search box + self.fill_text_field(search_box_selector, item_name) + + except Exception as e: + raise Exception(f"An error occurred while searching for {item_type} '{item_name}'") + + def check_visibility_of_element(self, selector, error_message): + """ + :param selector: A tuple representing the element locator (e.g., (By.ID, "element_id")). + :param error_message: The error message to display when the element is not visible. + """ + try: + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(selector)) + except TimeoutException: + raise Exception(error_message) \ No newline at end of file diff --git a/selenium/helper/Survey.py b/selenium/helper/Survey.py new file mode 100644 index 00000000..21671b74 --- /dev/null +++ b/selenium/helper/Survey.py @@ -0,0 +1,290 @@ +from selenium.common import TimeoutException +from selenium.webdriver.chrome.webdriver 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.support.ui import Select + +from helper.Navigation import NavigationHelper +from helper.Question import QuestionType +from helper.SeleniumUtils import SeleniumUtils, DropdownMethod + + +class SurveySelectors: + BUTTON_SHOW_BUNDLES = (By.ID, "showBundles") + BUTTON_START_SURVEY = (By.ID, "startSurveyButton") + BUTTON_NEXT_QUESTION = (By.ID, "buttonNext") + BUTTON_CHECK_CASE = (By.ID, "checkButton") + + DROPDOWN_CLINIC_SELECTION = (By.ID, "clinic-selection") + DROPDOWN_BUNDLE_SELECTION = (By.ID, "bundle-selection") + DROPDOWN_LANGUAGE_SELECTION = (By.ID, "bundle-language-selection") + DROPDOWN_GENERIC = (By.ID, "dropDown") + + INPUT_CASE_NUMBER = (By.ID, "caseNumber") + INPUT_NUMBER = (By.ID, "numberInput") + INPUT_DATE = (By.ID, "dateInput") + INPUT_TEXTAREA = (By.ID, "textarea") + + RADIO_REGISTER = (By.ID, "register") + RADIO_SEARCH_HIS = (By.ID, "isHIS") + RADIO_PSEUDONYMIZATION = (By.ID, "pseudonym") + + SLIDER = (By.ID, "range") + + TEXT_QUESTION_CONTENT = (By.ID, "questionContent") + TEXT_QUESTION_TITLE = (By.ID, "questionTitle") + + LABEL_FOR_CHECKBOX = lambda selected_value: (By.CSS_SELECTOR, f"label[for='numberedCheckbox_{selected_value}']") + LABEL_BY_OPTION_TEXT = lambda option_text: (By.XPATH, f"//div[@class='right' and text()='{option_text}']/..") + +class SurveyHelper: + + # Default values for survey interactions + DEFAULT_LANGUAGE_CODE = "de_DE" + DEFAULT_FREETEXT = "Default answer for freetext questions" + DEFAULT_SLIDER_POSITION = 0.5 + DEFAULT_DATE = "1970-01-01" + + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.utils = SeleniumUtils(driver) + self.navigation_helper = navigation_helper + + def click_next_button(self): + self.utils.click_element(SurveySelectors.BUTTON_NEXT_QUESTION) + + def start_survey(self, configuration, clinic_name, case_number): + """ + Starts a survey as a user by interacting with the clinic selection, + configuration options, and case number input. + + :param configuration: The configuration to select (e.g., "searchHIS" or "pseudonym"). + :param clinic_name: The name of the clinic to select. + :param case_number: The case number to input. + """ + try: + # Select clinic + clinic_dropdown = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(SurveySelectors.DROPDOWN_CLINIC_SELECTION) + ) + + # Check if the dropdown is disabled + is_disabled = clinic_dropdown.get_attribute("disabled") + if is_disabled: + # Validate that the preselected clinic matches the expected clinic + selected_option = clinic_dropdown.find_element(By.CSS_SELECTOR, "option[selected]") + selected_clinic = selected_option.text + assert selected_clinic == clinic_name, f"Expected clinic '{clinic_name}', but got '{selected_clinic}'" + else: + # Select the clinic from the dropdown + self.utils.select_dropdown(SurveySelectors.DROPDOWN_CLINIC_SELECTION, clinic_name, DropdownMethod.VISIBLE_TEXT) + + # Select configuration + config_selector = configuration['config_selector'] + self.utils.click_element(config_selector) + + # Input case number + self.utils.fill_text_field(SurveySelectors.INPUT_CASE_NUMBER, case_number) + + # Click check button + self.utils.click_element(SurveySelectors.BUTTON_CHECK_CASE) + + except TimeoutException: + raise Exception("Timeout while starting the survey process.") + except Exception as e: + raise Exception(f"Error while starting survey: {e}") + + def complete_survey(self): + self.click_next_button() + + # Wait for the survey completion message to appear + WebDriverWait(self.driver, 10).until(EC.presence_of_element_located( + SurveySelectors.TEXT_QUESTION_CONTENT)) + + # Validate that the survey completion page is displayed + completion_content = self.driver.find_element(*SurveySelectors.TEXT_QUESTION_CONTENT).text + + # Optional: Return the completion content for further validation or logging + return completion_content + + def proceed_to_bundle_selection(self, bundle_name, language_code=None): + """ + :param bundle_name: The name of the bundle to select. + :param language_code: The code of the language to select (e.g., "de_DE") (optional). + """ + language_code = language_code or self.DEFAULT_LANGUAGE_CODE + try: + # Click "Show Bundles" button to proceed + self.utils.click_element(SurveySelectors.BUTTON_SHOW_BUNDLES) + + # Select bundle + self.utils.select_dropdown(SurveySelectors.DROPDOWN_BUNDLE_SELECTION, bundle_name, DropdownMethod.VISIBLE_TEXT) + + # Select language if provided + if language_code: + self.utils.select_dropdown(SurveySelectors.DROPDOWN_LANGUAGE_SELECTION, language_code, DropdownMethod.VALUE) + + # Start the survey + self.utils.click_element(SurveySelectors.BUTTON_START_SURVEY) + + except TimeoutException: + raise Exception("Timeout while proceeding to bundle selection.") + except Exception as e: + raise Exception(f"Error while proceeding to bundle selection: {e}") + + def answer_multiple_choice_question(self, question): + """ + :param question: A dictionary containing the question details. + """ + options = question.get("options") + min_answers = question.get("min_answers") + max_answers = question.get("max_answers") + + # Validate min_answers and max_answers + if min_answers < 0 or max_answers > len(options) or min_answers > max_answers: + raise ValueError("Invalid min_answers or max_answers values.") + + # Select at least min_answers options, but not more than max_answers + for i, option_text in enumerate(options[:max_answers]): + label = self.driver.find_element(*SurveySelectors.LABEL_BY_OPTION_TEXT(option_text)) + label.click() + + def answer_slider_question(self, question, slider_position=None): + """ + :param question: The question dictionary containing slider configuration. + :param slider_position: A float value between 0 and 1 that determines the slider position. + """ + slider_position = slider_position or self.DEFAULT_SLIDER_POSITION + + # Ensure slider_position is between 0 and 1 + if not (0 <= slider_position <= 1): + raise ValueError("slider_position must be a value between 0 and 1.") + + # Extract slider properties from the question + min_value = question.get("min_value") + max_value = question.get("max_value") + + # Calculate the target value for the slider + target_value = min_value + (slider_position * (max_value - min_value)) + + # Set the slider value in the DOM + self.driver.execute_script("document.getElementById('range').value=arguments[0];", target_value) + + # Trigger events to simulate user interaction with the slider + slider_element = self.driver.find_element(*SurveySelectors.SLIDER) + self.driver.execute_script("arguments[0].dispatchEvent(new Event('mousedown'));", slider_element) + self.driver.execute_script("arguments[0].dispatchEvent(new Event('mouseup'));", slider_element) + + def answer_number_checkbox_question(self, question, selected_value=None): + """ + :param question: The question dictionary containing numeric checkbox configuration. + :param selected_value: A numeric value to select, between min_value and max_value. + """ + # Extract the min, max, and step values from the question + min_value = question.get("min_value") + max_value = question.get("max_value") + step_size = question.get("step_size") + selected_value = selected_value or min_value + + if not (min_value <= selected_value <= max_value): + raise ValueError(f"Value {selected_value} is not within the allowed range {min_value} to {max_value}.") + if (selected_value - min_value) % step_size != 0: + raise ValueError(f"Value {selected_value} does not match the step size {step_size}.") + + # Find and select the checkbox + label_element = self.driver.find_element(*SurveySelectors.LABEL_FOR_CHECKBOX(selected_value)) + label_element.click() + + def answer_text_question(self, text=None): + """ + :param text: The text to input. Defaults to `DEFAULT_FREETEXT`. + """ + text = text or self.DEFAULT_FREETEXT + self.driver.find_element(*SurveySelectors.INPUT_TEXTAREA).send_keys(text) + + def answer_number_checkbox_text_question(self, question, selected_value=None, text=None): + """ + :param question: A dictionary containing numeric checkbox configuration (min/max/step values). + :param selected_value: The numeric value to select. Defaults to the minimum value. + :param text: The text to input in the associated text field. Defaults to `DEFAULT_FREETEXT`. + """ + self.answer_number_checkbox_question(question, selected_value) + self.answer_text_question(text) + + def answer_date_question(self, question, date=None): + """ + :param question: A dictionary containing question details. The function uses a default date if none is specified. + :param date: A string representing the date to set (e.g., "2024-12-20"). + """ + date = date or self.DEFAULT_DATE + self.driver.execute_script("document.getElementById('dateInput').valueAsDate = new Date(arguments[0]);", date) + self.driver.execute_script("document.getElementById('dateInput').dispatchEvent(new Event('blur'));") + + def select_dropdown_option(self, question, option_text=None): + """ + :param question: A dictionary containing the dropdown question details, including available options. + :param option_text: The visible text of the dropdown option to select. If not provided, + the first option from the "options" list in the question will be selected. + """ + options = question.get("options") + option_text = option_text or options[0] + + Select(self.driver.find_element(*SurveySelectors.DROPDOWN_GENERIC)).select_by_visible_text(option_text) + + def answer_numbered_input_question(self, question, value=None): + """ + :param question: The question dictionary containing the numbered input configuration. + :param value: The numeric value to input. Defaults to the minimum value from the question configuration. + """ + # Extract constraints from the question + min_value = question.get("min_value", 0) + max_value = question.get("max_value", 10) + step_size = question.get("step_size", 1) + + # Use default value if not explicitly provided + if value is None: + value = min_value + + + # Locate the input element and set the value + input_element = self.driver.find_element(*SurveySelectors.INPUT_NUMBER) + input_element.clear() + input_element.send_keys(str(value)) + + # Trigger the `onchange` event if necessary + self.driver.execute_script("arguments[0].dispatchEvent(new Event('change'));", input_element) + + def end_survey(self): + # Get the current URL before clicking the "Next" button + current_url = self.driver.current_url + + # Click the "Next" button + self.click_next_button() + + # Wait until the URL changes + WebDriverWait(self.driver, 15).until( + lambda driver: driver.current_url != current_url, + message="URL did not change after clicking 'Next' button." + ) + +class SurveyAssertHelper(SurveyHelper): + + def assertion_for_welcome_text(self, text): + WebDriverWait(self.driver, 10).until( + EC.text_to_be_present_in_element( + SurveySelectors.TEXT_QUESTION_CONTENT, text)) + welcome_text = self.driver.find_element(*SurveySelectors.TEXT_QUESTION_CONTENT).text + assert text in welcome_text, "Welcome text not found!" + + def assertion_for_question_title(self, question): + if question["type"] == QuestionType.INFO_TEXT: + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + SurveySelectors.TEXT_QUESTION_CONTENT)) + question_text = self.driver.find_element(*SurveySelectors.TEXT_QUESTION_CONTENT).text + assert question['text'] in question_text, "Question text not found!" + else: + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located( + SurveySelectors.TEXT_QUESTION_TITLE)) + question_text = self.driver.find_element(*SurveySelectors.TEXT_QUESTION_TITLE).text + assert question['text'] in question_text, "Question title not found!" \ No newline at end of file diff --git a/selenium/helper/User.py b/selenium/helper/User.py new file mode 100644 index 00000000..1a1f30ae --- /dev/null +++ b/selenium/helper/User.py @@ -0,0 +1,138 @@ +from enum import Enum +from selenium.webdriver.chrome.webdriver 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 helper.Navigation import NavigationHelper +from helper.SeleniumUtils import SearchBoxSelectors, SeleniumUtils + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + +class UserSelector: + BUTTON_INVITE_USER = (By.ID, "newInvitationButton") + BUTTON_ADD_USER = (By.ID, "addInvitationButton") + BUTTON_MOVE_CLINIC = lambda id: (By.ID, f"{id}") + BUTTON_SEND_INVITE = (By.ID, "inviteButton") + BUTTON_EDIT_INVITE = (By.ID, "editInvite") + BUTTON_REMOVE_INVITATION = (By.CLASS_NAME, "removeInvitationButton") + BUTTON_PREVIEW = (By.ID, "previewButton") + BUTTON_MAIL_PREVIEW = (By.ID, "mailPreviewButton") + BUTTON_MAIL_TO_ALL = (By.ID, "mailButton") + + INPUT_USER_FIRST_NAME = lambda index: (By.ID, f"invitationUsers{index}.firstName") + INPUT_USER_LAST_NAME = lambda index: (By.ID, f"invitationUsers{index}.lastName") + INPUT_USER_EMAIL = lambda index: (By.ID, f"invitationUsers{index}.email") + INPUT_PERSONAL_TEXT = (By.ID, "personalText") + INPUT_CSV = (By.CLASS_NAME, "file-input") + INPUT_SUBJECT = (By.ID, "subject") + INPUT_MESSAGE = (By.ID, "mailContent") + + SELECT_USER_ROLE = (By.ID, "userRoleSelect") + SELECT_USER_LANGUAGE = (By.ID, "userLanguageSelect") + SELECT_MAIL_LANGUAGE = (By.ID, "language") + + TABLE_USERS = (By.ID, "userTable") + TABLE_INVITAIONS = (By.ID, "invitationTable") + TABLE_ACTION_BUTTONS= (By.CSS_SELECTOR, "td.actionColumn") + TABLE_AVAILABLE_CLINICS = (By.ID, "availableClinicsTable") + TABLE_ASSIGNED_CLINICS = (By.ID, "assignedClinicsTable") + + PAGINATION_USER_TABLE = (By.ID, "userTable_paginate") + PAGINATION_INVITATION_TABLE = (By.ID, "invitationTable_paginate") + + DIV_PREVIEW = (By.ID, "previewDiv") + DIV_PREVIEW_MAIL = (By.ID, "preview") + + +class UserRoles(Enum): + ADMIN = "ROLE_USER" + USER = "ROLE_USER" + MODERATOR = "ROLE_MODERATOR" + ENCOUNTERMANAGER = "ROLE_ENCOUNTERMANAGER" + EDITOR = "ROLE_EDITOR" + +class UserHelper: + def __init__(self, driver: WebDriver, navigation_helper: NavigationHelper): + self.driver = driver + self.navigation_helper = navigation_helper + self.utils = SeleniumUtils(driver) + + def invite_user(self, users, role=UserRoles.USER, language="DEUTSCH", invite_message=None, clinic=None): + """ + :param users: An array of user json objects like {"first_name": str, "last_name":str, "email":str}. + :param role: The role of the user to be invited. + :param language: The language of the invitation email. + :param invite_message: The message to be sent with the invitation. + :param clinic: The array of clinics to which the user is invited. + """ + try: + self.navigation_helper.navigate_to_manager_user() + + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(UserSelector.BUTTON_INVITE_USER) + ) + + self.utils.click_element(UserSelector.BUTTON_INVITE_USER) + + for index in range(len(users)): + try: + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(UserSelector.INPUT_USER_FIRST_NAME(index)) + ) + self.utils.fill_text_field(UserSelector.INPUT_USER_FIRST_NAME(index), users[index]['first_name']) + self.utils.fill_text_field(UserSelector.INPUT_USER_LAST_NAME(index), users[index]['last_name']) + self.utils.fill_text_field(UserSelector.INPUT_USER_EMAIL(index), users[index]['email']) + self.utils.click_element(UserSelector.BUTTON_ADD_USER) + except Exception as e: + raise Exception(f"Failed to fill user data: {e}") + + self.utils.select_dropdown(UserSelector.SELECT_USER_ROLE, role.value) + self.utils.select_dropdown(UserSelector.SELECT_USER_LANGUAGE, language) + if invite_message: + self.utils.fill_text_field(UserSelector.INPUT_PERSONAL_TEXT, invite_message) + + if clinic: + try: + for c in clinic: + self.utils.click_element(UserSelector.BUTTON_MOVE_CLINIC(c)) + except Exception as e: + raise Exception(f"Failed to move assign clinics to user while inviting: {e}") + + self.utils.click_element(UserSelector.BUTTON_INVITE_USER) + + except Exception as e: + raise Exception(f"Failed to invite user: {e}") + + def send_invite(self, users): + """ + :param users: An array of user json objects like {"first_name": str, "last_name":str, "email":str}. + """ + try: + # send the invite + self.utils.click_element(UserSelector.BUTTON_SEND_INVITE) + + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.ID, "invitationTable")) + ) + invite_ids = [] + for user in users: + # Search for the bundle by name + self.utils.fill_text_field(SearchBoxSelectors.INVITATION, user['email']) + + # Extract the bundle ID from the link + edit_link = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((UserSelector.BUTTON_EDIT_INVITE)) + ) + invite_id = edit_link.get_attribute("href").split("id=")[1] + if invite_id: + invite_ids.append(invite_id) + + return invite_ids + + except Exception as e: + raise Exception(f"Error while inviting users: {e}") \ No newline at end of file diff --git a/selenium/helper/test_images/test_logo.png b/selenium/helper/test_images/test_logo.png new file mode 100644 index 00000000..2a2d3ef3 Binary files /dev/null and b/selenium/helper/test_images/test_logo.png differ diff --git a/selenium/helper/test_images/test_upload_image.png b/selenium/helper/test_images/test_upload_image.png new file mode 100644 index 00000000..3a625bdb Binary files /dev/null and b/selenium/helper/test_images/test_upload_image.png differ diff --git a/selenium/secrets/localhost8080.json b/selenium/secrets/localhost8080.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/selenium/secrets/localhost8080.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/selenium/selenium_tests.py b/selenium/selenium_tests.py index 722cd304..a433559d 100644 --- a/selenium/selenium_tests.py +++ b/selenium/selenium_tests.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from time import gmtime, strftime +import datetime import unittest import json import os @@ -13,7 +14,22 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC + +from helper.Authentication import AuthenticationHelper, AuthenticationAssertHelper +from helper.Bundle import BundleHelper, BundleSelectors +from helper.Clinic import ClinicHelper, ClinicSelectors +from helper.Condition import ConditionHelper, ConditionSelectors, ConditionAssertHelper +from helper.Configuration import ConfigurationHelper, ConfigurationSelectors +from helper.Encounter import EncounterHelper, EncounterSelectors, EncounterScheduleType from helper.Login import LoginHelper +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper, QuestionAssertHelper, QuestionType +from helper.Questionnaire import QuestionnaireHelper, QuestionnaireAssertHelper +from helper.Score import ScoreHelper, ScoreAssertHelper +from helper.SeleniumUtils import SeleniumUtils, ErrorSelectors +from helper.Survey import SurveyHelper, SurveyAssertHelper +from helper.Language import LanguageSelectors, LanguageHelper +from helper.User import UserHelper, UserRoles, UserSelector, EmailSelectors loginHelper = LoginHelper() @@ -117,11 +133,37 @@ def _setLocalDriver(self, directory): class CustomTest(IMISeleniumBaseTest): + + def setUp(self): + super().setUp() + + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, self.navigation_helper) + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.configuration_helper = ConfigurationHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, self.navigation_helper) + self.question_assert_helper = QuestionAssertHelper(self.driver, self.navigation_helper) + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + self.authentication_assert_helper = AuthenticationAssertHelper(self.driver) + self.questionnaire_assert_helper = QuestionnaireAssertHelper(self.driver, self.navigation_helper) + self.score_assert_helper = ScoreAssertHelper(self.driver, self.navigation_helper) + self.condition_assert_helper = ConditionAssertHelper(self.driver, self.navigation_helper) + self.language_helper = LanguageHelper(self.driver, self.navigation_helper) + self.encounter_helper = EncounterHelper(self.driver, self.navigation_helper) + def test_login_admin(self): if(self.secret['admin-username']!='' and self.secret['admin-password']!=''): self.driver.get(self.https_base_url) - loginHelper.login(self.driver,self.secret['admin-username'], self.secret['admin-password']) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) WebDriverWait(self.driver, 10).until(EC.url_to_be(self.https_base_url + "admin/index")) @@ -132,7 +174,764 @@ def test_login_admin(self): assert True else: pass + + def test_admin_interface_login(self): + self.driver.get(self.https_base_url) + # a + self.authentication_assert_helper.assert_mobile_user_login() + # b + self.authentication_assert_helper.assert_mobile_user_password() + + def test_admin_interface_index(self): + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # c + self.authentication_assert_helper.assert_admin_index() + + self.authentication_helper.logout() + + def test_admin_interface_questionnaire_question_types_score(self): + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + self.navigation_helper.navigate_to_manage_questionnaires() + + # d + self.questionnaire_assert_helper.assert_questionnaire_list() + # e + self.questionnaire_helper.click_add_questionnaire_button() + questionnaire = self.questionnaire_assert_helper.assert_questionnaire_fill_page() + # f + self.navigation_helper.search_and_open_questionnaire(questionnaire['name']) + self.questionnaire_helper.save_questionnaire_edit_question() + self.questionnaire_helper.click_add_question_button() + self.question_assert_helper.assert_question_fill_page() + # f (question types) + # TODO [LJ] implement for all types + question_list = list() + excluded_question_types = {QuestionType.IMAGE} + question_types = [question_type for question_type in QuestionType if + question_type not in excluded_question_types] + for question_type in question_types: + self.questionnaire_helper.click_add_question_button() + question_by_type = self.question_assert_helper.assert_question_by_type(question_type) + question_list.append(question_by_type) + # g + self.question_assert_helper.assert_question_table_functionality(len(question_list)) + # h + self.navigation_helper.navigate_to_scores_of_questionnaire(questionnaire['id'], questionnaire['name']) + self.score_assert_helper.assert_scores_list() + # i + self.navigation_helper.navigate_to_scores_of_questionnaire(questionnaire['id'], questionnaire['name']) + self.score_assert_helper.assert_score_fill() + + self.authentication_helper.logout() + + def test_admin_interface_conditions(self): + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # j + # Create source and target questionnaires with specific question types and add the questionnaires to a bundle to enable selection as a condition target + source_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions(question_types={QuestionType.SLIDER, QuestionType.MULTIPLE_CHOICE, QuestionType.DROP_DOWN}) + target_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions(question_types={QuestionType.INFO_TEXT}) + bundle = self.bundle_helper.create_bundle(questionnaires=[source_questionnaire, target_questionnaire]) + bundle['id'] = self.bundle_helper.save_bundle(bundle['name']) + + threshold_supported_question_types = {QuestionType.SLIDER, QuestionType.NUMBER_CHECKBOX, QuestionType.NUMBER_INPUT} + + # Select a question where condition can be added with threshold value from the source questionnaire + threshold_condition_question = next((question for question in source_questionnaire['questions'] + if question['type'] in threshold_supported_question_types), None) + + # Navigate to the source questionnaire's questions and reorder the slider question to the first position + self.navigation_helper.navigate_to_questions_of_questionnaire(source_questionnaire['id'], source_questionnaire['name']) + self.questionnaire_helper.reorder_question(threshold_condition_question['id'], 0) + # Open the condition page for the question and add conditions targeting question, answer, and questionnaire + self.condition_helper.open_conditions_of_question(threshold_condition_question['id']) + condition_id_1 = self.condition_helper.add_condition_for_threshold_questions(threshold_steps=1, target_type=ConditionSelectors.TargetType.QUESTION) + condition_id_2 = self.condition_helper.add_condition_for_threshold_questions(threshold_steps=2, target_type=ConditionSelectors.TargetType.ANSWER) + condition_id_3 = self.condition_helper.add_condition_for_threshold_questions(threshold_steps=3, target_type=ConditionSelectors.TargetType.QUESTIONNAIRE) + + # Assert the conditions are correctly listed in the tables + self.condition_assert_helper.assert_condition_list_and_search_de() + self.condition_helper.delete_condition(condition_id_1) + self.condition_helper.delete_condition(condition_id_2) + self.condition_helper.delete_condition(condition_id_3) + self.condition_helper.navigate_back_to_questions_of_questionnaire() + + # k + self.condition_assert_helper.assert_add_condition_page(source_questionnaire) + + self.authentication_helper.logout() + + def test_bundle_list(self): + bundle={} + created_questionnaire={} + clinic={} + + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + except Exception as e: + self.fail(f"Failed to navigate to 'Bundles' page: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.TABLE_BUNDLE, "Bundle table not found") + + try: + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle['id']=self.bundle_helper.save_bundle(bundle_name=bundle['name']) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.CELL_FLAGICON, "Flag icon not found") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"] = self.clinic_helper.create_clinic(bundles=[bundle], + configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}]) + clinic['id']=self.clinic_helper.save_clinic(clinic_name=clinic['name']) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + # Assert - Find clinics assigned to the bundle + try: + self.navigation_helper.navigate_to_manage_bundles() + self.utils.search_item(bundle["name"], "bundle") + bundle_row = self.bundle_helper.get_first_bundle_row() + bundle_row.find_element(By.CSS_SELECTOR, "td:nth-child(3)") + clinic_link = bundle_row.find_element(By.CSS_SELECTOR, "ul > li > a") + self.assertEqual(clinic_link.text, clinic["name"], f"Clinic name '{clinic["name"]}' not found in bundle row.") + + except Exception as e: + self.fail(f"Failed to find clinic assigned to bundle: {e}") + + self.utils.check_visibility_of_element(BundleSelectors.INPUT_BUNDLE_SEARCH, "Bundle table search box not found") + self.utils.check_visibility_of_element(BundleSelectors.PAGINATION_BUNDLE, "Bundle table pagination not found") + self.utils.check_visibility_of_element(BundleSelectors.BUTTON_ADD_BUNDLE, "Bundle add button not found") + + try: + pass + finally: + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + self.authentication_helper.logout() + + def test_bundle_fill(self): + created_questionnaire = {} + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + except Exception as e: + self.fail(f"Failed to navigate to 'Bundles' page: {e}") + + self.utils.click_element(BundleSelectors.BUTTON_ADD_BUNDLE) + self.language_helper.open_language_dropdown() + self.utils.check_visibility_of_element(LanguageSelectors.LANGUAGE_DROPDOWN, "Failed to open language dropdown") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_NAME, "Failed to locate bundle input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_EDITABLE_DESCRIPTION, "Failed to locate bundle description input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_WELCOME_TEXT, "Failed to locate welcome input") + self.utils.check_visibility_of_element(BundleSelectors.INPUT_END_TEXT, "Failed to locate end input") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_PUBLISH, "Failed to locate publish checkbox") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_NAME_PROGRESS, "Failed to locate name progress checkbox") + self.utils.check_visibility_of_element(BundleSelectors.CHECKBOX_PROGRESS_WHOLE_PACKAGE, "Failed to locate progress whole package checkbox") + self.utils.check_visibility_of_element(BundleSelectors.TABLE_AVAILABLE_QUESTIONNAIRES, "Available questionnaires table not found") + self.utils.check_visibility_of_element(BundleSelectors.TABLE_ASSIGNED_QUESTIONNAIRES, "Assigned questionnaires table not found") + + #Assert - Test for assigning questionnaire to bundle + try: + self.bundle_helper.assign_multiple_questionnaires_to_bundle([created_questionnaire]) + except Exception as e: + self.fail(f"Failed to assign questionnaire to bundle: {e}") + + #Assert - Test for removing questionnaire to bundle + try: + self.bundle_helper.remove_multiple_questionnaires_from_bundle([created_questionnaire]) + except Exception as e: + self.fail(f"Failed to assign questionnaire to bundle: {e}") + + #Assert - Check form validation + try: + self.utils.click_element(BundleSelectors.BUTTON_SAVE) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + self.assertEqual(len(validation_errors), 2, "Expected 2 validation errors, but found {len(validation_errors)}") + except Exception as e: + self.fail(f"Failed to save bundle: {e}") + + #Finally + finally: + if(created_questionnaire): + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + self.authentication_helper.logout() + + def test_clinic_list(self): + clinic={} + + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + self.navigation_helper.navigate_to_manage_clinics() + except Exception as e: + self.fail(f"Failed to navigate to 'Clinic' page: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"] = self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}]) + clinic['id']=self.clinic_helper.save_clinic(clinic_name=clinic['name']) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_CLINIC, "Clinic table not found") + self.utils.check_visibility_of_element(ClinicSelectors.PAGINATION_CLINIC_TABLE, "Clinic table pagination not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_SEARCH, "Clinic table search not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ACTION_BUTTONS, "Clinic table action buttons not found") + self.utils.check_visibility_of_element(ClinicSelectors.BUTTON_ADD_CLINIC, "Add new clinic button not found") + + try: + pass + finally: + self.utils.search_and_delete_item(clinic["name"],clinic["id"],"clinic") + self.authentication_helper.logout() + + def test_clinic_fill(self): + clinic={} + created_questionnaire={} + bundle={} + + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + #Arrange + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + self.navigation_helper.navigate_to_manage_bundles() + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle['id']=self.bundle_helper.save_bundle(bundle_name=bundle['name']) + except Exception as e: + self.fail(f"Failed to setup questionnaire and bundle: {e}") + + self.navigation_helper.navigate_to_manage_clinics() + WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(ClinicSelectors.BUTTON_ADD_CLINIC)) + self.utils.click_element(ClinicSelectors.BUTTON_ADD_CLINIC) + + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_CLINIC_NAME, "Clinic name input not found") + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_EDITABLE_DESCRIPTION, "Clinic description input not found") + self.utils.check_visibility_of_element(ClinicSelectors.INPUT_CLINIC_EMAIL, "Clinic email input not found") + + #Assert - Check if the clinic configuration is displayed + try: + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ClinicSelectors.DIV_CLINIC_CONFIGURATION) + ) + clinic_configuration = self.driver.find_element(*ClinicSelectors.DIV_CLINIC_CONFIGURATION) + clinic_configuration_list = clinic_configuration.find_elements(*ClinicSelectors.CLINIC_CONFIGURATION_LIST) + self.assertGreaterEqual(len(clinic_configuration_list), 1, "Clinic configuration list should have at least one item") + except: + self.fail( + f"Clinic configuration not found") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_AVAIALBLE_BUNDLES, "Available bundles table not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ASSIGNED_BUNDLES, "Assigned bundles table not found") + + + #Assert - Check if the bundles can be added to the clinic + try: + self.clinic_helper.assign_multiple_bundes_to_clinic([{'id': bundle["id"], 'name': bundle["name"]}]) + except Exception as e: + self.fail(f"Failed to assign bundle to clinic: {e}") + + #Assert - Check if the bundles can be removed from the clinic + try: + self.clinic_helper.remove_multiple_bundes_from_clinic([{'id': bundle["id"], 'name': bundle["name"]}]) + except Exception as e: + self.fail(f"Failed to remove bundle from clinic: {e}") + + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_AVAIALBLE_USERS, "Available users table not found") + self.utils.check_visibility_of_element(ClinicSelectors.TABLE_ASSIGNED_USERS, "Assigned users table not found") + + #Assert - Check if the users can be added to the clinic + try: + self.clinic_helper.assign_multiple_users_to_clinic([self.secret.get('admin-username')]) + except Exception as e: + self.fail(f"Failed to assign users to clinic: {e}") + + #Assert - Check if the users can be removed from the clinic + try: + self.clinic_helper.remove_multiple_users_from_clinic([self.secret.get('admin-username')]) + except Exception as e: + self.fail(f"Failed to remove users from clinic: {e}") + + #Assert - Check form validation + try: + self.utils.click_element(ClinicSelectors.BUTTON_SAVE) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + configuration_errors = self.driver.find_elements(*ErrorSelectors.CONFIGURATION_ERROR_SELECTOR) + self.assertEqual(len(validation_errors), 2, "Expected 2 validation errors, but found {len(validation_errors)}") + self.assertEqual(len(configuration_errors), 1, "Expected 1 configuration errors, but found {len(configuration_errors)}") + except Exception as e: + self.fail(f"Failed to save bundle: {e}") + + #Assert - Check if the clinic can be created + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic['name']=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[{'id': bundle["id"], 'name': bundle["name"]}], users=[self.secret.get('admin-username')]) + clinic['id'] = self.clinic_helper.save_clinic(clinic["name"]) + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located(ClinicSelectors.TABLE_CLINIC) + ) + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + finally: + if clinic["id"]: + self.utils.search_and_delete_item(clinic['name'],clinic["id"],"clinic") + if bundle["id"]: + self.utils.search_and_delete_item(bundle["name"],bundle["id"],"bundle") + if created_questionnaire: + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + self.authentication_helper.logout() + + def test_encounter_list(self): + created_questionnaire = {} + bundle={} + clinic={} + + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + bundle = self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle["id"]=self.bundle_helper.save_bundle(bundle["name"]) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePatientDataLookup > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[bundle]) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + # Act + self.navigation_helper.navigate_to_manage_surveys() + + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_ENCOUNTER_TABLE, "Encounter Table button not found") + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE, "Encounter Schedule Table button not found") + + # Act - Click on "All Encounters" tab + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_TABLE) + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_ALL_ENCOUNTERS, "All Encounters table not found") + + self.utils.check_visibility_of_element(EncounterSelectors.PAGINATION_ENCOUNTER_TABLE, "Pagination for All Encounters table not found") + self.utils.check_visibility_of_element(EncounterSelectors.SEARCH_ALL_ENCOUNTERS, "Search for All Encounters table not found") + + #TODO: Action column, number of exports [after create survey function implementation] + + self.utils.check_visibility_of_element(EncounterSelectors.BUTTON_EXECUTE_ENCOUNTER, "Execute Encounter button not found") + + # Act - Click on "Scheduled Encounters" tab + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_SCHEDULED_ENCOUNTERS, "Scheduled Encounters table not found") + self.utils.check_visibility_of_element(EncounterSelectors.PAGINATION_ENCOUNTER_SCHEDULE_TABLE, "Pagination for Scheduled Encounters table not found") + + self.utils.check_visibility_of_element(EncounterSelectors.SEARCH_SCHEDULED_ENCOUNTERS, "Search for Scheduled Encounters table not found") + + encounter_id = None + try: + self.utils.click_element(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + encounter_id = self.encounter_helper.schedule_encounter("123456", clinic["name"], bundle["name"], "test@email.com", EncounterScheduleType.UNIQUELY,(datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")) + except Exception as e: + self.fail(f"Failed to schedule encounter: {e}") + + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + + self.utils.check_visibility_of_element(EncounterSelectors.TABLE_ACTION_COLUMN, "Action column for Scheduled Encounters table not found") + + #TODO: number of exports [after survey schedule function implementation] + + #Assert - Check for button for scheduling an encounter + try: + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + ) + except Exception as e: + self.fail("Schedule Encounter button not found") + + finally: + self.encounter_helper.delete_scheduled_encounter(encounter_id, "123456") + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + + + def test_encounter_schedule(self): + clinic={} + bundle={} + created_questionnaire = {} + + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + try: + created_questionnaire = self.questionnaire_helper.create_questionnaire_with_questions() + except Exception as e: + self.fail(f"Failed to create questionnaire: {e}") + + try: + self.navigation_helper.navigate_to_manage_bundles() + bundle=self.bundle_helper.create_bundle(publish_bundle=True, questionnaires=[created_questionnaire]) + bundle["id"]=self.bundle_helper.save_bundle(bundle["name"]) + except Exception as e: + self.fail(f"Failed to create bundle: {e}") + + try: + self.navigation_helper.navigate_to_manage_clinics() + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePatientDataLookup > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}], + bundles=[bundle]) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + + except Exception as e: + self.fail(f"Failed to create clinic: {e}") + + try: + self.navigation_helper.navigate_to_manage_surveys() + self.utils.click_element(EncounterSelectors.BUTTON_ENCOUNTER_SCHEDULE_TABLE) + self.utils.click_element(EncounterSelectors.BUTTON_SCHEDULE_ENCOUNTER) + except Exception as e: + self.fail(f"Failed to navigate to Schedule Encounter form: {e}") + + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_SCHEDULE_CASE_NUMBER, "Case Number input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SCHEDULE_CLINIC, "Clinic select not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SCHEDULE_BUNDLE, "Bundle select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_SCHEDULE_EMAIL, "Email input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_SURVEY_TYPE, "Survey Type select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_DATE, "Date input not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_END_DATE, "End Date input not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_TIME_PERIOD, "Time Period input not found") + self.utils.check_visibility_of_element(EncounterSelectors.SELECT_LANGUAGE, "Language select not found") + self.utils.check_visibility_of_element(EncounterSelectors.INPUT_PERSONAL_TEXT, "Personal Text input not found") + + + encounter_id = None + try: + encounter_id = self.encounter_helper.schedule_encounter("123456", clinic["name"], bundle["name"], "test@email.com", EncounterScheduleType.UNIQUELY,(datetime.date.today() + datetime.timedelta(days=1)).strftime("%Y-%m-%d")) + except Exception as e: + self.fail(f"Failed to schedule encounter: {e}") + + finally: + self.encounter_helper.delete_scheduled_encounter(encounter_id, "123456") + self.utils.search_and_delete_item(clinic["name"],clinic["id"], "clinic") + self.utils.search_and_delete_item(bundle["name"],bundle["id"], "bundle") + self.utils.search_and_delete_item(created_questionnaire['name'], created_questionnaire['id'], "questionnaire") + self.authentication_helper.logout() + + def test_user_list(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_manager_user() + + self.utils.check_visibility_of_element(UserSelector.TABLE_USERS, "User list not displayed") + self.utils.check_visibility_of_element(UserSelector.PAGINATION_USER_TABLE, "Pagination not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_ACTION_BUTTONS, "Action buttons not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_INVITE_USER, "Invite user button not displayed") + + def test_invitation_edit(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + #Arrange - Create a new clinic + clinic={} + self.navigation_helper.navigate_to_manage_clinics() + + try: + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}],) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + except Exception: + self.fail("Failed to create clinic") + + #Arrange - Click on the user menu + self.navigation_helper.navigate_to_manager_user() + + self.utils.click_element(UserSelector.BUTTON_INVITE_USER) + + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_FIRST_NAME(0), "First name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_LAST_NAME(0), "Last name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_EMAIL(0), "Email input field not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_ADD_USER, "Add user button not displayed") + self.utils.click_element(UserSelector.BUTTON_ADD_USER) + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_FIRST_NAME(1), "Second user's first name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_LAST_NAME(1), "Second user's last name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_EMAIL(1), "Second user's email input field not displayed") + self.utils.click_element(UserSelector.BUTTON_REMOVE_INVITATION) + + #Assert - Check if the fields were removed + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_FIRST_NAME(0)) + ) + except Exception: + self.fail("First name input field still displayed") + + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_LAST_NAME(0)) + ) + except Exception: + self.fail("Last name input field still displayed") + + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_EMAIL(0)) + ) + except Exception: + self.fail("Email input field still displayed") + + self.utils.check_visibility_of_element(UserSelector.INPUT_CSV, "File upload button not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_USER_ROLE, "Role dropdown not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_USER_LANGUAGE, "Language dropdown not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_PERSONAL_TEXT, "Invite message input field not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_AVAILABLE_CLINICS, "Available clinic table not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_ASSIGNED_CLINICS, "Assigned clinic table not displayed") + + self.utils.click_element(UserSelector.BUTTON_MOVE_CLINIC(clinic["id"])) + + self.utils.click_element(UserSelector.BUTTON_MOVE_CLINIC(clinic["id"])) + + + #Assert - Check validations + try: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.BUTTON_SEND_INVITE) + ) + self.utils.click_element(UserSelector.BUTTON_SEND_INVITE) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + self.assertEqual(len(validation_errors), 3, "Expected 3 validation errors, but found {len(validation_errors)}") + + except Exception as e: + self.fail("Validation error not displayed") + + #Assert - Check if preview button works + try: + self.utils.fill_text_field(UserSelector.INPUT_USER_FIRST_NAME(0), "Test1") + self.utils.fill_text_field(UserSelector.INPUT_USER_LAST_NAME(0), "Test2") + self.utils.fill_text_field(UserSelector.INPUT_USER_EMAIL(0), "test@test.com") + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.BUTTON_PREVIEW) + ) + self.utils.click_element(UserSelector.BUTTON_PREVIEW) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.DIV_PREVIEW) + ) + except Exception as e: + self.fail("Preview not displayed") + finally: + if clinic["id"]: + self.utils.search_and_delete_item(clinic["name"], clinic["id"], "clinic") + + def test_user_mail_to_all(self): + test_subject = "Test Subject" + test_content = "Test Content" + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + self.navigation_helper.navigate_to_email_to_all_users() + + self.utils.check_visibility_of_element(EmailSelectors.SUBJECT_INPUT, "Subject input field not displayed") + self.utils.check_visibility_of_element(EmailSelectors.CONTENT_INPUT, "Content input field not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_MAIL_LANGUAGE, "Language dropdown not displayed") + self.utils.check_visibility_of_element(EmailSelectors.MAIL_PREVIEW_BUTTON, "Preview button not displayed") + self.utils.check_visibility_of_element(EmailSelectors.SEND_BUTTON, "Send button not displayed") + + + #Assert - Check if the preview button works + try: + self.utils.fill_text_field(EmailSelectors.SUBJECT_INPUT, test_subject) + self.utils.fill_text_field(EmailSelectors.CONTENT_INPUT, test_content) + self.utils.click_element(EmailSelectors.MAIL_PREVIEW_BUTTON) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.DIV_PREVIEW_MAIL) + ) + except Exception as e: + self.fail("Preview not displayed") + + self.authentication_helper.logout() + + def test_invitation_list(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_manage_invitations() + self.utils.check_visibility_of_element(UserSelector.TABLE_INVITAIONS, "User list not displayed") + self.utils.check_visibility_of_element(UserSelector.PAGINATION_INVITATION_TABLE, "Pagination not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_INVITE_USER, "Invite user button not displayed") + + self.authentication_helper.logout() + + def test_configuration_edit(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_configuration() + + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_LANGUAGE, "Select Language not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_CASE_NUMBER_TYPE, "Case Number Type input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_STORAGE_PATH_FOR_UPLOADS, "Storage Path for Uploads input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_BASE_URL, "Base URL input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_PATH_UPLOAD_IMAGES, self.configuration_helper.DEFAUL_EXPORT_IMAGE_UPLOAD_PATH) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PATH_MAIN_DIRECTORY, "Path for Main Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_AD_AUTH) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_AD, "URL AD input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_DOMAIN_AD, "Domain AD input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_DEFAULT_LANGUAGE_AD, "Default Language AD select not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_NUMBER_AD, "Phone Number AD input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_PATIENT_LOOKUP) + self.utils.check_visibility_of_element(ConfigurationSelectors.SELECT_PATIENT_LOOKUP_IMPLEMENTATION, "Patient Lookup Implementation select not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HOST_PATIENT_LOOKUP, "Host Patient Lookup input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_PORT_PATIENT_LOOKUP, self.configuration_helper.DEFAULT_PORT_HL7_PATIENT_LOOKUP) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PORT_PATIENT_LOOKUP, "Port Patient Lookup input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PSEUDONYMIZATION_URL, "Pseudonymization URL input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PSEUDONYMIZATION_API_KEY, "Pseudonymization API Key input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_OID, "OID input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_ODM_TO_PDF, "URL ODM to PDF input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SYSTEM_URI_FOR_FHIR, "System URI for FHIR input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_EXPORT_PATH_ORBIS, self.configuration_helper.DEFAULT_EXPORT_PATH) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_HL7_DIRECTORY, "Export Path HL7 Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_VIA_SERVER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HL7_HOST, "HL7 Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_HL7_PORT, "HL7 Port input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SENDING_FACILITY, "Sending Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_RECEIVING_APPLICATION, "Receiving Application input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_RECEIVING_FACILITY, "Receiving Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_FILE_ORDER_NUMBER, "File Order Number input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENCRYPT_MESSAGE_TLS) + self.utils.check_visibility_of_element(ConfigurationSelectors.FILE_PATH_CERTIFICATE, "File Path Certificate input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_USE_CERTIFICATE) + self.utils.check_visibility_of_element(ConfigurationSelectors.FILE_PKCS_ARCHIVE, "File PKCS Archive input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PASSWORD_PKCS_ARCHIVE, "Password PKCS Archive input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_ODM_DIRECTORY, "Export Path ODM Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_REST) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_REST_INTERFACE, "URL REST Interface input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_HL7) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_HL7_HOST, "ODM HL7 Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_HL7_PORT, "ODM HL7 Port input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_SENDING_FACILITY, "ODM Sending Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_RECEIVING_APPLICATION, "ODM Receiving Application input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_RECEIVING_FACILITY, "ODM Receiving Facility input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_ODM_FILE_ORDER_NUMBER, "ODM File Order Number input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_FHIR_DIRECTORY, "Export Path FHIR Directory input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_COMMUNICATION_SERVER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_FHIR_HOST, "FHIR Host input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_INTO_DIRECTORY) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EXPORT_PATH_REDCAP, "Export Path REDCap input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_VIA_REST) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_URL_REDCAP, "URL REDCap input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TOKEN_REDCAP, "Token REDCap input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER, "Time Delete Incomplete Encounter input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE, "Time Delete Incomplete Encounter and Not Sent Questionnaire input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_EMAIL_COMPLETED_ENCOUNTER, "Time Delete Email Completed Encounter input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTERS, "Time Delete Incomplete Scheduled Encounters input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_TIME_DELETE_INCOMPLETE_SCHEDULED_ENCOUNTER_AND_NOT_SENT_QUESTIONNAIRE, "Time Delete Incomplete Scheduled Encounter and Not Sent Questionnaire input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_UTILIZE_MAILER) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_HOST, "Mail Host input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_PORT, "Mail Port input not found") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENABLE_TLS) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_SMTP_AUTHENTICATION) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_USERNAME_MAILER, "Username Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PASSWORD_MAILER, "Password Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_SENDER_MAILER, "Sender Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_EMAIL_ADDRESS_MAILER, "Email Address Mailer input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_MAILER, "Phone Mailer input not found") + self.utils.fill_text_field(ConfigurationSelectors.INPUT_MAIL_SUPPORT, self.configuration_helper.DEFAULT_MAIL_SUPPORT) + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_MAIL_SUPPORT, "Mail Support input not found") + self.utils.check_visibility_of_element(ConfigurationSelectors.INPUT_PHONE_SUPPORT, "Phone Support input not found") + + self.configuration_helper.save_configuration() + + # Count all divs with class config_error + error_divs = self.driver.find_elements(By.CLASS_NAME, "config_error") + error_count = len(error_divs) + + # Expect 7 and throw error if less + expected_error_count = 7 + if error_count < expected_error_count: + raise AssertionError(f"Expected at least {expected_error_count} divs with class 'config_error', but found {error_count}") + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_AD_AUTH, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_PATIENT_LOOKUP, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_HL7_VIA_SERVER, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENCRYPT_MESSAGE_TLS, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_USE_CERTIFICATE, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_REST, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_ODM_VIA_HL7, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_FHIR_INTO_COMMUNICATION_SERVER, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_INTO_DIRECTORY, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_EXPORT_REDCAP_VIA_REST, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_SMTP_AUTHENTICATION, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_ENABLE_TLS, False) + self.utils.toggle_checkbox(ConfigurationSelectors.CHECKBOX_UTILIZE_MAILER, False) + + self.configuration_helper.add_additional_logo() + + self.configuration_helper.save_configuration() + self.utils.scroll_to_bottom() + self.utils.check_visibility_of_element(ConfigurationSelectors.IMAGE_ADDITIONAL_LOGO, "Additional Logo not found") + self.authentication_helper.logout() def tearDown(self): self.driver.quit() @@ -146,6 +945,7 @@ def _setServerDriver(self): options = webdriver.ChromeOptions() options.set_capability("acceptInsecureCerts", True) options.add_argument("--headless=new") + options.add_argument("--window-size=1920,1080") options.set_capability("selenoid:options", { "enableVNC": False, "enableVideo": False, diff --git a/selenium/user_test.py b/selenium/user_test.py new file mode 100644 index 00000000..b9994633 --- /dev/null +++ b/selenium/user_test.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import unittest + +from helper.User import UserSelector +from selenium import webdriver +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager + +from helper.Authentication import AuthenticationHelper +from helper.Bundle import BundleHelper +from helper.Clinic import ClinicHelper +from helper.Condition import ConditionHelper +from helper.Language import LanguageHelper +from helper.Navigation import NavigationHelper +from helper.Question import QuestionHelper +from helper.Questionnaire import QuestionnaireHelper +from helper.Score import ScoreHelper +from helper.SeleniumUtils import SeleniumUtils, ErrorSelectors +from helper.Survey import SurveyHelper, SurveyAssertHelper +from utils.imiseleniumtest import IMISeleniumBaseTest, IMISeleniumChromeTest + + +class URLPaths: + ADMIN_INDEX = "/admin/index" + ADMIN_INDEX_DE = "/admin/index?lang=de_DE" + MOBILE_USER_LOGIN = "/mobile/user/login" + MOBILE_USER_LOGIN_DE = "/mobile/user/login?lang=de_DE" + MOBILE_SURVEY_INDEX = "/mobile/survey/index" + LOGIN_BAD_CREDENTIALS = "/mobile/user/login?message=BadCredentialsException" + LOGIN_DISABLED_EXCEPTION = "/mobile/user/login?message=DisabledException" + PASSWORD_FORGOT = "/mobile/user/password" + + +class EmailSelectors: + SUBJECT_INPUT = (By.ID, "subject") + CONTENT_INPUT = (By.ID, "mailContent") + MAIL_PREVIEW_BUTTON = (By.ID, "mailPreviewButton") + SEND_BUTTON = (By.ID, "mailButton") + + +class PasswordResetSelectors: + FORGOT_PASSWORD_LINK = (By.ID, "forgotPasswordLink") + USERNAME_INPUT = (By.ID, "username") + ERROR_MESSAGE = (By.CLASS_NAME, "error") + + +class CustomTest(IMISeleniumChromeTest, unittest.TestCase): + seleniumMode: IMISeleniumBaseTest.SeleniumMode = IMISeleniumBaseTest.SeleniumMode.LOCAL + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.base_url = "localhost:8080" + cls.https_base_url = "http://localhost:8080" + + def setUp(self): + chrome_options = Options() + # chrome_options.add_argument("--headless=new") + chrome_options.add_argument("start-maximized") + driver = webdriver.Chrome(options=chrome_options, service=Service(ChromeDriverManager().install())) + # Initialize the WebDriver + self.driver = webdriver.Chrome(options=chrome_options) + + # Initialize Navigation and Utils + self.navigation_helper = NavigationHelper(self.driver) + self.utils = SeleniumUtils(self.driver, navigation_helper=self.navigation_helper) + self.navigation_helper.utils = self.utils + + # Initialize other helpers + self.authentication_helper = AuthenticationHelper(self.driver) + self.questionnaire_helper = QuestionnaireHelper(self.driver, navigation_helper=self.navigation_helper) + self.question_helper = QuestionHelper(self.driver, navigation_helper=self.navigation_helper) + self.condition_helper = ConditionHelper(self.driver, navigation_helper=self.navigation_helper) + self.score_helper = ScoreHelper(self.driver, navigation_helper=self.navigation_helper) + + self.bundle_helper = BundleHelper(self.driver, self.navigation_helper) + self.clinic_helper = ClinicHelper(self.driver, self.navigation_helper) + self.survey_helper = SurveyHelper(self.driver, self.navigation_helper) + + self.survey_assert_helper = SurveyAssertHelper(self.driver, self.navigation_helper) + self.language_helper = LanguageHelper(self.driver, self.navigation_helper) + + def test_user_list(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_manager_user() + + self.utils.check_visibility_of_element(UserSelector.TABLE_USERS, "User list not displayed") + self.utils.check_visibility_of_element(UserSelector.PAGINATION_USER_TABLE, "Pagination not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_ACTION_BUTTONS, "Action buttons not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_INVITE_USER, "Invite user button not displayed") + + def test_invitation_edit(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + #Arrange - Create a new clinic + clinic={} + self.navigation_helper.navigate_to_manage_clinics() + + try: + clinic["name"]=self.clinic_helper.create_clinic(configurations=[{'selector': (By.CSS_SELECTOR, '#usePseudonymizationService > div:nth-child(1) > div:nth-child(3) > label:nth-child(1)')}],) + clinic["id"]=self.clinic_helper.save_clinic(clinic["name"]) + except TimeoutException: + self.fail("Failed to create clinic") + + #Arrange - Click on the user menu + self.navigation_helper.navigate_to_manager_user() + + self.utils.click_element(UserSelector.BUTTON_INVITE_USER) + + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_FIRST_NAME(0), "First name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_LAST_NAME(0), "Last name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_EMAIL(0), "Email input field not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_ADD_USER, "Add user button not displayed") + self.utils.click_element(UserSelector.BUTTON_ADD_USER) + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_FIRST_NAME(1), "Second user's first name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_LAST_NAME(1), "Second user's last name input field not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_USER_EMAIL(1), "Second user's email input field not displayed") + self.utils.click_element(UserSelector.BUTTON_REMOVE_INVITATION) + + #Assert - Check if the fields were removed + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_FIRST_NAME(0)) + ) + except TimeoutException: + self.fail("First name input field still displayed") + + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_LAST_NAME(0)) + ) + except TimeoutException: + self.fail("Last name input field still displayed") + + try: + WebDriverWait(self.driver, 10).until_not( + EC.presence_of_element_located(UserSelector.INPUT_USER_EMAIL(0)) + ) + except TimeoutException: + self.fail("Email input field still displayed") + + self.utils.check_visibility_of_element(UserSelector.INPUT_CSV, "File upload button not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_USER_ROLE, "Role dropdown not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_USER_LANGUAGE, "Language dropdown not displayed") + self.utils.check_visibility_of_element(UserSelector.INPUT_PERSONAL_TEXT, "Invite message input field not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_AVAILABLE_CLINICS, "Available clinic table not displayed") + self.utils.check_visibility_of_element(UserSelector.TABLE_ASSIGNED_CLINICS, "Assigned clinic table not displayed") + + self.utils.click_element(UserSelector.BUTTON_MOVE_CLINIC(clinic["id"])) + + self.utils.click_element(UserSelector.BUTTON_MOVE_CLINIC(clinic["id"])) + + + #Assert - Check validations + try: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.BUTTON_SEND_INVITE) + ) + self.utils.click_element(UserSelector.BUTTON_SEND_INVITE) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(ErrorSelectors.INPUT_VALIDATION_SELECTOR) + ) + validation_errors = self.driver.find_elements(*ErrorSelectors.INPUT_VALIDATION_SELECTOR) + self.assertEqual(len(validation_errors), 3, "Expected 3 validation errors, but found {len(validation_errors)}") + + except TimeoutException: + self.fail("Validation error not displayed") + + #Assert - Check if preview button works + try: + self.utils.fill_text_field(UserSelector.INPUT_USER_FIRST_NAME(0), "Test1") + self.utils.fill_text_field(UserSelector.INPUT_USER_LAST_NAME(0), "Test2") + self.utils.fill_text_field(UserSelector.INPUT_USER_EMAIL(0), "test@test.com") + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.BUTTON_PREVIEW) + ) + self.utils.click_element(UserSelector.BUTTON_PREVIEW) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.DIV_PREVIEW) + ) + except TimeoutException: + self.fail("Preview not displayed") + finally: + if clinic["id"]: + self.utils.search_and_delete_item(clinic["name"], clinic["id"], "clinic") + + def test_user_mail_to_all(self): + test_subject = "Test Subject" + test_content = "Test Content" + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + self.navigation_helper.navigate_to_email_to_all_users() + + self.utils.check_visibility_of_element(EmailSelectors.SUBJECT_INPUT, "Subject input field not displayed") + self.utils.check_visibility_of_element(EmailSelectors.CONTENT_INPUT, "Content input field not displayed") + self.utils.check_visibility_of_element(UserSelector.SELECT_MAIL_LANGUAGE, "Language dropdown not displayed") + self.utils.check_visibility_of_element(EmailSelectors.MAIL_PREVIEW_BUTTON, "Preview button not displayed") + self.utils.check_visibility_of_element(EmailSelectors.SEND_BUTTON, "Send button not displayed") + + + #Assert - Check if the preview button works + try: + self.utils.fill_text_field(EmailSelectors.SUBJECT_INPUT, test_subject) + self.utils.fill_text_field(EmailSelectors.CONTENT_INPUT, test_content) + self.utils.click_element(EmailSelectors.MAIL_PREVIEW_BUTTON) + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located(UserSelector.DIV_PREVIEW_MAIL) + ) + except TimeoutException: + self.fail("Preview not displayed") + + self.authentication_helper.logout() + + def test_invitation_list(self): + # Arrange + self.driver.get(self.https_base_url) + self.authentication_helper.login(self.secret['admin-username'], self.secret['admin-password']) + + # Act + self.navigation_helper.navigate_to_manage_invitations() + self.utils.check_visibility_of_element(UserSelector.TABLE_INVITAIONS, "User list not displayed") + self.utils.check_visibility_of_element(UserSelector.PAGINATION_INVITATION_TABLE, "Pagination not displayed") + self.utils.check_visibility_of_element(UserSelector.BUTTON_INVITE_USER, "Invite user button not displayed") + + self.authentication_helper.logout() + + def tearDown(self): + if self.driver: + self.driver.quit() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/selenium/utils/imiseleniumtest.py b/selenium/utils/imiseleniumtest.py new file mode 100644 index 00000000..f7db0e62 --- /dev/null +++ b/selenium/utils/imiseleniumtest.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Script containing the base class of selenium tests. +Handles driver initialization and server/local switches. +""" +import json +import os +from abc import ABC, abstractmethod +import requests +import unittest +import inspect +from selenium import webdriver +import enum +from time import gmtime, strftime + + +# noinspection PyStatementEffect +class IMISeleniumBaseTest(ABC): + """ + Base class of all IMI selenium tests. + * Handles the initialization of the driver. + * Switches between server and local mode + """ + + + class SeleniumMode(enum.Enum): + """ + Helper to configure if selenium should run on server or locally + Default mode is AUTO + """ + SERVER = 1 # run on server + LOCAL = 2 # run local + AUTO = 3 # try server and if an error occurs switch to local + + + currentResult = None + """ Attribute used to set a cookie for validation purposes. """ + driver = None + """ Selenium driver used in the tests. """ + seleniumMode: SeleniumMode = SeleniumMode.AUTO + """ Use selenium on the server or locally? """ + + @classmethod + def setUpClass(cls) -> None: + """ + Used to initialize constants. + """ + # get filename of calling script (url) + url = os.path.basename(inspect.getouterframes(inspect.currentframe())[-1].filename)[:-3] + cls.base_url = url + cls.https_base_url = f"https://{url}" + # secret used in the subclass + cls.secret = cls._loadSecretFile(cls, url) + cls.basic_auth = cls._loadSecretFile(cls, "basic-auth") + if cls.basic_auth is not None: + cls.selenium_grid_url = f"https://{cls.basic_auth['user']}:{cls.basic_auth['passwd']}@selenoid.uni-muenster.de/wd/hub/" + else: + cls.selenium_grid_url = None + + def setUp(self) -> None: + """ + Start a new driver for each test. + Checks, if the script is called on the server or locally. + """ + if (self.seleniumMode == self.SeleniumMode.SERVER): + try: + self._setServerDriver() + except Exception as e: + print(e) + elif self.seleniumMode == self.SeleniumMode.LOCAL: + try: + # create folder for driver + directory = os.path.join(os.getcwd(), "driver") + if not os.path.exists(directory): + os.makedirs(directory) + # init driver + self._setLocalDriver(directory) + except Exception as e: + print(e) + elif self.seleniumMode == self.SeleniumMode.AUTO: + # check if script runs on server or local + try: + # if this call fails, we are local + requests.get(self.selenium_grid_url) + # init driver + self._setServerDriver() + except Exception as e: + # create folder for driver + directory = os.path.join(os.getcwd(), "driver") + if not os.path.exists(directory): + os.makedirs(directory) + # init driver + self._setLocalDriver(directory) + + # maximize window to full-screen + self.driver.maximize_window() + + def run(self, result=None) -> None: + """ + Main function of the unit test. + """ + self.currentResult = result + unittest.TestCase.run(self, result) + + def tearDown(self) -> None: + """ + Sets the cookie to validate, if the test was successful or not. + """ + if self.currentResult.wasSuccessful(): + cookie = {'name': 'zaleniumTestPassed', 'value': 'true'} + else: + cookie = {'name': 'zaleniumTestPassed', 'value': 'false'} + self.driver.add_cookie(cookie) + self.driver.quit() + + def _loadSecretFile(self, filename): + """ + Used to try loading file from server or locally. + + :param filename: file to be loaded without .json + :return: The loaded json object or None + """ + secret_server = f"/etc/selenium/{filename}.json" + if os.path.exists(secret_server): + with open(secret_server) as f: + return json.load(f) + else: + secret_local = os.path.join(os.getcwd(), "secrets", f"{filename}.json") + if os.path.exists(secret_local): + with open(secret_local) as f: + return json.load(f) + return None + + @abstractmethod + def _setServerDriver(self): + self.driver = None + + @abstractmethod + def _setLocalDriver(self, directory): + self.driver = None + + +class IMISeleniumChromeTest(IMISeleniumBaseTest): + """ + Test class for Chrome tests. + """ + def _setServerDriver(self): + name: str = f"{strftime('%Y-%m-%d-%H-%M-%S', gmtime())}_{self.base_url}_chrome" + options = webdriver.ChromeOptions() + options.set_capability("acceptInsecureCerts", True) + options.set_capability("selenoid:options", { + "enableVNC": True, + "enableVideo": True, + "enableLog": True, + "name": name, + "videoName": f"{name}.mp4", + "logName": f"{name}.log" + }) + self.driver = webdriver.Remote(options=options, + command_executor=self.selenium_grid_url) + + + def _setLocalDriver(self, directory): + # download latest driver + from selenium.webdriver.chrome.service import Service + from webdriver_manager.chrome import ChromeDriverManager + from webdriver_manager.core.driver_cache import DriverCacheManager + # init driver + self.driver = webdriver.Chrome(service=Service(ChromeDriverManager(cache_manager=DriverCacheManager(directory)).install())) + + + +class IMISeleniumFirefoxTest(IMISeleniumBaseTest): + """ + Test class for Firefox tests. + """ + def _setServerDriver(self): + name: str = f"{strftime('%Y-%m-%d-%H-%M-%S', gmtime())}_{self.base_url}_firefox" + options = webdriver.FirefoxOptions() + options.set_capability("acceptInsecureCerts", True) + options.set_capability("selenoid:options", { + "enableVNC": True, + "enableVideo": True, + "enableLog": True, + "name": name, + "videoName": f"{name}.mp4", + "logName": f"{name}.log" + }) + self.driver = webdriver.Remote(options=options, + command_executor=self.selenium_grid_url) + + def _setLocalDriver(self, directory): + # download latest driver + from webdriver_manager.firefox import GeckoDriverManager + from selenium.webdriver.chrome.service import Service + from webdriver_manager.core.driver_cache import DriverCacheManager + # init driver + self.driver = webdriver.Firefox(service=Service(GeckoDriverManager(cache_manager=DriverCacheManager(directory)).install())) + diff --git a/src/main/resources/message/messages_de_DE.properties b/src/main/resources/message/messages_de_DE.properties index 460b03f8..4f7ab896 100644 --- a/src/main/resources/message/messages_de_DE.properties +++ b/src/main/resources/message/messages_de_DE.properties @@ -693,7 +693,7 @@ question.error.duplicateQuestionMissing=Frage konnte nicht dupliziert werden. Er question.error.infoTextIsNull=Die Frage ben\u00f6tigt einen lokalisierten Info-Text question.error.isConditionTarget=Die Frage ist Ziel einer Bedingung. Folgende Elemente verhindern ihre Aktion, da sie Bedingungen ausl\u00f6sen, die diese Frage betreffen: question.error.maxNumberAnswersMissing=Maximale Anzahl von zu beantwortenden Antworten muss vergeben werden -question.error.maxNumberBiggerThanAmountOfAnswers=Maximale Anzahl von zu beantwortenden Anworten ist gr\u00f6\u00dfer als die Anzahl der vorhandenen Antworten +question.error.maxNumberBiggerThanAmountOfAnswers=Maximale Anzahl von zu beantwortenden Antworten ist gr\u00f6\u00dfer als die Anzahl der vorhandenen Antworten question.error.maxNumberSmallerThanMinNumber=Maximale Anzahl von zu beantwortenden Antworten muss mindestens so gro\u00df wie die minimale Anzahl sein question.error.minNumberAnswersMissing=Minimale Anzahl von zu beantwortenden Antworten muss vergeben werden question.error.minNumberBiggerThanAmountOfAnswers=Minimale Anzahl von zu beantwortenden Antworten ist gr\u00f6\u00dfer als die Anzahl der vorhandenen Antworten diff --git a/src/main/webapp/WEB-INF/bundle/edit.html b/src/main/webapp/WEB-INF/bundle/edit.html index 77f14ee3..35f5527b 100644 --- a/src/main/webapp/WEB-INF/bundle/edit.html +++ b/src/main/webapp/WEB-INF/bundle/edit.html @@ -71,6 +71,7 @@
+