diff --git a/common/test/acceptance/pages/common/utils.py b/common/test/acceptance/pages/common/utils.py index 9418235143..80f15a9c2c 100644 --- a/common/test/acceptance/pages/common/utils.py +++ b/common/test/acceptance/pages/common/utils.py @@ -1,35 +1,57 @@ """ Utility methods common to Studio and the LMS. """ -from bok_choy.promise import EmptyPromise +from bok_choy.promise import BrokenPromise from common.test.acceptance.tests.helpers import disable_animations from selenium.webdriver.common.action_chains import ActionChains -def wait_for_notification(page): +def sync_on_notification(page, style='default', wait_for_hide=False): """ - Waits for the "mini-notification" to appear and disappear on the given page (subclass of PageObject). + Sync on notifications but do not raise errors. + + A BrokenPromise in the wait_for probably means that we missed it. + We should just swallow this error and not raise it for reasons including: + * We are not specifically testing this functionality + * This functionality is covered by unit tests + * This verification method is prone to flakiness + and browser version dependencies + + See classes in edx-platform: + lms/static/sass/elements/_system-feedback.scss """ - def _is_saving(): - """Whether or not the notification is currently showing.""" - return page.q(css='.wrapper-notification-mini.is-shown').present + hiding_class = 'is-hiding' + shown_class = 'is-shown' - def _is_saving_done(): - """Whether or not the notification is finished showing.""" - return page.q(css='.wrapper-notification-mini.is-hiding').present + def notification_has_class(style, el_class): + """ + Return a boolean representing whether + the notification has the class applied. + """ + if style == 'mini': + css_string = '.wrapper-notification-mini.{}' + else: + css_string = '.wrapper-notification-confirmation.{}' + return page.q(css=css_string.format(el_class)).present - EmptyPromise( - _is_saving, - 'Notification should have been shown.', - try_interval=0.1, - timeout=60, - ).fulfill() - EmptyPromise( - _is_saving_done, - 'Notification should have been hidden.', - try_interval=0.1, - timeout=60, - ).fulfill() + # Wait for the notification to show. + # This notification appears very quickly and maybe missed. Don't raise an error. + try: + page.wait_for( + lambda: notification_has_class(style, shown_class), + 'Notification should have been shown.', + timeout=5 + ) + except BrokenPromise as _err: + pass + + # Now wait for it to hide. + # This is not required for web page interaction, so not really needed. + if wait_for_hide: + page.wait_for( + lambda: notification_has_class(style, hiding_class), + 'Notification should have hidden.' + ) def click_css(page, css, source_index=0, require_notification=True): @@ -53,7 +75,7 @@ def click_css(page, css, source_index=0, require_notification=True): page.q(css=css).filter(_is_visible).nth(source_index).click() if require_notification: - wait_for_notification(page) + sync_on_notification(page) # Some buttons trigger ajax posts # (e.g. .add-missing-groups-button as configured in split_test_author_view.js) diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py index e11c91e472..007f1080ad 100644 --- a/common/test/acceptance/pages/studio/library.py +++ b/common/test/acceptance/pages/studio/library.py @@ -11,7 +11,7 @@ from common.test.acceptance.pages.studio.users import UsersPageMixin from common.test.acceptance.pages.studio.pagination import PaginatedMixin from selenium.webdriver.common.keys import Keys from common.test.acceptance.pages.studio.utils import HelpMixin -from common.test.acceptance.pages.common.utils import confirm_prompt, wait_for_notification +from common.test.acceptance.pages.common.utils import confirm_prompt, sync_on_notification from common.test.acceptance.pages.studio import BASE_URL @@ -92,7 +92,7 @@ class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin): Click on the duplicate button for the given XBlock """ self._action_btn_for_xblock_id(xblock_id, "duplicate").click() - wait_for_notification(self) + sync_on_notification(self) self.wait_for_ajax() def click_delete_button(self, xblock_id, confirm=True): @@ -101,7 +101,7 @@ class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin): """ self._action_btn_for_xblock_id(xblock_id, "delete").click() if confirm: - confirm_prompt(self) # this will also wait_for_notification() + confirm_prompt(self) # this will also sync_on_notification() self.wait_for_ajax() def _get_xblocks(self): diff --git a/common/test/acceptance/pages/studio/settings.py b/common/test/acceptance/pages/studio/settings.py index d214ec6192..7ba3d8fc88 100644 --- a/common/test/acceptance/pages/studio/settings.py +++ b/common/test/acceptance/pages/studio/settings.py @@ -285,6 +285,8 @@ class SettingsPage(CoursePage): '#alert-confirmation-title', 'Save confirmation message is visible' ) + # After visibility an ajax call is in process, waiting for that to complete + self.wait_for_ajax() def refresh_page(self, wait_for_confirmation=True): """ diff --git a/common/test/acceptance/pages/studio/settings_graders.py b/common/test/acceptance/pages/studio/settings_graders.py index 3cb3e516aa..df92e7c78f 100644 --- a/common/test/acceptance/pages/studio/settings_graders.py +++ b/common/test/acceptance/pages/studio/settings_graders.py @@ -2,10 +2,10 @@ Course Grading Settings page. """ -from common.test.acceptance.pages.studio.course_page import CoursePage +from common.test.acceptance.pages.studio.settings import SettingsPage -class GradingPage(CoursePage): +class GradingPage(SettingsPage): """ Course Grading Settings page. """ @@ -14,3 +14,91 @@ class GradingPage(CoursePage): def is_browser_on_page(self): return self.q(css='body.grading').present + + def letter_grade(self, selector): + """ + Returns: first letter of grade range on grading page + Example: if there are no manually added grades it would + return Pass, if a grade is added it will return 'A' + """ + return self.q(css=selector)[0].text + + def add_new_grade(self): + """ + Add new grade + """ + self.q(css='.new-grade-button').click() + self.save_changes() + + def remove_grade(self): + """ + Remove an added grade + """ + # Button displays after hovering on it + btn_css = '.remove-button' + self.browser.execute_script("$('{}').focus().click()".format(btn_css)) + self.wait_for_ajax() + self.save_changes() + + def remove_all_grades(self): + """ + Removes all grades + """ + while len(self.q(css='.remove-button')) > 0: + self.remove_grade() + + def add_new_assignment_type(self): + """ + Add New Assignment type + """ + self.q(css='.add-grading-data').click() + self.save_changes() + + def fill_assignment_type_fields( + self, + name, + abbreviation, + total_grade, + total_number, + drop + ): + """ + Fills text to Assignment Type fields according to assignment box + number and text provided + + Arguments: + name: Assignment Type Name + abbreviation: Abbreviation + total_grade: Weight of Total Grade + total_number: Total Number + drop: Number of Droppable + """ + self.q(css='#course-grading-assignment-name').fill(name) + self.q(css='#course-grading-assignment-shortname').fill(abbreviation) + self.q(css='#course-grading-assignment-gradeweight').fill(total_grade) + self.q( + css='#course-grading-assignment-totalassignments' + ).fill(total_number) + + self.q(css='#course-grading-assignment-droppable').fill(drop) + self.save_changes() + + def assignment_name_field_value(self): + """ + Returns: Assignment type field value + """ + return self.q(css='#course-grading-assignment-name').attrs('value') + + def delete_assignment_type(self): + """ + Deletes Assignment type + """ + self.q(css='.remove-grading-data').first.click() + self.save_changes() + + def delete_all_assignment_types(self): + """ + Deletes all assignment types + """ + while len(self.q(css='.remove-grading-data')) > 0: + self.delete_assignment_type() diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index 2a29899709..17eaea68c7 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -6,7 +6,7 @@ from selenium.webdriver.common.keys import Keys from bok_choy.javascript import js_defined from bok_choy.promise import EmptyPromise -from common.test.acceptance.pages.common.utils import click_css, wait_for_notification +from common.test.acceptance.pages.common.utils import click_css, sync_on_notification NAV_HELP_NOT_SIGNED_IN_CSS = '.nav-item.nav-not-signedin-help a' @@ -103,7 +103,7 @@ def add_component(page, item_type, specific_type, is_advanced_problem=False): all_options = page.q(css='.new-component-{} ul.new-component-template li button span'.format(item_type)) chosen_option = all_options.filter(text=specific_type).first chosen_option.click() - wait_for_notification(page) + sync_on_notification(page) page.wait_for_ajax() @@ -219,7 +219,7 @@ def drag(page, source_index, target_index, placeholder_height=0): action.release(target).perform() else: action.release().perform() - wait_for_notification(page) + sync_on_notification(page) def verify_ordering(test_class, page, expected_orderings): diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py index 974be6fe39..36109dc809 100644 --- a/common/test/acceptance/pages/studio/video/video.py +++ b/common/test/acceptance/pages/studio/video/video.py @@ -8,7 +8,7 @@ from bok_choy.promise import EmptyPromise, Promise from bok_choy.javascript import wait_for_js, js_defined from common.test.acceptance.tests.helpers import YouTubeStubConfig from common.test.acceptance.pages.lms.video.video import VideoPage -from common.test.acceptance.pages.common.utils import wait_for_notification +from common.test.acceptance.pages.common.utils import sync_on_notification from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.action_chains import ActionChains @@ -160,7 +160,7 @@ class VideoComponentPage(VideoPage): """ self.q(css=BUTTON_SELECTORS[button_name]).nth(index).click() if require_notification: - wait_for_notification(self) + sync_on_notification(self) self.wait_for_ajax() @staticmethod